arcane-os 0.42.2 → 0.44.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.42.2",
3
+ "version": "0.44.0",
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",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
87
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
88
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
89
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
90
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
91
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
92
92
  "check": "node tools/check-source.mjs && npm test",
@@ -31,12 +31,16 @@
31
31
  let content=null;
32
32
  let fileManager=null;
33
33
  let operationGeneration=0;
34
+ let openController = null;
35
+ let closingTask = null;
34
36
  let destroyed=false;
35
37
 
36
38
  host.beforeOpen=is.function(host.beforeOpen)
37
39
  ?host.beforeOpen
38
40
  :async function beforeOpen(){};
39
41
  host.open=openData;
42
+ host.close = closeData;
43
+ host.printPreview=printPreview;
40
44
  host.destroy=destroy;
41
45
 
42
46
  openButton.innerText=host.dataset.button||'View Data';
@@ -52,80 +56,159 @@
52
56
  {signal:lifecycleController.signal}
53
57
  );
54
58
 
55
- async function openData(){
56
- if(destroyed){
59
+ dataModal.addEventListener(
60
+ 'modal-closed',
61
+ function closeDataFromModal(event) {
62
+ if (event.target === dataModal && !closingTask) {
63
+ closeData();
64
+ }
65
+ },
66
+ {signal: lifecycleController.signal}
67
+ );
68
+
69
+ async function openData() {
70
+ if (destroyed) {
57
71
  return false;
58
72
  }
59
- const generation=++operationGeneration;
60
- await host.beforeOpen();
61
- if(destroyed||generation!==operationGeneration){
73
+ const requestedGeneration=operationGeneration;
74
+ if (closingTask && !await closingTask) {
62
75
  return false;
63
76
  }
64
-
65
- const reload=Boolean(content);
66
-
67
- const modal=await waitForComponent(
68
- dataModal,
69
- {
70
- methods:['populate','open'],
71
- property:'ready',
72
- event:'modal-ready',
73
- signal:lifecycleController.signal
74
- }
75
- );
76
- if(destroyed||generation!==operationGeneration){
77
+ if (destroyed||requestedGeneration!==operationGeneration) {
77
78
  return false;
78
79
  }
80
+ const generation = ++operationGeneration;
81
+ openController?.abort('data-view-open-replaced');
82
+ const controller = new AbortController();
83
+ openController = controller;
84
+
85
+ try {
86
+ await host.beforeOpen();
87
+ if (destroyed || generation !== operationGeneration) {
88
+ return false;
89
+ }
79
90
 
80
- if(!content){
81
- content=document.createElement('section');
82
- fileManager=document.createElement('html-import');
91
+ const reload = Boolean(content);
92
+ const modal = await waitForComponent(
93
+ dataModal,
94
+ {
95
+ methods: ['populate', 'open'],
96
+ property: 'ready',
97
+ event: 'modal-ready',
98
+ signal: controller.signal
99
+ }
100
+ );
101
+ if (destroyed || generation !== operationGeneration) {
102
+ return false;
103
+ }
83
104
 
84
- const heading=document.createElement('h1');
105
+ if (!content) {
106
+ content = document.createElement('section');
107
+ fileManager = document.createElement('html-import');
108
+
109
+ const heading = document.createElement('h1');
110
+
111
+ heading.innerText = host.dataset.title || 'Data';
112
+ fileManager.className = 'file-manager';
113
+ forwardPreviewOptions();
114
+ fileManager.dataset.dirs = host.dataset.dirs || '';
115
+ fileManager.dataset.layout = host.dataset.layout || 'files';
116
+ fileManager.setAttribute(
117
+ 'href',
118
+ new URL('./file-manager.html', componentHref).href
119
+ );
120
+
121
+ content.append(heading, fileManager);
122
+ await modal.populate(content, false);
123
+ if (destroyed || generation !== operationGeneration) {
124
+ return false;
125
+ }
126
+ }
85
127
 
86
- heading.innerText=host.dataset.title||'Data';
87
- fileManager.className='file-manager';
88
- forwardPreviewOptions();
89
- fileManager.dataset.dirs=host.dataset.dirs||'';
90
- fileManager.dataset.layout=host.dataset.layout||'files';
91
- fileManager.setAttribute(
92
- 'href',
93
- new URL('./file-manager.html',componentHref).href
128
+ if (reload) {
129
+ forwardPreviewOptions();
130
+ }
131
+
132
+ const manager = await waitForComponent(
133
+ fileManager,
134
+ {
135
+ methods: ['loadAll'],
136
+ property: 'ready',
137
+ event: 'file-manager-ready',
138
+ signal: controller.signal
139
+ }
94
140
  );
141
+ if (destroyed || generation !== operationGeneration) {
142
+ return false;
143
+ }
95
144
 
96
- content.append(heading,fileManager);
97
- await modal.populate(content,false);
98
- if(destroyed||generation!==operationGeneration){
145
+ if (reload) {
146
+ await manager.loadAll();
147
+ if (destroyed || generation !== operationGeneration) {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ return await modal.open();
153
+ } catch (error) {
154
+ if (controller.signal.aborted) {
99
155
  return false;
100
156
  }
157
+ throw error;
158
+ } finally {
159
+ if (openController === controller) {
160
+ openController = null;
161
+ }
101
162
  }
163
+ }
102
164
 
103
- if(reload){
104
- forwardPreviewOptions();
165
+ async function closeData() {
166
+ if (destroyed) {
167
+ return false;
168
+ }
169
+ operationGeneration += 1;
170
+ openController?.abort('data-view-closed');
171
+ openController = null;
172
+ if (closingTask) {
173
+ return closingTask;
105
174
  }
106
175
 
107
- const manager=await waitForComponent(
108
- fileManager,
109
- {
110
- methods:['loadAll'],
111
- property:'ready',
112
- event:'file-manager-ready',
113
- signal:lifecycleController.signal
114
- }
115
- );
116
- if(destroyed||generation!==operationGeneration){
117
- return false;
176
+ const childClosing = is.function(fileManager?.close)
177
+ ? fileManager.close()
178
+ : Promise.resolve(true);
179
+ closingTask = Promise.resolve(childClosing)
180
+ .then(closeDataAfterChildren)
181
+ .catch(
182
+ function reportDataCloseFailure(error) {
183
+ arcaneLogging.error('Unable to close data view:', error);
184
+ return false;
185
+ }
186
+ );
187
+ try {
188
+ return await closingTask;
189
+ } finally {
190
+ closingTask = null;
118
191
  }
192
+ }
119
193
 
120
- if(reload){
121
- await manager.loadAll();
122
- if(destroyed||generation!==operationGeneration){
194
+ async function closeDataAfterChildren(childrenClosed) {
195
+ if (childrenClosed === false || destroyed) {
196
+ return false;
197
+ }
198
+ if (is.function(dataModal.close)) {
199
+ const closed = await dataModal.close();
200
+ if (closed === false) {
123
201
  return false;
124
202
  }
125
203
  }
204
+ return !destroyed && dataModal.opened !== true;
205
+ }
126
206
 
127
- modal.open();
128
- return true;
207
+ async function printPreview(){
208
+ if(destroyed||dataModal.opened!==true||!is.function(fileManager?.printPreview)){
209
+ return false;
210
+ }
211
+ return fileManager.printPreview();
129
212
  }
130
213
 
131
214
  function forwardPreviewOptions(){
@@ -146,6 +229,8 @@
146
229
  }
147
230
  destroyed=true;
148
231
  operationGeneration+=1;
232
+ openController?.abort('data-view-destroyed');
233
+ openController = null;
149
234
  lifecycleController.abort('data-view-destroyed');
150
235
  fileManager?.destroy?.();
151
236
  dataModal.destroy?.();
@@ -469,8 +469,15 @@
469
469
  directory=>directory.trim()
470
470
  ).filter(Boolean);
471
471
  const fileEntityPromise=import('../entities/File.js');
472
+ const printModulePromise=import('../modules/PrintView.js');
473
+ void printModulePromise.catch(function reportPrintModuleFailure(error){
474
+ arcaneLogging.error('Unable to load rendered printing:',error);
475
+ });
472
476
 
473
477
  let activePreview=null;
478
+ let presentationController = new AbortController();
479
+ let uploadPresentationController = null;
480
+ let closingTask = null;
474
481
  let markdownModulePromise=null;
475
482
  let uploadDirectory='';
476
483
  let initialized=false;
@@ -497,6 +504,8 @@
497
504
  host.setDirectoryFilter=setDirectoryFilter;
498
505
  host.select=select;
499
506
  host.clearSelection=clearSelection;
507
+ host.close = closeFileManager;
508
+ host.printPreview=printPreview;
500
509
  host.destroy=destroy;
501
510
 
502
511
  Object.defineProperty(
@@ -1457,22 +1466,20 @@
1457
1466
  }
1458
1467
  }
1459
1468
 
1460
- async function openDirectory(directory=''){
1461
- if(destroyed||!isVisibleDirectory(directory)){
1469
+ async function openDirectory(directory = '', signal = presentationController.signal) {
1470
+ if (closingTask && !await closingTask) {
1471
+ return false;
1472
+ }
1473
+ if(destroyed||signal.aborted||!isVisibleDirectory(directory)){
1462
1474
  return false;
1463
1475
  }
1464
1476
  const generation=++directoryOpenGeneration;
1465
- const modal=await waitForComponent(
1477
+ const modal = await waitForModal(
1466
1478
  directoryModal,
1467
- {
1468
- methods:['populate','open'],
1469
- property:'ready',
1470
- event:'modal-ready',
1471
- signal:lifecycleController.signal
1472
- },
1473
- {signal:lifecycleController.signal}
1479
+ ['populate', 'open'],
1480
+ signal
1474
1481
  );
1475
- if(destroyed||generation!==directoryOpenGeneration||!isVisibleDirectory(directory)){
1482
+ if(!modal||destroyed||generation!==directoryOpenGeneration||!isVisibleDirectory(directory)){
1476
1483
  return false;
1477
1484
  }
1478
1485
  const keys=await dbopfs.getAllKeys(directory);
@@ -1534,7 +1541,11 @@
1534
1541
  }
1535
1542
 
1536
1543
  async function openFile(directory='',fileName=''){
1537
- if(destroyed){
1544
+ const signal = presentationController.signal;
1545
+ if (closingTask && !await closingTask) {
1546
+ return false;
1547
+ }
1548
+ if(destroyed||signal.aborted){
1538
1549
  return false;
1539
1550
  }
1540
1551
  releasePreview(activePreview,'file-preview-replaced');
@@ -1543,7 +1554,12 @@
1543
1554
  controller:new AbortController(),
1544
1555
  generation,
1545
1556
  urls:new Set(),
1546
- media:new Set()
1557
+ media:new Set(),
1558
+ title:`${directory}/${fileName}`,
1559
+ rendered:null,
1560
+ printPreparation:null,
1561
+ printRetains:0,
1562
+ released:false
1547
1563
  };
1548
1564
  activePreview=preview;
1549
1565
  let modal;
@@ -1577,8 +1593,20 @@
1577
1593
  const deleteButton=document.createElement('button');
1578
1594
  const deleteIcon=document.createElement('img');
1579
1595
  const status=document.createElement('p');
1596
+ const printButton=document.createElement('button');
1580
1597
 
1581
1598
  heading.innerText=`${directory}/${fileName}`;
1599
+ printButton.type='button';
1600
+ printButton.textContent='Print';
1601
+ printButton.disabled=true;
1602
+ preview.printButton=printButton;
1603
+ preview.printStatus=status;
1604
+ preview.container=content;
1605
+ printButton.addEventListener('click',function printOpenPreview(){
1606
+ void printPreview().catch(function reportPrintRejection(error){
1607
+ reportPreviewPrintFailure(error,preview);
1608
+ });
1609
+ },{signal:preview.controller.signal});
1582
1610
  actions.className='file-view-actions';
1583
1611
  deleteButton.type='button';
1584
1612
  deleteButton.className='file-view-delete';
@@ -1598,7 +1626,7 @@
1598
1626
  };
1599
1627
  status.innerText='Opening file...';
1600
1628
  status.setAttribute('role','status');
1601
- actions.append(deleteButton);
1629
+ actions.append(printButton,deleteButton);
1602
1630
  content.append(
1603
1631
  styles,
1604
1632
  heading,
@@ -1631,6 +1659,10 @@
1631
1659
  }
1632
1660
  status.remove();
1633
1661
  content.append(rendered);
1662
+ preview.rendered=rendered;
1663
+ void preparePreviewPrint(preview).catch(function reportPrintPreparation(error){
1664
+ reportPreviewPrintFailure(error,preview);
1665
+ });
1634
1666
  return true;
1635
1667
  }catch(error){
1636
1668
  if(isCurrentPreview(preview)){
@@ -1643,6 +1675,69 @@
1643
1675
  }
1644
1676
  }
1645
1677
 
1678
+ function preparePreviewPrint(preview){
1679
+ if(!preview.printPreparation){
1680
+ preview.printPreparation=printModulePromise.then(function registerPreviewPrint({createPrintView}){
1681
+ if(!isCurrentPreview(preview)||!preview.rendered){
1682
+ return null;
1683
+ }
1684
+ const view=createPrintView({
1685
+ host,
1686
+ content:()=>preview.rendered,
1687
+ title:()=>preview.title,
1688
+ active:()=>isCurrentPreview(preview)&&fileModal.opened===true,
1689
+ priority:1,
1690
+ signal:preview.controller.signal,
1691
+ retain:()=>retainPreviewPrintResources(preview),
1692
+ onError:error=>reportPreviewPrintFailure(error,preview)
1693
+ });
1694
+ preview.printButton.disabled=false;
1695
+ return view;
1696
+ });
1697
+ }
1698
+ return preview.printPreparation;
1699
+ }
1700
+
1701
+ async function printPreview(){
1702
+ const preview=activePreview;
1703
+ if(!preview?.rendered||!isCurrentPreview(preview)||fileModal.opened!==true){
1704
+ return false;
1705
+ }
1706
+ const view=await preparePreviewPrint(preview);
1707
+ return view&&isCurrentPreview(preview)?view.print():false;
1708
+ }
1709
+
1710
+ function reportPreviewPrintFailure(error,preview){
1711
+ arcaneLogging.error('Unable to print file preview:',error);
1712
+ if(isCurrentPreview(preview)){
1713
+ preview.printStatus.textContent='Unable to open print preview. Please try again.';
1714
+ preview.printStatus.setAttribute('role','alert');
1715
+ preview.container.append(preview.printStatus);
1716
+ }
1717
+ }
1718
+
1719
+ function retainPreviewPrintResources(preview){
1720
+ preview.printRetains+=1;
1721
+ let retained=true;
1722
+ return function releasePrintedPreviewResources(){
1723
+ if(!retained){
1724
+ return;
1725
+ }
1726
+ retained=false;
1727
+ preview.printRetains-=1;
1728
+ if(preview.released&&preview.printRetains===0){
1729
+ releasePreviewURLs(preview);
1730
+ }
1731
+ };
1732
+ }
1733
+
1734
+ function releasePreviewURLs(preview){
1735
+ for(const url of preview.urls){
1736
+ URL.revokeObjectURL(url);
1737
+ }
1738
+ preview.urls.clear();
1739
+ }
1740
+
1646
1741
  function configureFileModal(modal){
1647
1742
  const appearance=getComputedStyle(host);
1648
1743
  for(const property of ['--modal-width','--modal-max-height','--modal-padding']){
@@ -1660,9 +1755,10 @@
1660
1755
  }
1661
1756
 
1662
1757
  function releasePreview(preview,reason){
1663
- if(!preview){
1758
+ if(!preview||preview.released){
1664
1759
  return;
1665
1760
  }
1761
+ preview.released=true;
1666
1762
  preview.controller.abort(reason);
1667
1763
  for(const element of preview.media){
1668
1764
  if(element.localName==='audio'){
@@ -1676,11 +1772,10 @@
1676
1772
  element.load();
1677
1773
  }
1678
1774
  }
1679
- for(const url of preview.urls){
1680
- URL.revokeObjectURL(url);
1775
+ if(preview.printRetains===0){
1776
+ releasePreviewURLs(preview);
1681
1777
  }
1682
1778
  preview.media.clear();
1683
- preview.urls.clear();
1684
1779
  if(activePreview===preview){
1685
1780
  activePreview=null;
1686
1781
  }
@@ -2091,8 +2186,12 @@
2091
2186
 
2092
2187
  openButton.addEventListener(
2093
2188
  'click',
2094
- async()=>{
2189
+ async function openSelectedFile() {
2190
+ const signal = presentationController.signal;
2095
2191
  await select(entry.path,{entry:entry});
2192
+ if (destroyed || signal.aborted) {
2193
+ return;
2194
+ }
2096
2195
  const detail={path:entry.path,entry:entry,trigger:'click'};
2097
2196
  const operationId=nextOperationId('open');
2098
2197
  const openAllowed=emit('file-manager-open',detail,true,operationId);
@@ -2130,21 +2229,21 @@
2130
2229
  }
2131
2230
 
2132
2231
  async function confirmDelete(directory='',fileName='',closeFile=false){
2133
- if(destroyed||!directory||!fileName){
2232
+ const signal = presentationController.signal;
2233
+ if (closingTask && !await closingTask) {
2234
+ return false;
2235
+ }
2236
+ if(destroyed||signal.aborted||!directory||!fileName){
2134
2237
  return false;
2135
2238
  }
2136
2239
  const generation=++deleteGeneration;
2137
2240
 
2138
- const modal=await waitForComponent(
2241
+ const modal = await waitForModal(
2139
2242
  deleteModal,
2140
- {
2141
- methods:['populate','open','close'],
2142
- property:'ready',
2143
- event:'modal-ready',
2144
- signal:lifecycleController.signal
2145
- }
2243
+ ['populate', 'open', 'close'],
2244
+ signal
2146
2245
  );
2147
- if(destroyed||generation!==deleteGeneration){
2246
+ if(!modal||destroyed||generation!==deleteGeneration){
2148
2247
  return false;
2149
2248
  }
2150
2249
  const content=document.createElement('section');
@@ -2168,12 +2267,19 @@
2168
2267
  deleteButton.addEventListener(
2169
2268
  'click',
2170
2269
  async function deleteSelectedFile(){
2270
+ if (destroyed || signal.aborted || generation !== deleteGeneration) {
2271
+ return;
2272
+ }
2171
2273
  cancelButton.disabled=true;
2172
2274
  deleteButton.disabled=true;
2173
2275
 
2174
2276
  try{
2175
2277
  await dbopfs.delete(directory,fileName);
2176
- if(destroyed||generation!==deleteGeneration){
2278
+ if(destroyed){
2279
+ return;
2280
+ }
2281
+ if (generation !== deleteGeneration) {
2282
+ await loadAll();
2177
2283
  return;
2178
2284
  }
2179
2285
  await modal.close(undefined,true);
@@ -2201,7 +2307,9 @@
2201
2307
  message.innerText=`Unable to delete ${fileName}.`;
2202
2308
  cancelButton.disabled=false;
2203
2309
  deleteButton.disabled=false;
2204
- arcaneLogging.error('Unable to delete file:',error);
2310
+ }
2311
+ if (!destroyed) {
2312
+ arcaneLogging.error('Unable to delete file:', error);
2205
2313
  }
2206
2314
  }
2207
2315
  },
@@ -2230,6 +2338,7 @@
2230
2338
  }
2231
2339
 
2232
2340
  uploadDirectory=directory;
2341
+ uploadPresentationController = presentationController;
2233
2342
  fileUpload.click();
2234
2343
  }
2235
2344
 
@@ -2239,6 +2348,8 @@
2239
2348
  }
2240
2349
  const files=Array.from(fileUpload.files||[]);
2241
2350
  const directory=uploadDirectory;
2351
+ const presentation = uploadPresentationController || presentationController;
2352
+ uploadPresentationController = null;
2242
2353
  fileUpload.value='';
2243
2354
 
2244
2355
  if(!files.length||!directory){
@@ -2269,8 +2380,8 @@
2269
2380
  return false;
2270
2381
  }
2271
2382
 
2272
- if(layout==='grid'){
2273
- await openDirectory(directory);
2383
+ if(layout==='grid' && !presentation.signal.aborted){
2384
+ await openDirectory(directory, presentation.signal);
2274
2385
  }
2275
2386
 
2276
2387
  const failed=results.find(
@@ -2309,6 +2420,76 @@
2309
2420
  }
2310
2421
  }
2311
2422
 
2423
+ async function waitForModal(modal, methods, signal) {
2424
+ try {
2425
+ return await waitForComponent(
2426
+ modal,
2427
+ {
2428
+ methods,
2429
+ property: 'ready',
2430
+ event: 'modal-ready',
2431
+ signal
2432
+ }
2433
+ );
2434
+ } catch (error) {
2435
+ if (signal.aborted) {
2436
+ return null;
2437
+ }
2438
+ throw error;
2439
+ }
2440
+ }
2441
+
2442
+ async function closeFileManager() {
2443
+ if (destroyed) {
2444
+ return false;
2445
+ }
2446
+ presentationController.abort('file-manager-closed');
2447
+ presentationController = new AbortController();
2448
+ treeOpenGeneration += 1;
2449
+ directoryOpenGeneration += 1;
2450
+ deleteGeneration += 1;
2451
+ selectionGeneration += 1;
2452
+ // Cancel unfinished opens now; a settled preview remains usable until
2453
+ // its modal actually closes, including when a child refuses to close.
2454
+ if (!activePreview?.rendered) {
2455
+ fileOpenGeneration += 1;
2456
+ releasePreview(activePreview, 'file-manager-closed');
2457
+ }
2458
+ if (closingTask) {
2459
+ return closingTask;
2460
+ }
2461
+
2462
+ closingTask = Promise.resolve().then(closeOwnedDialogs);
2463
+ try {
2464
+ return await closingTask;
2465
+ } finally {
2466
+ closingTask = null;
2467
+ }
2468
+ }
2469
+
2470
+ async function closeOwnedDialogs() {
2471
+ try {
2472
+ for (const modal of [deleteModal, fileModal, directoryModal]) {
2473
+ if (destroyed) {
2474
+ return false;
2475
+ }
2476
+ if (is.function(modal.close)) {
2477
+ const closed = await modal.close();
2478
+ if (closed === false) {
2479
+ return false;
2480
+ }
2481
+ }
2482
+ if (modal.opened === true) {
2483
+ return false;
2484
+ }
2485
+ }
2486
+ return !destroyed;
2487
+ } catch (error) {
2488
+ arcaneLogging.error('Unable to close file manager:', error);
2489
+ return false;
2490
+ }
2491
+ }
2492
+
2312
2493
  function destroy(){
2313
2494
  if(destroyed){
2314
2495
  return false;
@@ -2317,6 +2498,7 @@
2317
2498
  initialized=false;
2318
2499
  host.ready=false;
2319
2500
  lifecycleController.abort('file-manager-destroyed');
2501
+ presentationController.abort('file-manager-destroyed');
2320
2502
  treeLoadSequence+=1;
2321
2503
  gridLoadSequence+=1;
2322
2504
  providerGeneration+=1;