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.
Files changed (41) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/core-image.mjs +360 -0
  4. package/browser-runtime/ai/core-model-assets.mjs +152 -0
  5. package/browser-runtime/core/client.mjs +8 -0
  6. package/browser-runtime/core/contracts.mjs +5 -0
  7. package/docs/reference/core/arcane-ai-contracts.md +17 -1
  8. package/docs/reference/core/arcane-api.md +12 -1
  9. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +20 -11
  10. package/docs/reference/core/reference/arcane-api/namespaces.md +14 -0
  11. package/docs/reference/core-client.md +67 -0
  12. package/docs/reference/core-native-packaging.md +66 -2
  13. package/docs/reference/inventory/package-api.json +2 -2
  14. package/docs/reference/inventory/runtime-modules.json +2 -2
  15. package/docs/reference/local-image-editing.md +79 -0
  16. package/docs/reference/local-image-generation.md +267 -0
  17. package/docs/reference/model-assets.md +147 -0
  18. package/docs/reference/runtime-modules.md +37 -5
  19. package/docs/reference/sdk-api.md +41 -5
  20. package/package.json +8 -3
  21. package/runtime/arcane/modules/ThemeBootstrap.js +217 -2
  22. package/schemas/arcane-app.schema.json +2 -2
  23. package/schemas/arcane-package.schema.json +2 -2
  24. package/src/core/hosts/windows/ArcaneHost.cs +211 -0
  25. package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
  26. package/src/core/services/image.mjs +563 -0
  27. package/src/core/services/local-ai.mjs +8 -3
  28. package/src/core/services/model-assets.mjs +267 -0
  29. package/src/import-map.mjs +2 -0
  30. package/src/local-ai/config.mjs +41 -1
  31. package/src/local-ai/dev.mjs +46 -9
  32. package/src/local-ai/image-editing.mjs +159 -0
  33. package/src/local-ai/image-library.mjs +985 -0
  34. package/src/local-ai/image-models/sd14.mjs +15 -0
  35. package/src/local-ai/image-png.mjs +74 -0
  36. package/src/local-ai/image-runtime.mjs +336 -0
  37. package/src/local-ai/image-worker.mjs +218 -0
  38. package/src/local-ai/install.mjs +326 -10
  39. package/src/native/portable-layout.mjs +28 -2
  40. package/src/native/windows-icon.mjs +582 -0
  41. 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
- it does not independently choose a provider, cloud fallback, model, runtime, or
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
- provider routes accept a `Blob` or `File` directly and leave media decoding,
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 both bootstrap-owned subscriptions.
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})` accepts an optional `AbortSignal`
6928
- through the same correlated request lifetime. The complete request and existing
6929
- 180,000 ms timeout remain unchanged. A pre-aborted signal sends no synthesis
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 interruption belongs
6933
- to the registered service and engine; this facade supplies no engine.
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.59.1",
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
- return appearanceDisposed||userDisposed;
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
  }