arcane-os 0.4.0 → 0.4.2
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/CHANGELOG.md +14 -0
- package/README.md +7 -7
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +83 -2
- package/package.json +1 -1
- package/runtime/arcane/components/chat.html +36 -2
- package/runtime/arcane/components/directory-picker.html +1 -5
- package/runtime/arcane/modules/DirectoryPicker.js +24 -40
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
- Added transparent Browser-WASM model loading status across cache checks,
|
|
6
|
+
ordered model-file downloads, and WebGPU initialization, including file
|
|
7
|
+
counts, elapsed time, and five-second heartbeats so long downloads remain
|
|
8
|
+
visibly active without byte-based progress.
|
|
9
|
+
|
|
10
|
+
## 0.4.1
|
|
11
|
+
|
|
12
|
+
- Preserved complete DirectoryPicker titles, paths, caller options, and
|
|
13
|
+
provider result fields without application clipping, trimming, freezing, or
|
|
14
|
+
generic path-character policy while retaining selected/cancelled semantics
|
|
15
|
+
and provider-owned path failures.
|
|
16
|
+
|
|
3
17
|
## 0.4.0
|
|
4
18
|
|
|
5
19
|
- Preserved complete finite provider progress values and units as
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.4.
|
|
22
|
+
This checkout defines the `0.4.2` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -126,7 +126,7 @@ uses the same controller for automatic memory extraction.
|
|
|
126
126
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
npx arcane-os@0.4.
|
|
129
|
+
npx arcane-os@0.4.2 new my-app --path ./my-app --target portable --git
|
|
130
130
|
cd my-app
|
|
131
131
|
npm install
|
|
132
132
|
npm run check
|
|
@@ -137,7 +137,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
137
137
|
missing Arcane files:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
npm install --save-dev --save-exact arcane-os@0.4.
|
|
140
|
+
npm install --save-dev --save-exact arcane-os@0.4.2
|
|
141
141
|
npm exec -- arcane init my-app --target portable
|
|
142
142
|
```
|
|
143
143
|
|
|
@@ -153,7 +153,7 @@ npm exec -- arcane-os targets
|
|
|
153
153
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
154
154
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
155
155
|
|
|
156
|
-
Use `npx arcane-os@0.4.
|
|
156
|
+
Use `npx arcane-os@0.4.2` for the initial bootstrap because it names this npm
|
|
157
157
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
158
158
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
159
159
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -174,7 +174,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
174
174
|
|
|
175
175
|
# From the generated app repository
|
|
176
176
|
cd ../local-app
|
|
177
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.4.
|
|
177
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.4.2.tgz
|
|
178
178
|
npm run check
|
|
179
179
|
npm ci
|
|
180
180
|
```
|
|
@@ -184,7 +184,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
184
184
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
185
185
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
186
186
|
runner also needs that tarball at the locked path. After publication, replace
|
|
187
|
-
the local declaration with the exact `arcane-os@0.4.
|
|
187
|
+
the local declaration with the exact `arcane-os@0.4.2` registry package and
|
|
188
188
|
commit the regenerated lock.
|
|
189
189
|
|
|
190
190
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -322,7 +322,7 @@ package installation, or assertions.
|
|
|
322
322
|
|
|
323
323
|
## Current target support
|
|
324
324
|
|
|
325
|
-
Version `0.4.
|
|
325
|
+
Version `0.4.2` exposes one browser target and five explicitly paired
|
|
326
326
|
native development targets: a non-runnable portable directory, a
|
|
327
327
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
328
328
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -24,6 +24,7 @@ const WEBGPU_ADAPTER_SELECTED_EVENT = "arcane.ai.browser-wasm.webgpu.adapter.sel
|
|
|
24
24
|
const WEBGPU_ADAPTER_SELECTION_PROTOCOL = "arcane-ai-webgpu-adapter-selection/1";
|
|
25
25
|
const CHROME_HIGH_PERFORMANCE_GPU_FLAG_URL =
|
|
26
26
|
"chrome://flags/#force-high-performance-gpu";
|
|
27
|
+
const MODEL_LOAD_HEARTBEAT_MS = 5_000;
|
|
27
28
|
const INTEL_VENDOR_ID = 0x8086;
|
|
28
29
|
const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
|
|
29
30
|
let highPerformanceGpuNoticeShown = false;
|
|
@@ -561,7 +562,10 @@ export function createDbopfsModelStore({
|
|
|
561
562
|
}
|
|
562
563
|
}
|
|
563
564
|
|
|
564
|
-
async function install(source, { signal } = {}) {
|
|
565
|
+
async function install(source, { signal, onProgress = null } = {}) {
|
|
566
|
+
if (onProgress !== null && typeof onProgress !== "function") {
|
|
567
|
+
throw new TypeError("Model store onProgress must be a function or null.");
|
|
568
|
+
}
|
|
565
569
|
const names = storageName(source);
|
|
566
570
|
const members = sourceMetadata(source).files;
|
|
567
571
|
await remove(source);
|
|
@@ -569,6 +573,13 @@ export function createDbopfsModelStore({
|
|
|
569
573
|
try {
|
|
570
574
|
for (let index = 0; index < members.length; index += 1) {
|
|
571
575
|
const member = members[index];
|
|
576
|
+
onProgress?.(completeValue({
|
|
577
|
+
phase: "download",
|
|
578
|
+
completed: index,
|
|
579
|
+
total: members.length,
|
|
580
|
+
unit: "files",
|
|
581
|
+
heartbeat: false,
|
|
582
|
+
}));
|
|
572
583
|
const opened = await source.open(index, { signal });
|
|
573
584
|
try {
|
|
574
585
|
await write(names.models[index].name, opened.body, { signal });
|
|
@@ -585,6 +596,13 @@ export function createDbopfsModelStore({
|
|
|
585
596
|
throw error;
|
|
586
597
|
}
|
|
587
598
|
}
|
|
599
|
+
onProgress?.(completeValue({
|
|
600
|
+
phase: "download",
|
|
601
|
+
completed: members.length,
|
|
602
|
+
total: members.length,
|
|
603
|
+
unit: "files",
|
|
604
|
+
heartbeat: false,
|
|
605
|
+
}));
|
|
588
606
|
return completeValue({
|
|
589
607
|
files: completeValue(modelFiles),
|
|
590
608
|
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
@@ -598,8 +616,20 @@ export function createDbopfsModelStore({
|
|
|
598
616
|
async function ensure(source, {
|
|
599
617
|
signal,
|
|
600
618
|
onCapabilityPolicy,
|
|
619
|
+
onProgress = null,
|
|
601
620
|
offline = false,
|
|
602
621
|
} = {}) {
|
|
622
|
+
if (onProgress !== null && typeof onProgress !== "function") {
|
|
623
|
+
throw new TypeError("Model store onProgress must be a function or null.");
|
|
624
|
+
}
|
|
625
|
+
const total = sourceMetadata(source).files.length;
|
|
626
|
+
onProgress?.(completeValue({
|
|
627
|
+
phase: "cache-check",
|
|
628
|
+
completed: 0,
|
|
629
|
+
total,
|
|
630
|
+
unit: "files",
|
|
631
|
+
heartbeat: false,
|
|
632
|
+
}));
|
|
603
633
|
const cached = await openCached(source, { signal });
|
|
604
634
|
if (cached) {
|
|
605
635
|
const storage = await storagePolicy({ cached: true });
|
|
@@ -611,7 +641,7 @@ export function createDbopfsModelStore({
|
|
|
611
641
|
}
|
|
612
642
|
const storage = await storagePolicy();
|
|
613
643
|
onCapabilityPolicy?.(storage);
|
|
614
|
-
const installed = await install(source, { signal });
|
|
644
|
+
const installed = await install(source, { signal, onProgress });
|
|
615
645
|
const admittedStorage = await storagePolicy({ cached: true });
|
|
616
646
|
onCapabilityPolicy?.(admittedStorage);
|
|
617
647
|
return completeValue({ ...installed, cache: "installed", storage: admittedStorage });
|
|
@@ -2068,16 +2098,54 @@ export function createBrowserWasmLlmProvider({
|
|
|
2068
2098
|
activeSecurity = effectiveSecurity;
|
|
2069
2099
|
return loadPromise;
|
|
2070
2100
|
}
|
|
2101
|
+
if (
|
|
2102
|
+
options.onProgress !== undefined
|
|
2103
|
+
&& options.onProgress !== null
|
|
2104
|
+
&& typeof options.onProgress !== "function"
|
|
2105
|
+
) {
|
|
2106
|
+
throw new TypeError("Browser-WASM load onProgress must be a function or null.");
|
|
2107
|
+
}
|
|
2071
2108
|
const externalSignal = options.signal ?? context.signal ?? null;
|
|
2072
2109
|
const linked = linkAbortSignal(externalSignal);
|
|
2073
2110
|
const signal = linked.controller.signal;
|
|
2074
2111
|
const generation = ++lifecycleGeneration;
|
|
2112
|
+
const reportProgress = typeof context.reportProgress === "function"
|
|
2113
|
+
? context.reportProgress
|
|
2114
|
+
: options.onProgress ?? null;
|
|
2115
|
+
const progressStartedAt = Date.now();
|
|
2116
|
+
let currentProgress = null;
|
|
2117
|
+
let progressHeartbeat = null;
|
|
2118
|
+
|
|
2119
|
+
function publishModelLoadProgress(progress) {
|
|
2120
|
+
if (!reportProgress) return;
|
|
2121
|
+
currentProgress = { ...progress, heartbeat: false };
|
|
2122
|
+
reportProgress(completeValue({
|
|
2123
|
+
...currentProgress,
|
|
2124
|
+
elapsedMs: Math.max(0, Date.now() - progressStartedAt),
|
|
2125
|
+
}));
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
function publishModelLoadHeartbeat() {
|
|
2129
|
+
if (!reportProgress || !currentProgress) return;
|
|
2130
|
+
reportProgress(completeValue({
|
|
2131
|
+
...currentProgress,
|
|
2132
|
+
heartbeat: true,
|
|
2133
|
+
elapsedMs: Math.max(0, Date.now() - progressStartedAt),
|
|
2134
|
+
}));
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2075
2137
|
loadAbort = linked.controller;
|
|
2076
2138
|
activeSource = requestedSource;
|
|
2077
2139
|
activeSecurity = effectiveSecurity;
|
|
2078
2140
|
activeLoadPlan = requestedLoadPlan;
|
|
2079
2141
|
state = "loading";
|
|
2080
2142
|
errorState = null;
|
|
2143
|
+
if (reportProgress) {
|
|
2144
|
+
progressHeartbeat = globalThis.setInterval(
|
|
2145
|
+
publishModelLoadHeartbeat,
|
|
2146
|
+
MODEL_LOAD_HEARTBEAT_MS,
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2081
2149
|
loadPromise = (async () => {
|
|
2082
2150
|
try {
|
|
2083
2151
|
throwIfAborted(signal, "load");
|
|
@@ -2085,6 +2153,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2085
2153
|
signal,
|
|
2086
2154
|
offline: options.offline === true,
|
|
2087
2155
|
onCapabilityPolicy: (value) => { storagePolicies.set(activeSource.id, value); },
|
|
2156
|
+
onProgress: publishModelLoadProgress,
|
|
2088
2157
|
});
|
|
2089
2158
|
cacheState = admitted.cache;
|
|
2090
2159
|
throwIfAborted(signal, "load");
|
|
@@ -2096,6 +2165,13 @@ export function createBrowserWasmLlmProvider({
|
|
|
2096
2165
|
throw fail("ARCANE_AI_OPERATION_SUPERSEDED", "The model load was superseded by unload.");
|
|
2097
2166
|
}
|
|
2098
2167
|
const members = sourceMetadata(activeSource).files;
|
|
2168
|
+
publishModelLoadProgress({
|
|
2169
|
+
phase: "initialize",
|
|
2170
|
+
completed: members.length,
|
|
2171
|
+
total: members.length,
|
|
2172
|
+
unit: "files",
|
|
2173
|
+
heartbeat: false,
|
|
2174
|
+
});
|
|
2099
2175
|
const modelFiles = admitted.files.map((file, index) => (
|
|
2100
2176
|
typeof globalThis.File === "function"
|
|
2101
2177
|
? new File([file], members[index].name, { type: "application/octet-stream" })
|
|
@@ -2148,6 +2224,10 @@ export function createBrowserWasmLlmProvider({
|
|
|
2148
2224
|
}
|
|
2149
2225
|
throw normalized;
|
|
2150
2226
|
} finally {
|
|
2227
|
+
if (progressHeartbeat !== null) {
|
|
2228
|
+
globalThis.clearInterval(progressHeartbeat);
|
|
2229
|
+
progressHeartbeat = null;
|
|
2230
|
+
}
|
|
2151
2231
|
if (loadAbort === linked.controller) loadAbort = null;
|
|
2152
2232
|
linked.release();
|
|
2153
2233
|
loadPromise = null;
|
|
@@ -2478,6 +2558,7 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2478
2558
|
...loadOptions,
|
|
2479
2559
|
modelId: selection.modelId,
|
|
2480
2560
|
signal,
|
|
2561
|
+
onProgress: progress,
|
|
2481
2562
|
...(security?.secure===true?{security:{secure:true}}:{}),
|
|
2482
2563
|
});
|
|
2483
2564
|
return status();
|
package/package.json
CHANGED
|
@@ -2072,17 +2072,51 @@
|
|
|
2072
2072
|
};
|
|
2073
2073
|
}
|
|
2074
2074
|
|
|
2075
|
+
function progressElapsedText(){
|
|
2076
|
+
const elapsedMilliseconds=Number(role?.progress?.elapsedMs);
|
|
2077
|
+
if(!Number.isFinite(elapsedMilliseconds)||elapsedMilliseconds<0){
|
|
2078
|
+
return '';
|
|
2079
|
+
}
|
|
2080
|
+
const elapsedSeconds=Math.floor(elapsedMilliseconds/1000);
|
|
2081
|
+
const hours=Math.floor(elapsedSeconds/3600);
|
|
2082
|
+
const minutes=Math.floor(elapsedSeconds%3600/60);
|
|
2083
|
+
const seconds=elapsedSeconds%60;
|
|
2084
|
+
if(hours>0){
|
|
2085
|
+
return `${hours}h ${minutes}m elapsed`;
|
|
2086
|
+
}
|
|
2087
|
+
if(minutes>0){
|
|
2088
|
+
return `${minutes}m ${String(seconds).padStart(2,'0')}s elapsed`;
|
|
2089
|
+
}
|
|
2090
|
+
return `${seconds}s elapsed`;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2075
2093
|
function progressMessage(){
|
|
2076
2094
|
const measured=determinateProgress();
|
|
2077
|
-
const
|
|
2095
|
+
const phaseValue=typeof role?.progress?.phase==='string'&&role.progress.phase
|
|
2078
2096
|
?role.progress.phase
|
|
2079
2097
|
:'loading';
|
|
2098
|
+
const phase=phaseValue.replaceAll('-',' ');
|
|
2080
2099
|
const parts=[
|
|
2081
2100
|
`Loading ${modelDisplayName()} through the Arcane SDK`,
|
|
2082
2101
|
phase
|
|
2083
2102
|
];
|
|
2084
2103
|
if(measured){
|
|
2085
|
-
|
|
2104
|
+
if(
|
|
2105
|
+
phaseValue==='download'
|
|
2106
|
+
&&measured.unit==='files'
|
|
2107
|
+
&&measured.completed<measured.total
|
|
2108
|
+
){
|
|
2109
|
+
parts.push(`part ${measured.completed+1} of ${measured.total}`);
|
|
2110
|
+
}else{
|
|
2111
|
+
parts.push(`${measured.completed} of ${measured.total} ${measured.unit}`);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const elapsed=progressElapsedText();
|
|
2115
|
+
if(elapsed){
|
|
2116
|
+
parts.push(elapsed);
|
|
2117
|
+
if(role?.progress?.heartbeat===true){
|
|
2118
|
+
parts.push('Still working');
|
|
2119
|
+
}
|
|
2086
2120
|
}
|
|
2087
2121
|
parts.push('The first activation can take several minutes; keep this tab open');
|
|
2088
2122
|
return parts.join(' · ');
|
|
@@ -165,11 +165,7 @@
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function normalizeValue(value=''){
|
|
168
|
-
|
|
169
|
-
if(/[\u0000-\u001f\u007f]/.test(next)){
|
|
170
|
-
throw new TypeError('The directory path must be plain text.');
|
|
171
|
-
}
|
|
172
|
-
return next;
|
|
168
|
+
return `${value??''}`;
|
|
173
169
|
}
|
|
174
170
|
|
|
175
171
|
function setDisabled(value=false){
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
const PATH_MAX_LENGTH=4096;
|
|
3
|
-
const CONTROL_CHARACTERS=/[\u0000-\u001f\u007f]/;
|
|
4
|
-
|
|
5
|
-
function isPlainRecord(value){
|
|
1
|
+
function isRecord(value){
|
|
6
2
|
return Boolean(value)
|
|
7
3
|
&&typeof value==='object'
|
|
8
|
-
&&!Array.isArray(value)
|
|
9
|
-
&&Object.getPrototypeOf(value)===Object.prototype;
|
|
4
|
+
&&!Array.isArray(value);
|
|
10
5
|
}
|
|
11
6
|
|
|
12
7
|
function coded(error,code){
|
|
@@ -14,37 +9,27 @@ function coded(error,code){
|
|
|
14
9
|
return error;
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
function optionalText(value,label
|
|
18
|
-
if(value===undefined||value===null
|
|
12
|
+
function optionalText(value,label){
|
|
13
|
+
if(value===undefined||value===null) return value;
|
|
19
14
|
if(typeof value!=='string') throw new TypeError(`${label} must be a string when provided.`);
|
|
20
|
-
|
|
21
|
-
if(!normalized) return null;
|
|
22
|
-
if(normalized.length>maximum) throw new RangeError(`${label} exceeds ${maximum} characters.`);
|
|
23
|
-
if(CONTROL_CHARACTERS.test(normalized)) throw new TypeError(`${label} cannot contain control characters.`);
|
|
24
|
-
return normalized;
|
|
15
|
+
return value;
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
function normalizeDirectoryPickerOptions(input={}){
|
|
28
|
-
if(!
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
...(initialPath?{initialPath}:{}),
|
|
38
|
-
});
|
|
19
|
+
if(!isRecord(input)) throw new TypeError('Directory picker options must be an object.');
|
|
20
|
+
const normalized={...input};
|
|
21
|
+
if(Object.prototype.hasOwnProperty.call(input,'title')){
|
|
22
|
+
normalized.title=optionalText(input.title,'title');
|
|
23
|
+
}
|
|
24
|
+
if(Object.prototype.hasOwnProperty.call(input,'initialPath')){
|
|
25
|
+
normalized.initialPath=optionalText(input.initialPath,'initialPath');
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
39
28
|
}
|
|
40
29
|
|
|
41
30
|
function normalizeDirectorySelection(input){
|
|
42
|
-
const keys=isPlainRecord(input)?Object.keys(input):[];
|
|
43
31
|
if(
|
|
44
|
-
!
|
|
45
|
-
||keys.length!==2
|
|
46
|
-
||!keys.includes('cancelled')
|
|
47
|
-
||!keys.includes('path')
|
|
32
|
+
!isRecord(input)
|
|
48
33
|
||typeof input.cancelled!=='boolean'
|
|
49
34
|
){
|
|
50
35
|
throw coded(
|
|
@@ -53,17 +38,15 @@ function normalizeDirectorySelection(input){
|
|
|
53
38
|
);
|
|
54
39
|
}
|
|
55
40
|
if(input.cancelled){
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
return Object.freeze({cancelled:true,path:null});
|
|
41
|
+
return {
|
|
42
|
+
...input,
|
|
43
|
+
cancelled:true,
|
|
44
|
+
path:input.path===undefined?null:input.path,
|
|
45
|
+
};
|
|
63
46
|
}
|
|
64
47
|
let path;
|
|
65
48
|
try{
|
|
66
|
-
path=optionalText(input.path,'The selected directory path'
|
|
49
|
+
path=optionalText(input.path,'The selected directory path');
|
|
67
50
|
}catch(error){
|
|
68
51
|
throw coded(error,'DIRECTORY_PICKER_INVALID_RESULT');
|
|
69
52
|
}
|
|
@@ -73,7 +56,7 @@ function normalizeDirectorySelection(input){
|
|
|
73
56
|
'DIRECTORY_PICKER_INVALID_RESULT',
|
|
74
57
|
);
|
|
75
58
|
}
|
|
76
|
-
return
|
|
59
|
+
return {...input,cancelled:false,path};
|
|
77
60
|
}
|
|
78
61
|
|
|
79
62
|
/**
|
|
@@ -81,7 +64,8 @@ function normalizeDirectorySelection(input){
|
|
|
81
64
|
*
|
|
82
65
|
* This wrapper does not enumerate directories, persist a selected path, or use
|
|
83
66
|
* a browser file picker. The injected provider must expose
|
|
84
|
-
* `selectDirectory(options)` and return `
|
|
67
|
+
* `selectDirectory(options)` and return a record with `cancelled` and `path`.
|
|
68
|
+
* Caller options and additional provider result fields pass through unchanged.
|
|
85
69
|
*/
|
|
86
70
|
export default class DirectoryPicker{
|
|
87
71
|
constructor(provider=globalThis.Arcane?.filesystem){
|