arcane-os 0.59.1 → 0.61.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/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-image.mjs +360 -0
- package/browser-runtime/ai/core-model-assets.mjs +152 -0
- package/browser-runtime/core/client.mjs +8 -0
- package/browser-runtime/core/contracts.mjs +5 -0
- package/docs/reference/core/arcane-ai-contracts.md +17 -1
- package/docs/reference/core/arcane-api.md +12 -1
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +20 -11
- package/docs/reference/core/reference/arcane-api/namespaces.md +14 -0
- package/docs/reference/core-client.md +67 -0
- package/docs/reference/core-native-packaging.md +66 -2
- package/docs/reference/inventory/package-api.json +2 -2
- package/docs/reference/inventory/runtime-modules.json +2 -2
- package/docs/reference/local-image-editing.md +79 -0
- package/docs/reference/local-image-generation.md +267 -0
- package/docs/reference/model-assets.md +147 -0
- package/docs/reference/runtime-modules.md +37 -5
- package/docs/reference/sdk-api.md +41 -5
- package/package.json +8 -3
- package/runtime/arcane/modules/ThemeBootstrap.js +217 -2
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/hosts/windows/ArcaneHost.cs +211 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
- package/src/core/services/image.mjs +563 -0
- package/src/core/services/local-ai.mjs +8 -3
- package/src/core/services/model-assets.mjs +267 -0
- package/src/import-map.mjs +2 -0
- package/src/local-ai/config.mjs +41 -1
- package/src/local-ai/dev.mjs +46 -9
- package/src/local-ai/image-editing.mjs +159 -0
- package/src/local-ai/image-library.mjs +985 -0
- package/src/local-ai/image-models/sd14.mjs +15 -0
- package/src/local-ai/image-png.mjs +74 -0
- package/src/local-ai/image-runtime.mjs +336 -0
- package/src/local-ai/image-worker.mjs +218 -0
- package/src/local-ai/install.mjs +326 -10
- package/src/native/portable-layout.mjs +28 -2
- package/src/native/windows-icon.mjs +582 -0
- package/src/native/windows-provider.mjs +60 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Native working files from stored model assets
|
|
2
|
+
|
|
3
|
+
The selected `0.60.0` package contains the model-assets helpers as internal
|
|
4
|
+
preparation. The public imports and Core registration shown below belong to
|
|
5
|
+
the pending local-image integration and are unavailable through `0.60.0`.
|
|
6
|
+
These examples document that integration's intended public contract.
|
|
7
|
+
|
|
8
|
+
`prepareCoreModelAssets()` streams complete browser `File` or `Blob` members
|
|
9
|
+
through the existing Core connection into native working files. DBOPFS keeps
|
|
10
|
+
the authoritative originals. The helper performs no model download and changes
|
|
11
|
+
neither stored content nor the model format.
|
|
12
|
+
|
|
13
|
+
Use complete members returned by `createDbopfsModelStore().ensure()` or files
|
|
14
|
+
read from the application's existing DBOPFS owner. The browser model store
|
|
15
|
+
already assembles persisted Range parts into one logical Blob per upstream
|
|
16
|
+
member. Preserve that member order and the original relative filenames,
|
|
17
|
+
including companion files required by a model.
|
|
18
|
+
|
|
19
|
+
## Browser preparation
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import {prepareCoreModelAssets} from 'arcane-os/ai/core-model-assets';
|
|
23
|
+
import {createCoreImageRuntime} from 'arcane-os/ai/core-image';
|
|
24
|
+
|
|
25
|
+
const stored = await modelStore.ensure(selectedSource, {signal});
|
|
26
|
+
const projection = await prepareCoreModelAssets({
|
|
27
|
+
client,
|
|
28
|
+
workingDirectory: selectedWorkingDirectory,
|
|
29
|
+
members: [{path: 'sd-v1-4.ckpt', file: stored.files[0]}],
|
|
30
|
+
signal,
|
|
31
|
+
onProgress: renderPreparationProgress
|
|
32
|
+
});
|
|
33
|
+
const image = createCoreImageRuntime({client});
|
|
34
|
+
try {
|
|
35
|
+
await image.load({
|
|
36
|
+
model: 'sd14',
|
|
37
|
+
assetProjectionId: projection.id,
|
|
38
|
+
resourcePaths: {model: 'sd-v1-4.ckpt'},
|
|
39
|
+
signal
|
|
40
|
+
});
|
|
41
|
+
} finally {
|
|
42
|
+
// Native loading takes its own retain handle before using these files.
|
|
43
|
+
await projection.release();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await image.generate({
|
|
47
|
+
model: 'sd14',
|
|
48
|
+
prompt: 'A dignified raccoon who has been put in charge of the moon.',
|
|
49
|
+
signal
|
|
50
|
+
});
|
|
51
|
+
// Display or save the complete returned image through the application owner.
|
|
52
|
+
} finally {
|
|
53
|
+
await image.unload();
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This example assumes that the application selected the `sd14` model and its
|
|
58
|
+
complete checkpoint source. Model choice, download approval, prompts, output
|
|
59
|
+
storage and the selected working directory remain application-owned.
|
|
60
|
+
|
|
61
|
+
`prepareCoreModelAssets({client,workingDirectory,members,signal,onProgress})`
|
|
62
|
+
requires an available Core connection exposing the model-assets service.
|
|
63
|
+
`client` defaults to the installed Core client. Every member is
|
|
64
|
+
`{path,file}`, where `path` is its relative filename or companion-file path and
|
|
65
|
+
`file` is the complete stored Blob or File. Relative working-directory paths
|
|
66
|
+
resolve from the native service's `appRoot`; an application may explicitly
|
|
67
|
+
select an absolute working directory.
|
|
68
|
+
|
|
69
|
+
The result contains `id`, `directory`, ordered
|
|
70
|
+
`members: [{path,nativePath}]`, and an idempotent asynchronous `release()`.
|
|
71
|
+
The operation creates a unique child directory under the selected working
|
|
72
|
+
directory. Member paths identify files inside that child directory. Original
|
|
73
|
+
DBOPFS files remain untouched.
|
|
74
|
+
|
|
75
|
+
Each incoming Blob stream chunk is encoded only for the existing JSON Core
|
|
76
|
+
transport. The helper waits for the corresponding native write before reading
|
|
77
|
+
the next chunk of that member. Independent members transfer concurrently;
|
|
78
|
+
the complete member set is returned only after every write and file close
|
|
79
|
+
finishes. There is no complete-model transport buffer or application content
|
|
80
|
+
limit. Progress reports actual preparation phases and member paths.
|
|
81
|
+
|
|
82
|
+
Cancellation stops active readers and requests. Failure joins the participating
|
|
83
|
+
transfers and requests native cleanup; that cleanup joins actual file writes
|
|
84
|
+
before removing the projection. Cleanup uses its own uncancelled operation so
|
|
85
|
+
an already-aborted preparation signal cannot skip it. Original failures and
|
|
86
|
+
cleanup failures remain observable.
|
|
87
|
+
|
|
88
|
+
## Native ownership
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
import {createModelAssetService} from 'arcane-os/core/model-assets';
|
|
92
|
+
|
|
93
|
+
const modelAssets = createModelAssetService({appRoot});
|
|
94
|
+
// Register this same service with Core and pass it to the native engine owner.
|
|
95
|
+
const use = modelAssets.retain(projectionId);
|
|
96
|
+
try {
|
|
97
|
+
// Load and use the complete files from use.members.
|
|
98
|
+
await ownNativeModelLifetime(use.members);
|
|
99
|
+
} finally {
|
|
100
|
+
// ownNativeModelLifetime must join actual model release before returning.
|
|
101
|
+
await use.release();
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`createModelAssetService({appRoot})` returns a Core service with `current()`,
|
|
106
|
+
`retain(id)` and `dispose()`. `retain()` accepts only a completed projection
|
|
107
|
+
and returns `{id,directory,members,release}`. A native engine takes this handle
|
|
108
|
+
before loading and releases it after actual unload, failed-load cleanup or
|
|
109
|
+
worker exit. A rejected inference/load promise alone does not establish native
|
|
110
|
+
release. Multiple contexts can retain one prepared set, and repeated inference
|
|
111
|
+
on a retained context reuses its files.
|
|
112
|
+
|
|
113
|
+
The browser's `projection.release()` relinquishes preparation ownership. It
|
|
114
|
+
does not delete files retained by a native engine. Deletion follows the final
|
|
115
|
+
native release and removes only that operation's working directory. Core closes
|
|
116
|
+
independent services concurrently, so the model-assets service waits for all
|
|
117
|
+
native retain handles during disposal instead of assuming service order.
|
|
118
|
+
Owners must release their handles after joining their actual engine lifetime.
|
|
119
|
+
|
|
120
|
+
Closing a browser accessor does not by itself establish native unload.
|
|
121
|
+
Explicitly release unused prepared projections. Core disposal also releases
|
|
122
|
+
preparation ownership and cleans up after retained engine uses end.
|
|
123
|
+
|
|
124
|
+
`current()` returns `{closing,projections}`. Each projection reports its `id`,
|
|
125
|
+
`directory`, `state`, ordered `members`, `preparationOwned`, number of native
|
|
126
|
+
`uses`, and `error`. Core emits `modelAssets.state` for lifecycle changes.
|
|
127
|
+
|
|
128
|
+
## Core transport operations
|
|
129
|
+
|
|
130
|
+
| Method | Parameters | Result |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `modelAssets.open` | `{id,workingDirectory,members:[{path}]}` | Preparing projection and native member paths |
|
|
133
|
+
| `modelAssets.write` | `{id,memberIndex,contentBase64}` | `{id,memberIndex,written:true}` after the native write |
|
|
134
|
+
| `modelAssets.complete` | `{id}` | Ready projection after all files close |
|
|
135
|
+
| `modelAssets.release` | `{id}` | Released preparation ownership; native uses may retain the files |
|
|
136
|
+
| `modelAssets.status` | `{}` | Current projection lifecycle records |
|
|
137
|
+
|
|
138
|
+
The browser helper owns this transport sequence. Correlation IDs identify
|
|
139
|
+
operations; they are unrelated to content identity. Original model files remain
|
|
140
|
+
separate and complete. A retry creates a new working projection from the stored
|
|
141
|
+
assets; it does not redownload them or replace the authoritative DBOPFS record.
|
|
142
|
+
|
|
143
|
+
The implementation uses browser streams, the existing portable Core JSON
|
|
144
|
+
transport and Node filesystem operations. Native paths and cleanup follow the
|
|
145
|
+
host's Windows, Linux or macOS filesystem behavior. Android requires its host's
|
|
146
|
+
Core/filesystem adaptation. These are source-level platform contracts, not a
|
|
147
|
+
claim of execution on every target.
|
|
@@ -387,8 +387,12 @@ existing `audioFormat` when the catalog does not declare response formats. When
|
|
|
387
387
|
the selected model declares `speech.responseFormats`, that setting is used only when supported; if
|
|
388
388
|
the setting is the instance's `opus` default and the model rejects it, the catalog's
|
|
389
389
|
`speech.defaultResponseFormat` is used, while any other unsupported setting is
|
|
390
|
-
rejected. It propagates the caller-owned signal and returns a playable `Blob
|
|
391
|
-
|
|
390
|
+
rejected. It propagates the caller-owned signal and returns a playable `Blob`.
|
|
391
|
+
The native `LOCAL_SPEACH` route forwards that signal separately from its
|
|
392
|
+
synthesis payload to `Arcane.speech.synthesize(request, {signal})`; Core aborts
|
|
393
|
+
are normalized to `AbortError` with `ARCANE_AI_REQUEST_ABORTED`. Actual engine
|
|
394
|
+
interruption belongs to the registered service. It does not independently
|
|
395
|
+
choose a provider, cloud fallback, model, runtime, or
|
|
392
396
|
voice policy for the application. Every call removes repeated same formatting
|
|
393
397
|
marks from a cloned outbound input before delegation; the caller's payload stays
|
|
394
398
|
unchanged. The third `preparation` argument is reserved for SDK-internal
|
|
@@ -529,8 +533,11 @@ operation boundary, generation, and stable reason. Muting, explicit
|
|
|
529
533
|
cancellation, permission waiting, and superseded generations do not emit a
|
|
530
534
|
failure. The operation event does not rewrite provider readiness; the consuming
|
|
531
535
|
Chat/Speech surface owns its visible mute and recovery state.
|
|
532
|
-
`fetchSTT(audioFile,signal)` propagates the caller-owned signal
|
|
533
|
-
|
|
536
|
+
`fetchSTT(audioFile,signal)` propagates the caller-owned signal, including the
|
|
537
|
+
native `Arcane.speech.transcribe(request, {signal})` route. The native signal
|
|
538
|
+
remains separate from the complete audio request; Core aborts are normalized
|
|
539
|
+
to `AbortError` with `ARCANE_AI_REQUEST_ABORTED`.
|
|
540
|
+
Provider routes accept a `Blob` or `File` directly and leave media decoding,
|
|
534
541
|
PCM normalization, and WAV construction to the selected shared provider;
|
|
535
542
|
delivery suppression is guaranteed after abort, while underlying provider-stop
|
|
536
543
|
claims remain limited to that provider's cancellation contract.
|
|
@@ -4168,7 +4175,9 @@ creating a profile or migrating saved data.
|
|
|
4168
4175
|
Exact exports: `arcaneThemeReady`, `bootstrapArcaneTheme`, `default`, and
|
|
4169
4176
|
`disposeArcaneThemeBootstrap`.
|
|
4170
4177
|
|
|
4171
|
-
`disposeArcaneThemeBootstrap()` removes
|
|
4178
|
+
`disposeArcaneThemeBootstrap()` removes bootstrap-owned subscriptions,
|
|
4179
|
+
native-window theme observers and listeners, and cancels its pending window
|
|
4180
|
+
request.
|
|
4172
4181
|
The cache is a presentation hint, not the authority for preferences or user
|
|
4173
4182
|
data. Preference loading and User readiness independently reconcile it; cache
|
|
4174
4183
|
failures are logged without blocking the page.
|
|
@@ -4178,6 +4187,29 @@ does not establish restoration before the first paint. Use the classic
|
|
|
4178
4187
|
[ThemePresentation head entry](#themepresentationjs) before CSS for that early
|
|
4179
4188
|
restoration. Neither entry imports User or starts its storage lifecycle.
|
|
4180
4189
|
|
|
4190
|
+
When a native host exposes `Arcane.window.setTheme`, the shared bootstrap
|
|
4191
|
+
samples the app body's inherited `--background` and `--text-color`. It resolves
|
|
4192
|
+
CSS variables, named colors, HSL and other browser-supported color forms through
|
|
4193
|
+
computed style. Modern color spaces use the browser's sRGB canvas conversion;
|
|
4194
|
+
the resolved alpha is retained separately, including translucency close to one.
|
|
4195
|
+
Authored styles remain unchanged. Missing semantic properties are omitted.
|
|
4196
|
+
|
|
4197
|
+
One listener on the shared `arcane-theme-change` owner, root/body presentation
|
|
4198
|
+
attribute observers, stylesheet-load events and device-color-scheme changes
|
|
4199
|
+
schedule samples. Identical channel values do not issue another request.
|
|
4200
|
+
Initial sampling and preference/profile reconciliation run independently of
|
|
4201
|
+
rendering and native completion. Page hide cancels the pending renderer wait;
|
|
4202
|
+
page restoration samples again. Print presentation is excluded. The host keeps
|
|
4203
|
+
its last accepted colors across navigation until the next document changes
|
|
4204
|
+
them. No additional theme bus, polling, storage or system-appearance mutation is
|
|
4205
|
+
introduced by this forwarding path.
|
|
4206
|
+
|
|
4207
|
+
Ordinary browsers and facades without the window method install no native
|
|
4208
|
+
sampler. An older host's unavailable-method response is recorded once in
|
|
4209
|
+
developer diagnostics and stops that sampler; genuine bridge failures retain
|
|
4210
|
+
their complete diagnostics. See the [current-window contract](core-client.md#current-window-theme)
|
|
4211
|
+
for per-field results, cancellation and actual adapter availability.
|
|
4212
|
+
|
|
4181
4213
|
### Availability and normalization
|
|
4182
4214
|
|
|
4183
4215
|
**Browser/native hybrid.** Theme state normalized; storage/native errors mixed. Transport: ThemeManager + Arcane.events. [Deep protocol details](protocols.md).
|
|
@@ -6924,13 +6924,30 @@ Import it from `arcane-os/core/client`. The result includes `runtime`, `events`,
|
|
|
6924
6924
|
`Error` and the existing `ai`, `environment`, `mail`, `speech`, `localAI`,
|
|
6925
6925
|
`ollama`, application, platform, storage and other host namespaces.
|
|
6926
6926
|
|
|
6927
|
-
`facade.speech.synthesize(request, {signal})`
|
|
6928
|
-
|
|
6929
|
-
|
|
6927
|
+
`facade.speech.synthesize(request, {signal})` and
|
|
6928
|
+
`facade.speech.transcribe(request, {signal})` accept an optional `AbortSignal`
|
|
6929
|
+
through the same correlated request lifetime. Each complete request and existing
|
|
6930
|
+
180,000 ms timeout remain unchanged. A pre-aborted signal sends no speech
|
|
6930
6931
|
request; an in-flight abort emits that request's `request.cancel`, rejects with
|
|
6931
6932
|
`ARCANE_REQUEST_ABORTED` and leaves unrelated requests active. Late responses
|
|
6932
|
-
cannot settle the cancelled request. Actual native synthesis
|
|
6933
|
-
to the registered service and engine; this facade supplies
|
|
6933
|
+
cannot settle the cancelled request. Actual native synthesis or transcription
|
|
6934
|
+
interruption belongs to the registered service and engine; this facade supplies
|
|
6935
|
+
no engine. The native routes of `AI.fetchTTS(payload, signal)` and
|
|
6936
|
+
`AI.fetchSTT(audio, signal)` forward their signals through these calls and
|
|
6937
|
+
normalize cancellation to `AbortError` with `ARCANE_AI_REQUEST_ABORTED`.
|
|
6938
|
+
|
|
6939
|
+
`facade.window.setTheme(presentation, {signal}?)` forwards the complete
|
|
6940
|
+
presentation to the current native window, independently of OS-user appearance.
|
|
6941
|
+
Optional `backgroundColor` and `textColor` accept `{red,green,blue,alpha}` or
|
|
6942
|
+
`null` for a platform-default reset; omitted fields remain unchanged. The
|
|
6943
|
+
result is `{platform,supported,applied,unsupported}` with actual per-field
|
|
6944
|
+
acceptance, rather than a measured-pixel or atomic-update claim. Cancellation
|
|
6945
|
+
uses the existing request lifetime and does not undo settings already accepted.
|
|
6946
|
+
Shared ThemeBootstrap forwards the app's computed semantic colors without
|
|
6947
|
+
blocking rendering; applications using it need no duplicate listener. See the
|
|
6948
|
+
[current-window contract](core-client.md#current-window-theme) for channel
|
|
6949
|
+
values, errors, navigation, disposal and current host support. The facade does
|
|
6950
|
+
not add this operation to an older executable or an unsupported host.
|
|
6934
6951
|
|
|
6935
6952
|
### Availability and normalization
|
|
6936
6953
|
|
|
@@ -7398,6 +7415,25 @@ including accepted service work, without a forced termination deadline. Missing
|
|
|
7398
7415
|
prerequisites, download/extraction failures and host failures remain observable
|
|
7399
7416
|
through the operation's error/event boundary.
|
|
7400
7417
|
|
|
7418
|
+
During assembly, `appDescriptor.native.icon` selects the app's existing image.
|
|
7419
|
+
PNG and ICO inputs produce a derived `runtime/arcane-app.ico` and update only
|
|
7420
|
+
the copied application's executable icon. Source assets, the selected host
|
|
7421
|
+
cache, other executable resources and the Core executable remain unchanged.
|
|
7422
|
+
`manifest.host.icon` identifies the selected runtime icon, while
|
|
7423
|
+
`manifest.host.executableIcon` records whether embedding succeeded.
|
|
7424
|
+
|
|
7425
|
+
The builder's `onEvent` receives `native.icon.completed` after successful
|
|
7426
|
+
embedding or `native.icon.unsupported` when the selected format or executable
|
|
7427
|
+
layout cannot provide it. Both carry `target`, `appId`, `icon` and `executable`;
|
|
7428
|
+
the unsupported event also carries the original `code` and complete `message`.
|
|
7429
|
+
Unsupported embedding preserves ordinary assembly and the original icon path.
|
|
7430
|
+
Unreadable files or malformed supported image data retain their ordinary errors.
|
|
7431
|
+
The matching host loads the window/taskbar icon independently of page and Core
|
|
7432
|
+
startup. PNG/ICO embedding and runtime codec support are separate boundaries;
|
|
7433
|
+
see [application icon and window colors](core-native-packaging.md#application-icon-and-window-colors)
|
|
7434
|
+
for formats, per-window colors, errors and platform limits. npm publication
|
|
7435
|
+
alone does not establish delivery or execution of the matching native archive.
|
|
7436
|
+
|
|
7401
7437
|
### Example
|
|
7402
7438
|
|
|
7403
7439
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.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",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
|
|
42
42
|
"./ai/core-local": "./browser-runtime/ai/core-local.mjs",
|
|
43
43
|
"./ai/core-onnx": "./browser-runtime/ai/core-onnx.mjs",
|
|
44
|
+
"./ai/core-image": "./browser-runtime/ai/core-image.mjs",
|
|
45
|
+
"./ai/core-model-assets": "./browser-runtime/ai/core-model-assets.mjs",
|
|
44
46
|
"./ai/browser-decisions": "./browser-runtime/ai/browser-decisions.mjs",
|
|
45
47
|
"./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
|
|
46
48
|
"./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
|
|
@@ -63,10 +65,13 @@
|
|
|
63
65
|
"./core/preferences": "./src/core/services/preferences.mjs",
|
|
64
66
|
"./core/diarization": "./src/diarization/service.mjs",
|
|
65
67
|
"./core/local-ai": "./src/core/services/local-ai.mjs",
|
|
68
|
+
"./core/image": "./src/core/services/image.mjs",
|
|
69
|
+
"./core/model-assets": "./src/core/services/model-assets.mjs",
|
|
66
70
|
"./diarization": "./src/diarization/index.mjs",
|
|
67
71
|
"./diarization/build": "./src/diarization/build.mjs",
|
|
68
72
|
"./local-ai": "./src/local-ai/install.mjs",
|
|
69
73
|
"./local-ai/onnx": "./src/local-ai/onnx.mjs",
|
|
74
|
+
"./local-ai/image": "./src/local-ai/image-runtime.mjs",
|
|
70
75
|
"./integrated-provider": "./src/integrated-provider-loader.mjs",
|
|
71
76
|
"./packager": "./src/packager/core.mjs",
|
|
72
77
|
"./release-bundle": "./src/release-bundle.mjs",
|
|
@@ -107,8 +112,8 @@
|
|
|
107
112
|
"scripts": {
|
|
108
113
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
109
114
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
110
|
-
"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",
|
|
111
|
-
"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-decisions.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/core-classic-source.test.mjs test/core-client.test.mjs test/core-host.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.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-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.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/rendered-print.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/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
115
|
+
"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/native-windows-icon.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
116
|
+
"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-decisions.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/core-classic-source.test.mjs test/core-client.test.mjs test/core-host.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.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/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.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/rendered-print.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/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
112
117
|
"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-macos-provider.test.mjs test/native-plan.test.mjs test/native-portable-provider.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",
|
|
113
118
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.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",
|
|
114
119
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -3,11 +3,12 @@ const is=new Is(false);
|
|
|
3
3
|
|
|
4
4
|
import { arcaneLogging } from 'arcane-os/logging';
|
|
5
5
|
import {applyUserSkin,loadAndApplyTheme} from './ThemeManager.js';
|
|
6
|
-
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
6
|
+
import {arcaneEvents,createArcaneEventSource} from 'arcane-os/event-manager';
|
|
7
7
|
|
|
8
8
|
const sharedKey='arcaneThemeReady';
|
|
9
9
|
const listenerKey='arcaneThemeAppearanceListener';
|
|
10
10
|
const userListenerKey='arcaneThemeUserListener';
|
|
11
|
+
const windowListenerKey='arcaneThemeWindowListener';
|
|
11
12
|
const THEME_BOOTSTRAP_EVENT_OWNER={};
|
|
12
13
|
let themeBootstrapOperationSequence=0;
|
|
13
14
|
const themeBootstrapEvents=createArcaneEventSource(
|
|
@@ -99,11 +100,222 @@ function installUserSkinListener(){
|
|
|
99
100
|
applyReadyUserSkin();
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
function installWindowThemeListener(ready){
|
|
104
|
+
const host=globalThis.Arcane;
|
|
105
|
+
const document=globalThis.document;
|
|
106
|
+
if(globalThis[windowListenerKey]||!host?.runtime?.current?.().native
|
|
107
|
+
||!is.function(host?.window?.setTheme)||!document?.documentElement) return;
|
|
108
|
+
|
|
109
|
+
let active=true;
|
|
110
|
+
let suspended=false;
|
|
111
|
+
let scheduled=false;
|
|
112
|
+
let lastPresentation=null;
|
|
113
|
+
let pending=null;
|
|
114
|
+
let observedBody=null;
|
|
115
|
+
let canvasContext=null;
|
|
116
|
+
const root=document.documentElement;
|
|
117
|
+
const scheme=globalThis.matchMedia('(prefers-color-scheme: dark)');
|
|
118
|
+
const print=globalThis.matchMedia('print');
|
|
119
|
+
const probe=document.createElement('span');
|
|
120
|
+
probe.style.setProperty('display','none','important');
|
|
121
|
+
|
|
122
|
+
function report(error){
|
|
123
|
+
arcaneLogging.warn('[Arcane theme] Unable to update the native window colors.',error);
|
|
124
|
+
}
|
|
125
|
+
function colorRecord(resolved){
|
|
126
|
+
// Read the browser's resolved color, not an authored CSS grammar. Keep
|
|
127
|
+
// its alpha separately so raster conversion cannot round translucency away.
|
|
128
|
+
const rgb=resolved.match(/^rgba?\(([^)]+)\)$/u);
|
|
129
|
+
if(rgb){
|
|
130
|
+
const values=rgb[1].split(/[\s,/]+/u).filter(Boolean).map(Number);
|
|
131
|
+
if((values.length===3||values.length===4)&&values.every(Number.isFinite)){
|
|
132
|
+
return {red:values[0],green:values[1],blue:values[2],alpha:values[3]??1};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const alphaMatch=resolved.match(/\/\s*(none|[-+\d.eE]+%?)\s*\)$/u);
|
|
136
|
+
const alphaValue=alphaMatch?.[1];
|
|
137
|
+
const alpha=alphaValue===undefined?1:alphaValue==='none'?0:
|
|
138
|
+
Number.parseFloat(alphaValue)/(alphaValue.endsWith('%')?100:1);
|
|
139
|
+
if(!Number.isFinite(alpha)||(resolved.includes('/')&&!alphaMatch)){
|
|
140
|
+
throw new TypeError(`Unable to read the resolved CSS color: ${resolved}`);
|
|
141
|
+
}
|
|
142
|
+
const opaque=alphaMatch?resolved.replace(/\/[^/]*\)$/u,'/ 1)'):resolved;
|
|
143
|
+
if(!canvasContext){
|
|
144
|
+
const canvas=document.createElement('canvas');
|
|
145
|
+
canvas.width=1;
|
|
146
|
+
canvas.height=1;
|
|
147
|
+
canvasContext=canvas.getContext(
|
|
148
|
+
'2d',
|
|
149
|
+
{colorSpace:'srgb'}
|
|
150
|
+
);
|
|
151
|
+
if(!canvasContext) throw new Error('The browser cannot convert the resolved color to sRGB.');
|
|
152
|
+
}
|
|
153
|
+
// An unsupported fillStyle assignment leaves the old value intact.
|
|
154
|
+
// Two different starting values distinguish rejection from a valid black.
|
|
155
|
+
canvasContext.fillStyle='#000000';
|
|
156
|
+
canvasContext.fillStyle=opaque;
|
|
157
|
+
const accepted=canvasContext.fillStyle;
|
|
158
|
+
canvasContext.fillStyle='#ffffff';
|
|
159
|
+
canvasContext.fillStyle=opaque;
|
|
160
|
+
if(canvasContext.fillStyle!==accepted){
|
|
161
|
+
throw new TypeError(`The browser canvas cannot convert the resolved CSS color: ${resolved}`);
|
|
162
|
+
}
|
|
163
|
+
canvasContext.clearRect(0,0,1,1);
|
|
164
|
+
canvasContext.fillRect(0,0,1,1);
|
|
165
|
+
const [red,green,blue]=canvasContext.getImageData(0,0,1,1).data;
|
|
166
|
+
return {red,green,blue,alpha};
|
|
167
|
+
}
|
|
168
|
+
function readPresentation(){
|
|
169
|
+
const target=document.body;
|
|
170
|
+
if(!target) return {};
|
|
171
|
+
const styles=globalThis.getComputedStyle(target);
|
|
172
|
+
const presentation={};
|
|
173
|
+
target.appendChild(probe);
|
|
174
|
+
try {
|
|
175
|
+
const properties=[
|
|
176
|
+
['backgroundColor','--background'],
|
|
177
|
+
['textColor','--text-color']
|
|
178
|
+
];
|
|
179
|
+
for(const [field,property] of properties){
|
|
180
|
+
const authored=styles.getPropertyValue(property);
|
|
181
|
+
if(!authored.trim()) continue;
|
|
182
|
+
if(!globalThis.CSS.supports('color',authored)){
|
|
183
|
+
report(
|
|
184
|
+
new TypeError(`The app theme ${property} is not a supported CSS color: ${authored}`)
|
|
185
|
+
);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Use the target's resolved value: registered properties may
|
|
189
|
+
// deliberately not inherit into this child resolution element.
|
|
190
|
+
probe.style.setProperty('color',authored,'important');
|
|
191
|
+
try {
|
|
192
|
+
const resolved=globalThis.getComputedStyle(probe).color;
|
|
193
|
+
presentation[field]=colorRecord(resolved);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
report(error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} finally {
|
|
199
|
+
probe.remove();
|
|
200
|
+
}
|
|
201
|
+
return presentation;
|
|
202
|
+
}
|
|
203
|
+
function samePresentation(next){
|
|
204
|
+
return lastPresentation&&['backgroundColor','textColor'].every(
|
|
205
|
+
function sameColor(field){
|
|
206
|
+
const before=lastPresentation[field];
|
|
207
|
+
const after=next[field];
|
|
208
|
+
if(before===after) return true;
|
|
209
|
+
if(!before||!after) return false;
|
|
210
|
+
return ['red','green','blue','alpha'].every(
|
|
211
|
+
function sameChannel(channel){
|
|
212
|
+
return before[channel]===after[channel];
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function observeElements(){
|
|
219
|
+
if(observedBody===document.body) return;
|
|
220
|
+
observer.disconnect();
|
|
221
|
+
observer.observe(
|
|
222
|
+
root,
|
|
223
|
+
{attributes:true,childList:true,attributeFilter:['class','style','data-color-scheme','data-user-skin','data-arcane-skin']}
|
|
224
|
+
);
|
|
225
|
+
observedBody=document.body;
|
|
226
|
+
if(observedBody) observer.observe(
|
|
227
|
+
observedBody,
|
|
228
|
+
{attributes:true,attributeFilter:['class','style']}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
function sample(){
|
|
232
|
+
scheduled=false;
|
|
233
|
+
if(!active||suspended||print.matches) return;
|
|
234
|
+
observeElements();
|
|
235
|
+
const presentation=readPresentation();
|
|
236
|
+
if(!Object.keys(presentation).length||samePresentation(presentation)) return;
|
|
237
|
+
lastPresentation=presentation;
|
|
238
|
+
pending?.abort();
|
|
239
|
+
const controller=new AbortController();
|
|
240
|
+
pending=controller;
|
|
241
|
+
try {
|
|
242
|
+
const operation=host.window.setTheme(
|
|
243
|
+
presentation,
|
|
244
|
+
{signal:controller.signal}
|
|
245
|
+
);
|
|
246
|
+
Promise.resolve(operation).catch(
|
|
247
|
+
function reportWindowFailure(error){
|
|
248
|
+
if(error?.code==='ARCANE_REQUEST_ABORTED'||(controller.signal.aborted&&error?.name==='AbortError')) return;
|
|
249
|
+
report(error);
|
|
250
|
+
if(error?.code==='METHOD_NOT_ALLOWED') dispose();
|
|
251
|
+
}
|
|
252
|
+
).finally(
|
|
253
|
+
function finishWindowUpdate(){
|
|
254
|
+
if(pending===controller) pending=null;
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if(pending===controller) pending=null;
|
|
259
|
+
report(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function schedule(){
|
|
263
|
+
if(!active||suspended||scheduled) return;
|
|
264
|
+
scheduled=true;
|
|
265
|
+
Promise.resolve().then(sample).catch(report);
|
|
266
|
+
}
|
|
267
|
+
function stylesheetLoaded(event){
|
|
268
|
+
if(event.target?.tagName==='LINK'&&event.target.relList?.contains('stylesheet')) schedule();
|
|
269
|
+
}
|
|
270
|
+
function hide(){
|
|
271
|
+
suspended=true;
|
|
272
|
+
pending?.abort();
|
|
273
|
+
}
|
|
274
|
+
function show(){
|
|
275
|
+
suspended=false;
|
|
276
|
+
lastPresentation=null;
|
|
277
|
+
schedule();
|
|
278
|
+
}
|
|
279
|
+
function dispose(){
|
|
280
|
+
if(!active) return false;
|
|
281
|
+
active=false;
|
|
282
|
+
pending?.abort();
|
|
283
|
+
observer.disconnect();
|
|
284
|
+
unsubscribe();
|
|
285
|
+
scheme.removeEventListener('change',schedule);
|
|
286
|
+
print.removeEventListener('change',schedule);
|
|
287
|
+
document.removeEventListener('DOMContentLoaded',schedule);
|
|
288
|
+
document.removeEventListener('load',stylesheetLoaded,true);
|
|
289
|
+
globalThis.removeEventListener('pagehide',hide);
|
|
290
|
+
globalThis.removeEventListener('pageshow',show);
|
|
291
|
+
canvasContext=null;
|
|
292
|
+
if(globalThis[windowListenerKey]===dispose) delete globalThis[windowListenerKey];
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
const observer=new globalThis.MutationObserver(schedule);
|
|
296
|
+
const unsubscribe=arcaneEvents.subscribe('arcane-theme-change',schedule);
|
|
297
|
+
observer.observe(
|
|
298
|
+
root,
|
|
299
|
+
{attributes:true,childList:true,attributeFilter:['class','style','data-color-scheme','data-user-skin','data-arcane-skin']}
|
|
300
|
+
);
|
|
301
|
+
observeElements();
|
|
302
|
+
scheme.addEventListener('change',schedule);
|
|
303
|
+
print.addEventListener('change',schedule);
|
|
304
|
+
document.addEventListener('DOMContentLoaded',schedule);
|
|
305
|
+
document.addEventListener('load',stylesheetLoaded,true);
|
|
306
|
+
globalThis.addEventListener('pagehide',hide);
|
|
307
|
+
globalThis.addEventListener('pageshow',show);
|
|
308
|
+
globalThis[windowListenerKey]=dispose;
|
|
309
|
+
schedule();
|
|
310
|
+
Promise.resolve(ready).then(schedule).catch(report);
|
|
311
|
+
}
|
|
312
|
+
|
|
102
313
|
export function bootstrapArcaneTheme(options={}){
|
|
103
314
|
const useSharedPromise=Object.keys(options).length===0;
|
|
104
315
|
if(useSharedPromise&&globalThis[sharedKey]){
|
|
105
316
|
installAppearanceListener(globalThis[sharedKey]);
|
|
106
317
|
installUserSkinListener();
|
|
318
|
+
installWindowThemeListener(globalThis[sharedKey]);
|
|
107
319
|
return globalThis[sharedKey];
|
|
108
320
|
}
|
|
109
321
|
|
|
@@ -116,6 +328,7 @@ export function bootstrapArcaneTheme(options={}){
|
|
|
116
328
|
if(useSharedPromise){
|
|
117
329
|
installAppearanceListener(ready);
|
|
118
330
|
installUserSkinListener();
|
|
331
|
+
installWindowThemeListener(ready);
|
|
119
332
|
}
|
|
120
333
|
return ready;
|
|
121
334
|
}
|
|
@@ -123,9 +336,11 @@ export function bootstrapArcaneTheme(options={}){
|
|
|
123
336
|
export function disposeArcaneThemeBootstrap(){
|
|
124
337
|
const dispose=globalThis[listenerKey];
|
|
125
338
|
const disposeUser=globalThis[userListenerKey];
|
|
339
|
+
const disposeWindow=globalThis[windowListenerKey];
|
|
126
340
|
const appearanceDisposed=is.function(dispose)?dispose():false;
|
|
127
341
|
const userDisposed=is.function(disposeUser)?disposeUser():false;
|
|
128
|
-
|
|
342
|
+
const windowDisposed=is.function(disposeWindow)?disposeWindow():false;
|
|
343
|
+
return appearanceDisposed||userDisposed||windowDisposed;
|
|
129
344
|
}
|
|
130
345
|
|
|
131
346
|
export const arcaneThemeReady=bootstrapArcaneTheme();
|
|
@@ -262,12 +262,12 @@
|
|
|
262
262
|
"type": "array",
|
|
263
263
|
"items": {
|
|
264
264
|
"oneOf": [
|
|
265
|
-
{ "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
265
|
+
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
266
266
|
{
|
|
267
267
|
"type": "object",
|
|
268
268
|
"required": ["id"],
|
|
269
269
|
"properties": {
|
|
270
|
-
"id": { "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
270
|
+
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
271
271
|
"version": { "type": "string" },
|
|
272
272
|
"url": { "type": "string" }
|
|
273
273
|
}
|
|
@@ -156,12 +156,12 @@
|
|
|
156
156
|
"type": "array",
|
|
157
157
|
"items": {
|
|
158
158
|
"oneOf": [
|
|
159
|
-
{ "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
159
|
+
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
160
160
|
{
|
|
161
161
|
"type": "object",
|
|
162
162
|
"required": ["id"],
|
|
163
163
|
"properties": {
|
|
164
|
-
"id": { "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
164
|
+
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
165
165
|
"version": { "type": "string" },
|
|
166
166
|
"url": { "type": "string" }
|
|
167
167
|
}
|