arcane-os 0.5.11 → 0.5.13

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 (36) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +8 -8
  3. package/docs/architecture.md +1 -1
  4. package/docs/reference/README.md +5 -5
  5. package/docs/reference/ai/browser-speech.md +73 -7
  6. package/docs/reference/ai/twin-cloud.md +1 -1
  7. package/docs/reference/asset-versioning.md +69 -0
  8. package/docs/reference/cli.md +1 -1
  9. package/docs/reference/core/arcane-ai-contracts.md +2 -3
  10. package/docs/reference/core/arcane-api.md +5 -5
  11. package/docs/reference/core/ollama-module.md +1 -1
  12. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +2 -2
  13. package/docs/reference/inventory/package-api.json +1 -1
  14. package/docs/reference/inventory/runtime-components.json +1 -1
  15. package/docs/reference/inventory/runtime-modules.json +3 -3
  16. package/docs/reference/protocols.md +5 -5
  17. package/docs/reference/runtime-components.md +5 -2
  18. package/docs/reference/runtime-modules.md +39 -4
  19. package/docs/reference/sdk-api.md +6 -6
  20. package/package.json +1 -1
  21. package/runtime/arcane/components/calculator.html +1 -1
  22. package/runtime/arcane/components/chart.html +2 -7
  23. package/runtime/arcane/components/dashboard-config.html +3 -8
  24. package/runtime/arcane/components/file-manager.html +6 -6
  25. package/runtime/arcane/components/markdown-document.html +1 -6
  26. package/runtime/arcane/components/markdown-editor.html +2 -7
  27. package/runtime/arcane/components/media-embed.html +1 -1
  28. package/runtime/arcane/components/screen-capture.html +1 -1
  29. package/runtime/arcane/components/voice-transcription.html +4 -9
  30. package/runtime/arcane/modules/AI.js +59 -10
  31. package/runtime/arcane/modules/HTMLImport.js +184 -13
  32. package/src/dev-server.mjs +72 -5
  33. package/src/import-map.mjs +548 -16
  34. package/src/installed-sdk-runtime.mjs +1 -0
  35. package/src/packager/core.mjs +60 -1
  36. package/src/workspace-runtime.mjs +19 -7
@@ -18,7 +18,7 @@ This table is the Node `package.json#exports` map: it defines package
18
18
  entrypoints for SDK/tooling code. It is distinct from the generated browser
19
19
  import map that resolves application-facing `arcane/*` modules and the focused
20
20
  EventManager entry. See [browser runtime delivery](protocols.md#browser-runtime-delivery)
21
- for the installed-inventory-derived physical-runtime contract in SDK `0.5.11`.
21
+ for the installed-inventory-derived physical-runtime contract in SDK `0.5.12`.
22
22
 
23
23
  | Specifier | Purpose |
24
24
  | --- | --- |
@@ -748,7 +748,7 @@ deterministic map. The package root also contains the public
748
748
  {
749
749
  schemaVersion: 1,
750
750
  kind: 'arcane-app-runtime-projection',
751
- sdkVersion: '0.5.11',
751
+ sdkVersion: '0.5.12',
752
752
  pathPrefix: 'arcane/',
753
753
  files: [{path}]
754
754
  }
@@ -2667,7 +2667,7 @@ The import-map operation also reports the stable operation-specific strings
2667
2667
  `ARCANE_IMPORT_MAP_INVALID`, `ARCANE_IMPORT_MAP_UNRESOLVED`, and
2668
2668
  `ARCANE_IMPORT_MAP_COLLISION`; package assembly can additionally report
2669
2669
  `ARCANE_IMPORT_MAP_CLEANUP_FAILED`. They are normalized `ArcaneError.code`
2670
- values, but are not properties added to this general registry in SDK `0.5.11`.
2670
+ values, but are not properties added to this general registry in SDK `0.5.12`.
2671
2671
 
2672
2672
  ### Value and import
2673
2673
 
@@ -3437,7 +3437,7 @@ workspace it additionally returns the exact installed package authority:
3437
3437
  packageSource,
3438
3438
  canonicalPackageRoot,
3439
3439
  packageName: 'arcane-os',
3440
- packageVersion: '0.5.11',
3440
+ packageVersion: '0.5.12',
3441
3441
  runtimeRoot,
3442
3442
  browserRuntimeRoot
3443
3443
  }
