arcane-os 0.59.1 → 0.60.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 +20 -0
- package/README.md +1 -1
- 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/model-assets.md +144 -0
- package/docs/reference/runtime-modules.md +37 -5
- package/docs/reference/sdk-api.md +9 -5
- package/package.json +3 -3
- package/runtime/arcane/modules/ThemeBootstrap.js +217 -2
- package/src/core/hosts/windows/ArcaneHost.cs +211 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
- package/src/core/services/model-assets.mjs +267 -0
- package/src/local-ai/image-editing.mjs +159 -0
- package/src/native/windows-icon.mjs +582 -0
- package/src/native/windows-provider.mjs +60 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Local image edit inputs
|
|
2
|
+
|
|
3
|
+
The SDK input decoder prepares a PNG raster for the shared native image
|
|
4
|
+
runtime. Decoding does not load a model or perform inference. The runtime owns
|
|
5
|
+
model selection, supported editing operations, native calls and their complete
|
|
6
|
+
results. Applications retain the original image and decide whether to save a
|
|
7
|
+
newly generated result.
|
|
8
|
+
|
|
9
|
+
## Decoder contract
|
|
10
|
+
|
|
11
|
+
`src/local-ai/image-editing.mjs` exports the internal worker function
|
|
12
|
+
`decodeImage({data, mediaType, signal})`.
|
|
13
|
+
|
|
14
|
+
| Field | Contract |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `data` | Complete encoded PNG in a `Uint8Array`; a Node `Buffer` is also accepted. Only the supplied view is read. |
|
|
17
|
+
| `mediaType` | `image/png`, an empty string, or omitted. The PNG signature is read from the actual input. Other declared formats report unsupported input. |
|
|
18
|
+
| `signal` | Optional `AbortSignal` belonging to the image request. |
|
|
19
|
+
|
|
20
|
+
The promise returns `{width, height, channel, data}`. Dimensions come from the
|
|
21
|
+
source raster; `channel` is `3` for RGB or `4` for RGBA. Returned `data` is a
|
|
22
|
+
separate contiguous `Uint8Array` containing interleaved samples, with pixels
|
|
23
|
+
ordered left to right and rows ordered top to bottom. The native request owner
|
|
24
|
+
retains this allocation until the native operation actually completes,
|
|
25
|
+
including after a cancellation request.
|
|
26
|
+
|
|
27
|
+
Input decoding accepts static, non-interlaced, 8-bit RGB and RGBA PNGs, all
|
|
28
|
+
five PNG row filters, and image data spread across multiple `IDAT` chunks.
|
|
29
|
+
RGB transparent-color information is represented as RGBA. An optional suggested
|
|
30
|
+
palette on an RGB or RGBA image does not change its color type.
|
|
31
|
+
|
|
32
|
+
Indexed-color, grayscale, 16-bit, interlaced and animated PNGs currently report
|
|
33
|
+
unsupported input. JPEG and WebP are also unsupported by this decoder. The
|
|
34
|
+
decoder does not silently convert those formats or select one animation frame.
|
|
35
|
+
|
|
36
|
+
The original encoded image is copied for the operation and never modified or
|
|
37
|
+
detached. Decoding preserves stored color samples and straight alpha, including
|
|
38
|
+
RGB values underneath transparent pixels. It does not composite onto a chosen
|
|
39
|
+
background, rotate pixels from metadata, apply a color-profile conversion,
|
|
40
|
+
resize, crop or create a file. Metadata remains in the original image; the
|
|
41
|
+
native raster record contains pixels and dimensions only.
|
|
42
|
+
|
|
43
|
+
## Execution and cancellation
|
|
44
|
+
|
|
45
|
+
Invoke the decoder inside the shared image runtime's existing worker.
|
|
46
|
+
Inflation uses asynchronous Node zlib. Row reconstruction yields between
|
|
47
|
+
scanlines so the worker can receive cancellation. An abort during inflation is
|
|
48
|
+
observed after that native operation settles; it does not claim instant
|
|
49
|
+
interruption. A cancelled decode returns no raster for inference.
|
|
50
|
+
|
|
51
|
+
`ARCANE_IMAGE_INPUT_UNSUPPORTED` describes an unavailable input format or
|
|
52
|
+
encoding. `ARCANE_IMAGE_DECODE_FAILED` describes unreadable PNG structure or
|
|
53
|
+
scanlines. Node zlib failures retain their complete original errors, and
|
|
54
|
+
cancellation retains the owning signal's reason or Node `AbortError`. A decode
|
|
55
|
+
failure never returns partial imagery or substitutes a different image.
|
|
56
|
+
|
|
57
|
+
## Editing semantics at the native owner
|
|
58
|
+
|
|
59
|
+
Whole-image img2img uses an initial image, prompt and strength. Masked editing
|
|
60
|
+
adds a separate mask; instruction/reference editing uses a model that supports
|
|
61
|
+
reference conditioning. These are distinct operations and must be advertised
|
|
62
|
+
only when the selected model and native path implement them. Ordinary img2img
|
|
63
|
+
does not require LoRA training. The candidate engine documents this directly
|
|
64
|
+
with its base SD1.4 model in the
|
|
65
|
+
[tagged img2img example](https://github.com/leejet/stable-diffusion.cpp/blob/master-929-3f8527a/docs/sd.md#img2img-example).
|
|
66
|
+
|
|
67
|
+
Source dimensions are decoder output, not an implicit request to change the
|
|
68
|
+
generation dimensions. The model owner documents its actual canvas adaptation
|
|
69
|
+
and reports actual output dimensions. In the candidate engine, temporary
|
|
70
|
+
initial images use model preprocessing while caller images remain unchanged;
|
|
71
|
+
even `mode=none` does not suppress all later model adaptation. See the
|
|
72
|
+
[native preprocessing contract](https://github.com/leejet/stable-diffusion.cpp/blob/master-929-3f8527a/docs/image_preprocessing.md).
|
|
73
|
+
|
|
74
|
+
An RGB-only model such as the selected SD1.4 candidate consumes the stored RGB
|
|
75
|
+
and does not preserve alpha. The SDK must not silently choose a compositing
|
|
76
|
+
background or claim that passing RGBA preserves its displayed appearance.
|
|
77
|
+
Strength zero is also not an exact-copy operation: native img2img still uses
|
|
78
|
+
the model's encode, sampling and decode path. These model behaviors are
|
|
79
|
+
separate from the decoder preserving the original input.
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
const result = await image.generate({
|
|
46
|
+
model: 'sd14',
|
|
47
|
+
prompt: 'A dignified raccoon who has been put in charge of the moon.',
|
|
48
|
+
signal
|
|
49
|
+
});
|
|
50
|
+
// Display or save the complete returned image through the application owner.
|
|
51
|
+
await image.unload();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This example assumes that the application selected the `sd14` model and its
|
|
55
|
+
complete checkpoint source. Model choice, download approval, prompts, output
|
|
56
|
+
storage and the selected working directory remain application-owned.
|
|
57
|
+
|
|
58
|
+
`prepareCoreModelAssets({client,workingDirectory,members,signal,onProgress})`
|
|
59
|
+
requires an available Core connection exposing the model-assets service.
|
|
60
|
+
`client` defaults to the installed Core client. Every member is
|
|
61
|
+
`{path,file}`, where `path` is its relative filename or companion-file path and
|
|
62
|
+
`file` is the complete stored Blob or File. Relative working-directory paths
|
|
63
|
+
resolve from the native service's `appRoot`; an application may explicitly
|
|
64
|
+
select an absolute working directory.
|
|
65
|
+
|
|
66
|
+
The result contains `id`, `directory`, ordered
|
|
67
|
+
`members: [{path,nativePath}]`, and an idempotent asynchronous `release()`.
|
|
68
|
+
The operation creates a unique child directory under the selected working
|
|
69
|
+
directory. Member paths identify files inside that child directory. Original
|
|
70
|
+
DBOPFS files remain untouched.
|
|
71
|
+
|
|
72
|
+
Each incoming Blob stream chunk is encoded only for the existing JSON Core
|
|
73
|
+
transport. The helper waits for the corresponding native write before reading
|
|
74
|
+
the next chunk of that member. Independent members transfer concurrently;
|
|
75
|
+
the complete member set is returned only after every write and file close
|
|
76
|
+
finishes. There is no complete-model transport buffer or application content
|
|
77
|
+
limit. Progress reports actual preparation phases and member paths.
|
|
78
|
+
|
|
79
|
+
Cancellation stops active readers and requests. Failure joins the participating
|
|
80
|
+
transfers and requests native cleanup; that cleanup joins actual file writes
|
|
81
|
+
before removing the projection. Cleanup uses its own uncancelled operation so
|
|
82
|
+
an already-aborted preparation signal cannot skip it. Original failures and
|
|
83
|
+
cleanup failures remain observable.
|
|
84
|
+
|
|
85
|
+
## Native ownership
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import {createModelAssetService} from 'arcane-os/core/model-assets';
|
|
89
|
+
|
|
90
|
+
const modelAssets = createModelAssetService({appRoot});
|
|
91
|
+
// Register this same service with Core and pass it to the native engine owner.
|
|
92
|
+
const use = modelAssets.retain(projectionId);
|
|
93
|
+
try {
|
|
94
|
+
// Load and use the complete files from use.members.
|
|
95
|
+
await ownNativeModelLifetime(use.members);
|
|
96
|
+
} finally {
|
|
97
|
+
// ownNativeModelLifetime must join actual model release before returning.
|
|
98
|
+
await use.release();
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`createModelAssetService({appRoot})` returns a Core service with `current()`,
|
|
103
|
+
`retain(id)` and `dispose()`. `retain()` accepts only a completed projection
|
|
104
|
+
and returns `{id,directory,members,release}`. A native engine takes this handle
|
|
105
|
+
before loading and releases it after actual unload, failed-load cleanup or
|
|
106
|
+
worker exit. A rejected inference/load promise alone does not establish native
|
|
107
|
+
release. Multiple contexts can retain one prepared set, and repeated inference
|
|
108
|
+
on a retained context reuses its files.
|
|
109
|
+
|
|
110
|
+
The browser's `projection.release()` relinquishes preparation ownership. It
|
|
111
|
+
does not delete files retained by a native engine. Deletion follows the final
|
|
112
|
+
native release and removes only that operation's working directory. Core closes
|
|
113
|
+
independent services concurrently, so the model-assets service waits for all
|
|
114
|
+
native retain handles during disposal instead of assuming service order.
|
|
115
|
+
Owners must release their handles after joining their actual engine lifetime.
|
|
116
|
+
|
|
117
|
+
Closing a browser accessor does not by itself establish native unload.
|
|
118
|
+
Explicitly release unused prepared projections. Core disposal also releases
|
|
119
|
+
preparation ownership and cleans up after retained engine uses end.
|
|
120
|
+
|
|
121
|
+
`current()` returns `{closing,projections}`. Each projection reports its `id`,
|
|
122
|
+
`directory`, `state`, ordered `members`, `preparationOwned`, number of native
|
|
123
|
+
`uses`, and `error`. Core emits `modelAssets.state` for lifecycle changes.
|
|
124
|
+
|
|
125
|
+
## Core transport operations
|
|
126
|
+
|
|
127
|
+
| Method | Parameters | Result |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| `modelAssets.open` | `{id,workingDirectory,members:[{path}]}` | Preparing projection and native member paths |
|
|
130
|
+
| `modelAssets.write` | `{id,memberIndex,contentBase64}` | `{id,memberIndex,written:true}` after the native write |
|
|
131
|
+
| `modelAssets.complete` | `{id}` | Ready projection after all files close |
|
|
132
|
+
| `modelAssets.release` | `{id}` | Released preparation ownership; native uses may retain the files |
|
|
133
|
+
| `modelAssets.status` | `{}` | Current projection lifecycle records |
|
|
134
|
+
|
|
135
|
+
The browser helper owns this transport sequence. Correlation IDs identify
|
|
136
|
+
operations; they are unrelated to content identity. Original model files remain
|
|
137
|
+
separate and complete. A retry creates a new working projection from the stored
|
|
138
|
+
assets; it does not redownload them or replace the authoritative DBOPFS record.
|
|
139
|
+
|
|
140
|
+
The implementation uses browser streams, the existing portable Core JSON
|
|
141
|
+
transport and Node filesystem operations. Native paths and cleanup follow the
|
|
142
|
+
host's Windows, Linux or macOS filesystem behavior. Android requires its host's
|
|
143
|
+
Core/filesystem adaptation. These are source-level platform contracts, not a
|
|
144
|
+
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,17 @@ 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`.
|
|
6934
6938
|
|
|
6935
6939
|
### Availability and normalization
|
|
6936
6940
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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",
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"scripts": {
|
|
108
108
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
109
109
|
"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",
|
|
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/native-windows-icon.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/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
112
|
"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
113
|
"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
114
|
"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();
|