@@ -3445,9 +3445,9 @@ workspace it additionally returns the exact installed package authority:
3445
3445
  ```
3446
3446
 
3447
3447
  The dependency can be named `arcane-os` or be one exact npm alias for
3448
- `npm:arcane-os@0.5.11`. The selected installation must still be one direct,
3448
+ `npm:arcane-os@0.5.12`. The selected installation must still be one direct,
3449
3449
  physical, non-link package directory whose manifest identifies exactly as
3450
- `arcane-os@0.5.11`; duplicate canonical/alias declarations reject.
3450
+ `arcane-os@0.5.12`; duplicate canonical/alias declarations reject.
3451
3451
  `allowMissingManagedImportMap` is an internal packaging/development seam. An
3452
3452
  ordinary caller should leave it `false`.
3453
3453
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -1,7 +1,7 @@
1
1
  <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;max-width:34rem}.calculator{display:grid;gap:var(--arcane-space-3);padding:var(--arcane-space-4)}.display{display:grid;gap:var(--arcane-space-2)}.display input{font:600 1.3rem/1.4 ui-monospace,monospace;text-align:right}.result{min-height:2.5rem;margin:0;text-align:right;font-size:1.8rem;font-weight:750;overflow-wrap:anywhere}.keys{display:grid;gap:var(--arcane-space-2);grid-template-columns:repeat(5,1fr)}.keys button{min-width:0;padding:.7rem .35rem}.status{min-height:1.4rem;margin:0;color:var(--arcane-muted)}@media(max-width:28rem){.keys{grid-template-columns:repeat(4,1fr)}}</style>
3
3
  <section class="calculator arcane-card"><div class="display arcane-field"><label for="expression">Expression</label><input id="expression" inputmode="decimal" autocomplete="off" spellcheck="false" value=""><p id="result" class="result" aria-live="polite">0</p></div><div id="keys" class="keys" aria-label="Calculator keypad"></div><p id="status" class="status" role="status"></p></section>
4
- <script type="module">const host=this,root=host.shadowRoot,input=root.querySelector('#expression'),result=root.querySelector('#result'),status=root.querySelector('#status'),keys=root.querySelector('#keys');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/calculator.html',document.baseURI);const {default:CalculatorEngine}=await import(new URL('../modules/CalculatorEngine.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.calculator',eventTypes:['calculation-complete','calculation-error','calculator-ready']}),lifecycleController=new AbortController();const engine=new CalculatorEngine(),definitions=['7','8','9','/','sqrt(','4','5','6','*','^','1','2','3','-','%','0','.','(',')','+','pi','e','C','⌫','='];let destroyed=false,operationSequence=0;
4
+ <script type="module">const host=this,root=host.shadowRoot,input=root.querySelector('#expression'),result=root.querySelector('#result'),status=root.querySelector('#status'),keys=root.querySelector('#keys');const {default:CalculatorEngine}=await import('../modules/CalculatorEngine.js');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.calculator',eventTypes:['calculation-complete','calculation-error','calculator-ready']}),lifecycleController=new AbortController();const engine=new CalculatorEngine(),definitions=['7','8','9','/','sqrt(','4','5','6','*','^','1','2','3','-','%','0','.','(',')','+','pi','e','C','⌫','='];let destroyed=false,operationSequence=0;
5
5
  for(const value of definitions){const button=document.createElement('button');button.type='button';button.className=value==='='?'arcane-button':'arcane-button arcane-button--secondary';button.textContent=value;button.dataset.value=value;keys.append(button)}
6
6
  keys.addEventListener('click',event=>{const value=event.target.closest('[data-value]')?.dataset.value;if(!value)return;if(value==='C'){input.value='';result.textContent='0';status.textContent='';return}if(value==='⌫'){input.value=input.value.slice(0,-1);return}if(value==='='){calculate();return}input.value+=value;input.focus()},{signal:lifecycleController.signal});input.addEventListener('keydown',event=>{if(event.key==='Enter'){event.preventDefault();calculate()}},{signal:lifecycleController.signal});
7
7
  function nextOperationId(){operationSequence+=1;return `${events.descriptor.instanceId}:calculate:${operationSequence}`}
@@ -162,16 +162,11 @@
162
162
 
163
163
  <script type="module">
164
164
  const host=this;
165
- const componentURL=new URL(
166
- host.getAttribute('href')||'./arcane/components/chart.html',
167
- document.baseURI
168
- );
169
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
170
- const {default:loadChartLibrary}=await import(moduleURL('ChartLibrary.js'));
165
+ const {default:loadChartLibrary}=await import('../modules/ChartLibrary.js');
171
166
  const {
172
167
  normalizeChartOptions,
173
168
  normalizeChartRows
174
- }=await import(moduleURL('ComponentContracts.js'));
169
+ }=await import('../modules/ComponentContracts.js');
175
170
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
176
171
  const events=createArcaneEventSource(host,{source:'arcane.component.chart',eventTypes:['chart-remove','chart-ready']});
177
172
  const lifecycleController=new AbortController();
@@ -11,18 +11,13 @@
11
11
 
12
12
  <script type="module">
13
13
  const host=this;
14
- const componentURL=new URL(
15
- host.getAttribute('href')||'./arcane/components/dashboard-config.html',
16
- document.baseURI
17
- );
18
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
19
- const {default:waitForComponent}=await import(moduleURL('WaitForComponent.js'));
14
+ const {default:waitForComponent}=await import('../modules/WaitForComponent.js');
20
15
  const {
21
16
  effectiveDashboardVisibility,
22
17
  normalizeDashboardDefinitions,
23
18
  normalizeDashboardOptions,
24
19
  normalizeDashboardVisibility
25
- }=await import(moduleURL('ComponentContracts.js'));
20
+ }=await import('../modules/ComponentContracts.js');
26
21
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
27
22
  const events=createArcaneEventSource(host,{source:'arcane.component.dashboard-config',eventTypes:['dashboard-config-closed','dashboard-config-opened','dashboard-config-change','dashboard-config-ready']});
28
23
  const lifecycleController=new AbortController();
@@ -232,7 +227,7 @@
232
227
  const items=document.createElement('section');
233
228
 
234
229
  styles.rel='stylesheet';
235
- styles.href='./arcane/css/dashboard-config.css?v=2';
230
+ styles.href=new URL('./arcane/css/dashboard-config.css?v=2',document.baseURI).href;
236
231
  heading.dataset.dashboardConfigHeading='';
237
232
  message.dataset.dashboardConfigDescription='';
238
233
  empty.className='dashboard-config-empty';
@@ -825,8 +825,8 @@
825
825
  icon.className='tree-entry-icon';
826
826
  icon.alt='';
827
827
  icon.src=entry.kind==='directory'
828
- ?'./arcane/img/folder.svg'
829
- :'./arcane/img/doc.svg';
828
+ ?new URL('./arcane/img/folder.svg',document.baseURI).href
829
+ :new URL('./arcane/img/doc.svg',document.baseURI).href;
830
830
  name.className='tree-entry-name';
831
831
  name.innerText=entry.name;
832
832
  count.className='tree-file-count';
@@ -1404,7 +1404,7 @@
1404
1404
  deleteButton.innerText='Delete File';
1405
1405
  deleteIcon.alt='';
1406
1406
  deleteIcon.className='action-icon';
1407
- deleteIcon.src='./arcane/img/trash.svg';
1407
+ deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1408
1408
  deleteButton.prepend(deleteIcon);
1409
1409
  deleteButton.addEventListener(
1410
1410
  'click',
@@ -1610,7 +1610,7 @@
1610
1610
 
1611
1611
  fileIcon.alt='';
1612
1612
  fileIcon.className='icon file-icon';
1613
- fileIcon.src='./arcane/img/doc.svg';
1613
+ fileIcon.src=new URL('./arcane/img/doc.svg',document.baseURI).href;
1614
1614
  fileDetails.className='file-details';
1615
1615
  fileLabel.className='file-name';
1616
1616
  fileLabel.innerText=fileName;
@@ -1633,7 +1633,7 @@
1633
1633
 
1634
1634
  deleteIcon.alt='';
1635
1635
  deleteIcon.className='action-icon';
1636
- deleteIcon.src='./arcane/img/trash.svg';
1636
+ deleteIcon.src=new URL('./arcane/img/trash.svg',document.baseURI).href;
1637
1637
 
1638
1638
  openButton.addEventListener(
1639
1639
  'click',
@@ -1933,7 +1933,7 @@
1933
1933
  directory.dataset.directory=tableName;
1934
1934
  folder.className='file folder';
1935
1935
  folderIcon.className='icon';
1936
- folderIcon.src='./arcane/img/folder.svg';
1936
+ folderIcon.src=new URL('./arcane/img/folder.svg',document.baseURI).href;
1937
1937
  folderName.innerText=tableName;
1938
1938
  fileCount.className='file-count';
1939
1939
  fileCount.innerText=`${files.length} ${files.length===1?'file':'files'}`;
@@ -214,12 +214,7 @@
214
214
  const host=this;
215
215
  host.ready=false;
216
216
 
217
- const componentURL=new URL(
218
- host.getAttribute('href')||'./arcane/components/markdown-document.html',
219
- document.baseURI
220
- );
221
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
222
- const {default:MD}=await import(moduleURL('MD.js?v=2'));
217
+ const {default:MD}=await import('../modules/MD.js?v=2');
223
218
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
224
219
  const events=createArcaneEventSource(host,{source:'arcane.component.markdown-document',eventTypes:['markdown-document-state','markdown-document-empty','markdown-document-error','markdown-document-loading','markdown-document-rendered','markdown-document-navigate','markdown-document-ready']});
225
220
  const lifecycleController=new AbortController();
@@ -75,16 +75,11 @@
75
75
 
76
76
  <script type="module">
77
77
  const host=this;
78
- const componentURL=new URL(
79
- host.getAttribute('href')||'./arcane/components/markdown-editor.html',
80
- document.baseURI
81
- );
82
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
83
- const {default:MD}=await import(moduleURL('MD.js?v=2'));
78
+ const {default:MD}=await import('../modules/MD.js?v=2');
84
79
  const {
85
80
  applyMarkdownFormat,
86
81
  normalizeMarkdownOptions
87
- }=await import(moduleURL('ComponentContracts.js'));
82
+ }=await import('../modules/ComponentContracts.js');
88
83
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
89
84
  const events=createArcaneEventSource(host,{source:'arcane.component.markdown-editor',eventTypes:['markdown-editor-change','markdown-editor-saved','markdown-editor-ready']});
90
85
  const lifecycleController=new AbortController();
@@ -1,7 +1,7 @@
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}.player-shell{display:grid;gap:var(--arcane-space-4)}.loader{display:flex;gap:var(--arcane-space-2)}.loader input{min-width:0;width:100%}.player{aspect-ratio:16/9;display:grid;place-items:center;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}iframe{width:100%;height:100%;border:0}.empty{text-align:center;color:var(--arcane-muted);padding:var(--arcane-space-5)}.footer{align-items:center;display:flex;gap:var(--arcane-space-3);justify-content:space-between}.status{margin:0;color:var(--arcane-muted)}@media(max-width:36rem){.loader,.footer{align-items:stretch;flex-direction:column}}</style>
3
3
  <section class="player-shell"><form id="loader" class="loader"><label class="visually-hidden" for="mediaUrl">Video or playlist address</label><input id="mediaUrl" type="text" inputmode="url" autocomplete="off" spellcheck="false" placeholder="Paste a YouTube video or playlist URL"><button class="arcane-button" type="submit">Play</button></form><div class="player"><div id="empty" class="empty"><strong>Choose something to play</strong><p>Paste a YouTube or YouTube Music video or playlist address.</p></div><iframe id="frame" title="Embedded media player" hidden allow="autoplay; encrypted-media; picture-in-picture; fullscreen" allowfullscreen></iframe></div><div class="footer"><p id="status" class="status" role="status" aria-live="polite">Nothing loaded.</p><button id="platform" class="arcane-button arcane-button--secondary" type="button">Open platform</button></div></section>
4
- <script type="module">const host=this,root=host.shadowRoot,form=root.querySelector('#loader'),input=root.querySelector('#mediaUrl'),frame=root.querySelector('#frame'),empty=root.querySelector('#empty'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/media-embed.html',document.baseURI);const {parseYouTubeMedia,youtubeEmbedUrl}=await import(new URL('../modules/YouTubeMedia.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.media-embed',eventTypes:['media-open-platform','media-load','media-error','media-embed-ready']}),lifecycleController=new AbortController();let platformUrl='https://www.youtube.com/',privacyEnhanced=false,destroyed=false,operationSequence=0;
4
+ <script type="module">const host=this,root=host.shadowRoot,form=root.querySelector('#loader'),input=root.querySelector('#mediaUrl'),frame=root.querySelector('#frame'),empty=root.querySelector('#empty'),status=root.querySelector('#status');const {parseYouTubeMedia,youtubeEmbedUrl}=await import('../modules/YouTubeMedia.js');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.media-embed',eventTypes:['media-open-platform','media-load','media-error','media-embed-ready']}),lifecycleController=new AbortController();let platformUrl='https://www.youtube.com/',privacyEnhanced=false,destroyed=false,operationSequence=0;
5
5
  function nextOperationId(kind){operationSequence+=1;return `${events.descriptor.instanceId}:${kind}:${operationSequence}`}
6
6
  function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail,operationId});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
7
7
  host.configure=options=>{if(destroyed)return false;platformUrl=String(options?.platformUrl||platformUrl);if(Object.hasOwn(options||{},'privacyEnhanced'))privacyEnhanced=options.privacyEnhanced===true;if(options?.placeholder)input.placeholder=String(options.placeholder);if(options?.openLabel)root.querySelector('#platform').textContent=String(options.openLabel);if(options?.initialValue){input.value=String(options.initialValue);load(input.value)}return host};host.load=load;host.destroy=destroy;
@@ -1,7 +1,7 @@
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}.capture{display:grid;gap:var(--arcane-space-4)}.actions{display:grid;gap:var(--arcane-space-3);grid-template-columns:repeat(3,minmax(0,1fr))}.action{display:grid;gap:var(--arcane-space-2);padding:var(--arcane-space-4);text-align:left}.action strong{font-size:1.05rem}.action span{color:var(--arcane-muted);font-size:.85rem}.recording{align-items:center;display:flex;gap:var(--arcane-space-3);justify-content:space-between;padding:var(--arcane-space-3);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-medium)}.preview{display:grid;place-items:center;min-height:16rem;overflow:auto;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}.preview :where(img,video){display:block}.empty{color:var(--arcane-muted);text-align:center}.status{margin:0;color:var(--arcane-muted)}@media(max-width:44rem){.actions{grid-template-columns:1fr}}</style>
3
3
  <section class="capture"><div class="actions"><button class="action arcane-card" type="button" data-capture="image"><strong>Screenshot</strong><span>Capture the selected display as PNG.</span></button><button class="action arcane-card" type="button" data-capture="video"><strong>Record video</strong><span>Record the selected display in a browser-supported video format.</span></button><button class="action arcane-card" type="button" data-capture="gif"><strong>Record GIF</strong><span>Record the selected display as an animated GIF.</span></button></div><div id="recording" class="recording" hidden><span id="recordingLabel">Recording…</span><button id="stop" class="arcane-button" type="button">Stop recording</button></div><div id="preview" class="preview"><div class="empty"><strong>No capture yet</strong><p>Your capture remains on this device unless you choose to save it.</p></div></div><p id="status" class="status" role="status" aria-live="polite">Choose a capture type. Your browser will ask which display to share.</p></section>
4
- <script type="module">const host=this,root=host.shadowRoot,preview=root.querySelector('#preview'),recording=root.querySelector('#recording'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/screen-capture.html',document.baseURI);const {default:ScreenCapture}=await import(new URL('../modules/ScreenCapture.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.screen-capture',eventTypes:['screen-capture-result','screen-capture-ready']});const capture=new ScreenCapture(),listeners=new AbortController();let destroyed=false,operationGeneration=0,previewUrl='';
4
+ <script type="module">const host=this,root=host.shadowRoot,preview=root.querySelector('#preview'),recording=root.querySelector('#recording'),status=root.querySelector('#status');const {default:ScreenCapture}=await import('../modules/ScreenCapture.js');const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.screen-capture',eventTypes:['screen-capture-result','screen-capture-ready']});const capture=new ScreenCapture(),listeners=new AbortController();let destroyed=false,operationGeneration=0,previewUrl='';
5
5
  function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{operationId,publicDetail});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
6
6
  function current(generation){return !destroyed&&generation===operationGeneration}
7
7
  function cleanupCaptureStart(originalError=null){const cleanupErrors=[];if(capture.recorder&&capture.recorder.state!=='inactive'){try{capture.recorder.stop()}catch(error){cleanupErrors.push(error)}}try{capture.stopTracks(capture.stream)}catch(error){cleanupErrors.push(error)}try{capture.reset()}catch(error){cleanupErrors.push(error)}for(const error of cleanupErrors)console.error('Error cleaning up rejected screen capture start:',error);return originalError}
@@ -84,21 +84,16 @@
84
84
  ]
85
85
  }
86
86
  );
87
- const componentURL=new URL(
88
- host.getAttribute('href')||'./arcane/components/voice-transcription.html',
89
- document.baseURI
90
- );
91
- const moduleURL=name=>new URL(`../modules/${name}`,componentURL).href;
92
- const {default:MD}=await import(moduleURL('MD.js?v=2'));
87
+ const {default:MD}=await import('../modules/MD.js?v=2');
93
88
  const {
94
89
  appendTranscription,
95
90
  createSTTActivationController,
96
91
  normalizeVoiceOptions
97
- }=await import(moduleURL('ComponentContracts.js'));
92
+ }=await import('../modules/ComponentContracts.js');
98
93
  const {
99
94
  requestAIRuntimeIntent,
100
95
  subscribeAIRuntimeState
101
- }=await import(moduleURL('AIRuntimeState.js'));
96
+ }=await import('../modules/AIRuntimeState.js');
102
97
  const description=host.shadowRoot.querySelector('#description');
103
98
  const transcriptElement=host.shadowRoot.querySelector('#transcript');
104
99
  const status=host.shadowRoot.querySelector('#status');
@@ -598,7 +593,7 @@
598
593
  return options.transcribe(file,context);
599
594
  }
600
595
  if(typeof globalThis.ai?.fetchSTT!=='function'){
601
- await import(moduleURL('AI.js'));
596
+ await import('../modules/AI.js');
602
597
  }
603
598
  if(typeof globalThis.ai?.fetchSTT!=='function'){
604
599
  const error=new Error('Speech transcription is not configured.');
@@ -5673,7 +5673,8 @@ class AI {
5673
5673
 
5674
5674
  streamTTS(
5675
5675
  text='',
5676
- end=false
5676
+ end=false,
5677
+ options={}
5677
5678
  ){
5678
5679
  if(this.muted){
5679
5680
  if(end){
@@ -5682,6 +5683,12 @@ class AI {
5682
5683
  return Promise.resolve(false);
5683
5684
  }
5684
5685
 
5686
+ const {voice,speed,waitForPlayback=false}=options;
5687
+ const pauseAfterMs=Number(options.pauseAfterMs??0);
5688
+ if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0){
5689
+ throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
5690
+ }
5691
+
5685
5692
  this.audioMessageChunks+=String(text||'');
5686
5693
  const outputs=this.#extractSpeechSegments(end);
5687
5694
 
@@ -5702,8 +5709,13 @@ class AI {
5702
5709
  const generation=this.speechGeneration;
5703
5710
  const jobs=[];
5704
5711
 
5705
- for(const output of outputs){
5706
- jobs.push(this.#queueSpeechJob(output,generation));
5712
+ for(const [index,output] of outputs.entries()){
5713
+ jobs.push(this.#queueSpeechJob(output,generation,{
5714
+ voice,
5715
+ speed,
5716
+ waitForPlayback,
5717
+ pauseAfterMs:index===outputs.length-1?pauseAfterMs:0
5718
+ }));
5707
5719
  }
5708
5720
 
5709
5721
  return Promise.all(jobs).then(
@@ -5857,7 +5869,7 @@ class AI {
5857
5869
  return this.#speechAbbreviations.has(token);
5858
5870
  }
5859
5871
 
5860
- #queueSpeechJob(text,generation){
5872
+ #queueSpeechJob(text,generation,options={}){
5861
5873
  const job={
5862
5874
  abortController:null,
5863
5875
  audioBuffer:null,
@@ -5867,13 +5879,22 @@ class AI {
5867
5879
  scheduledStart:null,
5868
5880
  sourceNode:null,
5869
5881
  state:'queued',
5870
- text
5882
+ text,
5883
+ voice:options.voice,
5884
+ speed:options.speed,
5885
+ pauseAfterMs:options.pauseAfterMs??0,
5886
+ resolvePlayback:null
5871
5887
  };
5872
5888
  const runtime=this;
5889
+ const playback=options.waitForPlayback===true
5890
+ ?new Promise(function captureSpeechPlaybackCompletion(resolve){
5891
+ job.resolvePlayback=resolve;
5892
+ })
5893
+ :null;
5873
5894
 
5874
5895
  this.speechJobs.push(job);
5875
5896
 
5876
- return Promise.resolve().then(
5897
+ const preparation=Promise.resolve().then(
5877
5898
  function synthesizeAvailableSpeech(){
5878
5899
  return runtime.#prepareSpeechJob(job);
5879
5900
  }
@@ -5886,6 +5907,7 @@ class AI {
5886
5907
  );
5887
5908
  }
5888
5909
  );
5910
+ return playback||preparation;
5889
5911
  }
5890
5912
 
5891
5913
  async #prepareSpeechJob(job){
@@ -5914,16 +5936,18 @@ class AI {
5914
5936
  async #requestSpeechAudio(job){
5915
5937
  job.abortController=new AbortController();
5916
5938
  const selection=this.#providerRuntime.selection('tts');
5917
- const voice=selection?this.#providerSpeechVoice():null;
5939
+ const voice=job.voice===undefined
5940
+ ?(selection?this.#providerSpeechVoice():null)
5941
+ :job.voice;
5918
5942
  const response=await this.fetchTTS(
5919
5943
  {
5920
5944
  model:selection?.modelId||this.modelTTS,
5921
5945
  input:job.text,
5922
- ...(voice?{voice}:{}),
5946
+ ...(job.voice!==undefined||voice?{voice}:{}),
5923
5947
  responseFormat:selection
5924
5948
  ?this.#providerSpeechResponseFormat()
5925
5949
  :this.audioFormat,
5926
- speed:this.voiceSpeed
5950
+ speed:job.speed===undefined?this.voiceSpeed:job.speed
5927
5951
  },
5928
5952
  job.abortController.signal
5929
5953
  );
@@ -6294,6 +6318,8 @@ class AI {
6294
6318
  for(const job of this.speechJobs){
6295
6319
  job.abortController?.abort();
6296
6320
  job.state='cancelled';
6321
+ job.resolvePlayback?.(false);
6322
+ job.resolvePlayback=null;
6297
6323
 
6298
6324
  if(job.sourceNode){
6299
6325
  job.sourceNode.onended=null;
@@ -6370,6 +6396,22 @@ class AI {
6370
6396
  if(attempt===this.speechResumeAttempt){
6371
6397
  this.speechResumePending=false;
6372
6398
  }
6399
+ if(context?.state==='closed'){
6400
+ this.#clearSpeechUnlock();
6401
+ const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
6402
+ return job.audioContext===context;
6403
+ });
6404
+ if(!jobs.length){
6405
+ this.#publishTTSFailure(error,{
6406
+ boundary:'playback-resume',
6407
+ generation:this.speechGeneration
6408
+ });
6409
+ }
6410
+ for(const job of jobs){
6411
+ this.#failSpeechJob(job,error,'playback-resume');
6412
+ }
6413
+ return false;
6414
+ }
6373
6415
  this.#waitForSpeechGesture(error,context);
6374
6416
  if(error?.name!=='NotAllowedError'){
6375
6417
  this.#publishTTSFailure(error,{
@@ -6543,7 +6585,7 @@ class AI {
6543
6585
  job.state='scheduled';
6544
6586
  job.scheduledStart=scheduledStart;
6545
6587
  job.scheduledEnd=hasKnownDuration
6546
- ?scheduledStart+duration
6588
+ ?scheduledStart+duration+(job.pauseAfterMs??0)/1000
6547
6589
  :null;
6548
6590
  job.sourceNode.__arcaneStarted=true;
6549
6591
  if(!this.currentSpeechJob){
@@ -6606,6 +6648,11 @@ class AI {
6606
6648
  job.sourceNode.onended=null;
6607
6649
  }
6608
6650
 
6651
+ if(job.scheduledEnd===null&&job.pauseAfterMs>0){
6652
+ this.speechScheduleContext=job.audioContext;
6653
+ this.speechScheduleTime=
6654
+ (Number(job.audioContext?.currentTime)||0)+job.pauseAfterMs/1000;
6655
+ }
6609
6656
  job.state='complete';
6610
6657
  this.#removeSpeechJob(job);
6611
6658
 
@@ -6727,6 +6774,8 @@ class AI {
6727
6774
  }
6728
6775
 
6729
6776
  #removeSpeechJob(job){
6777
+ job.resolvePlayback?.(job.state==='complete');
6778
+ job.resolvePlayback=null;
6730
6779
  const jobIndex=this.speechJobs.indexOf(job);
6731
6780
 
6732
6781
  if(jobIndex>=0){