arcane-os 0.59.0 → 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 CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.60.0
4
+
5
+ - Add `Arcane.window.setTheme(presentation, {signal})` for the current native
6
+ window. Optional sRGB RGBA background/text colors, null resets and omitted
7
+ fields retain their explicit semantics; results distinguish accepted and
8
+ unsupported fields. This does not change system-wide appearance.
9
+ - Forward application-computed theme colors without blocking rendering.
10
+ Observe existing theme and page lifecycles, cancel superseded requests, and
11
+ preserve ordinary browser behavior and native window controls.
12
+ - Apply app-owned Windows icons to the window/taskbar and copied executable.
13
+ Portable Node assembly handles PNG/ICO resources without an application
14
+ compiler, preserving source images and other executable resources. Other
15
+ formats retain assembly with an explicit unsupported-branding diagnostic.
16
+ - Windows caption/text colors use the host's supported per-window DWM
17
+ attributes. Using the new Windows behavior requires the matching host asset;
18
+ platform acceptance is reported rather than inferred from a request.
19
+ - Include completed internal PNG-decoding and stored-model working-file
20
+ preparation primitives. Image-service registration and engine integration
21
+ remain separate work; this release does not claim app-ready image generation.
22
+
23
+ ## 0.59.1
24
+
25
+ - Forward the existing AI synthesis signal into native Core synthesis and
26
+ preserve the established AI cancellation error contract.
27
+ - Carry `AI.fetchSTT(audio, signal)` cancellation through
28
+ `Arcane.speech.transcribe(request, {signal})` into the existing Core request
29
+ lifetime. Preserve complete requests and results, one-argument facade calls,
30
+ unrelated errors and the 180-second timeout. Actual inference interruption
31
+ and resource release remain with the selected speech service.
32
+ - No speech engine, model, runtime dependency, default-provider switch or data
33
+ migration is introduced.
34
+
3
35
  ## 0.59.0
4
36
 
5
37
  - Reconcile marked generated PWA references when a root application's PWA is
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.59.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.60.0` SDK contract. Development applications declare
24
24
  `arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
25
25
  designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
26
26
  to refresh and compare the installed SDK and lockfile with the published channel;
@@ -0,0 +1,152 @@
1
+ import Is from '../dependencies/strong-type/index.js';
2
+ import {getInstalledCoreClient} from '../core/client.mjs';
3
+
4
+ const is = new Is(false);
5
+
6
+ /** Projects complete caller-owned assets through Core without changing their store. */
7
+ export async function prepareCoreModelAssets({
8
+ client = getInstalledCoreClient(), workingDirectory, members, signal, onProgress
9
+ } = {}) {
10
+ if (!is.function(client?.invoke) || !is.function(client?.uuid)) {
11
+ const error = new Error('Native model assets require an available Core connection.');
12
+ error.code = 'ARCANE_MODEL_ASSETS_CORE_UNAVAILABLE';
13
+ throw error;
14
+ }
15
+ if (!is.string(workingDirectory) || workingDirectory === '') {
16
+ throw new TypeError('Model asset workingDirectory must be a nonempty string.');
17
+ }
18
+ if (!is.array(members) || members.length === 0) {
19
+ throw new TypeError('Model assets require at least one complete file.');
20
+ }
21
+ for (const member of members) {
22
+ if (!is.string(member?.path) || member.path === '' || !is.function(member?.file?.stream)) {
23
+ throw new TypeError('Each model asset requires a path and a complete Blob or File.');
24
+ }
25
+ }
26
+ if (onProgress !== undefined && !is.function(onProgress)) {
27
+ throw new TypeError('Model asset onProgress must be a function.');
28
+ }
29
+
30
+ const controller = new AbortController();
31
+ const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
32
+ operationSignal.throwIfAborted();
33
+ const id = client.uuid();
34
+ const cleanupErrors = [];
35
+ let completed = 0;
36
+ let openingStarted = false;
37
+ let failed = false;
38
+ let failure;
39
+ let releaseTask;
40
+
41
+ function retainFailure(error) {
42
+ if (failed) return;
43
+ failed = true;
44
+ failure = error;
45
+ controller.abort(error);
46
+ }
47
+
48
+ function progress(phase, memberIndex) {
49
+ operationSignal.throwIfAborted();
50
+ onProgress?.({
51
+ phase, completed, total: members.length, unit: 'files',
52
+ ...(memberIndex === undefined ? {} : {memberIndex, path: members[memberIndex].path})
53
+ });
54
+ }
55
+
56
+ function release() {
57
+ releaseTask ??= Promise.resolve().then(function releaseNativeProjection() {
58
+ return client.invoke('modelAssets.release', {id}, {timeoutMs: 0});
59
+ });
60
+ return releaseTask;
61
+ }
62
+
63
+ async function transferMember(member, memberIndex) {
64
+ let reader;
65
+ let cancellation;
66
+
67
+ function cancelReader() {
68
+ if (!reader || cancellation) return;
69
+ cancellation = Promise.resolve().then(function cancelMemberStream() {
70
+ return reader.cancel(operationSignal.reason);
71
+ }).catch(function retainReaderCleanupFailure(error) {
72
+ cleanupErrors.push(error);
73
+ });
74
+ }
75
+
76
+ try {
77
+ operationSignal.throwIfAborted();
78
+ reader = member.file.stream().getReader();
79
+ operationSignal.addEventListener('abort', cancelReader, {once: true});
80
+ if (operationSignal.aborted) cancelReader();
81
+ progress('write', memberIndex);
82
+ while (true) {
83
+ operationSignal.throwIfAborted();
84
+ const {done, value} = await reader.read();
85
+ operationSignal.throwIfAborted();
86
+ if (done) break;
87
+ // Base64 belongs only to the existing JSON transport boundary.
88
+ const contentBase64 = encodeChunk(value);
89
+ await client.invoke('modelAssets.write', {id, memberIndex, contentBase64}, {
90
+ signal: operationSignal, timeoutMs: 0
91
+ });
92
+ progress('write', memberIndex);
93
+ }
94
+ completed += 1;
95
+ progress('write', memberIndex);
96
+ } catch (error) {
97
+ retainFailure(error);
98
+ throw error;
99
+ } finally {
100
+ operationSignal.removeEventListener('abort', cancelReader);
101
+ if (operationSignal.aborted) cancelReader();
102
+ if (cancellation) await cancellation;
103
+ try { reader?.releaseLock(); } catch (error) {
104
+ if (failed) cleanupErrors.push(error);
105
+ else retainFailure(error);
106
+ }
107
+ }
108
+ }
109
+
110
+ try {
111
+ progress('open');
112
+ openingStarted = true;
113
+ await client.invoke('modelAssets.open', {
114
+ id, workingDirectory,
115
+ members: members.map(function describeMember(member) { return {path: member.path}; })
116
+ }, {signal: operationSignal, timeoutMs: 0});
117
+ operationSignal.throwIfAborted();
118
+ const results = await Promise.allSettled(members.map(transferMember));
119
+ for (const result of results) {
120
+ if (result.status === 'rejected') retainFailure(result.reason);
121
+ }
122
+ if (failed) throw failure;
123
+ if (cleanupErrors.length) {
124
+ throw new AggregateError(cleanupErrors.splice(0), 'Model asset streams could not be released.');
125
+ }
126
+ progress('complete');
127
+ const projection = await client.invoke('modelAssets.complete', {id}, {
128
+ signal: operationSignal, timeoutMs: 0
129
+ });
130
+ progress('ready');
131
+ operationSignal.throwIfAborted();
132
+ // The caller releases ownership after native consumers retain this projection.
133
+ return {id, directory: projection.directory, members: projection.members, release};
134
+ } catch (error) {
135
+ retainFailure(error);
136
+ if (openingStarted) {
137
+ try { await release(); } catch (cleanupError) { cleanupErrors.push(cleanupError); }
138
+ }
139
+ if (cleanupErrors.length) {
140
+ throw new AggregateError([failure, ...cleanupErrors],
141
+ 'Model asset preparation and cleanup failed.', {cause: failure});
142
+ }
143
+ throw failure;
144
+ }
145
+ }
146
+
147
+ function encodeChunk(content) {
148
+ if (is.function(content.toBase64)) return content.toBase64();
149
+ let binary = '';
150
+ for (const value of content) binary += String.fromCharCode(value);
151
+ return btoa(binary);
152
+ }
@@ -569,7 +569,7 @@ export function createCoreFacade(client){
569
569
  mail:{send:request=>invoke('mail.send',request??{},{timeoutMs:450000})},
570
570
  speech:{status:()=>invoke('speech.status',{},{timeoutMs:10000}),
571
571
  synthesize:(request,options)=>invoke('speech.synthesize',request??{},{timeoutMs:180000,signal:options?.signal}),
572
- transcribe:request=>invoke('speech.transcribe',request??{},{timeoutMs:180000})},
572
+ transcribe:(request,options)=>invoke('speech.transcribe',request??{},{timeoutMs:180000,signal:options?.signal})},
573
573
  localAI:{status:()=>invoke('localai.status',{},{timeoutMs:15000}),
574
574
  ensurePlatform:()=>invoke('localai.platform.ensure',{},long),recover:request=>invoke('localai.services.recover',request??{},long),
575
575
  setParallelRequests:request=>invoke('localai.parallel.requests.set',request??{},long),
@@ -616,6 +616,14 @@ export function createCoreFacade(client){
616
616
  storage:{list:()=>invoke('storage.list'),get:key=>invoke('storage.get',{key}),set:(key,value)=>invoke('storage.set',{key,value}),delete:key=>invoke('storage.delete',{key})},
617
617
  preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
618
618
  setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
619
+ window: {
620
+ setTheme(presentation, options) {
621
+ return invoke(
622
+ 'window.setTheme', presentation,
623
+ {signal: options?.signal}
624
+ );
625
+ }
626
+ },
619
627
  appearance:{current:()=>invoke('appearance.current'),apply:appearance=>invoke('appearance.apply',appearance??{})},session:{logout:()=>invoke('session.logout')},
620
628
  provisioning:{plan:usernames=>invoke('provisioning.plan',{usernames:Array.isArray(usernames)?usernames:[usernames].filter(Boolean)})},
621
629
  diagnostics:{recentErrors:()=>invoke('diagnostics.recent'),get:diagnosticId=>invoke('diagnostics.get',{diagnosticId})},
@@ -32,6 +32,11 @@ export const CORE_METHOD_CONTRACTS={
32
32
  'preferences.set':{input:'{key,value}',output:'{key,value}'},
33
33
  'preferences.setMany':{input:'{entries}',output:'preference-batch-result'},
34
34
  'preferences.delete':{input:'{key}',output:'{key,deleted}'},
35
+ 'window.setTheme': {
36
+ input: '{backgroundColor?,textColor?}: null reset or {red,green,blue,alpha}',
37
+ output: '{platform,supported,applied,unsupported}',
38
+ meaning: 'Current host window only. Omitted colors stay unchanged; applied records platform-accepted values, not measured pixels or an atomic operation.'
39
+ },
35
40
  'terminal.start':{input:'terminal-start-v1',output:'terminal-session-v1'},
36
41
  'terminal.list':{input:'empty-object-v1',output:'terminal-list-v1'},
37
42
  'terminal.write':{input:'terminal-write-v1',output:'terminal-write-result-v1'},
@@ -42,6 +42,7 @@ alone does not make bare module names resolve in a browser.
42
42
  | Inspect complete AI and speech calls using the shared developer-mode preference | [Shared logger](sdk-api.md#arcanelogging) and [speech developer diagnostics](ai/browser-speech.md#developer-diagnostics) |
43
43
  | Use the `arcane` command | [CLI reference](cli.md) |
44
44
  | Install an app and cache selected resources offline | [Progressive web applications](pwa.md) |
45
+ | Disable source PWA generation or explicitly retire confirmed generated output | [Source PWA retirement](pwa.md#disable-source-pwa-generation-and-retire-generated-output) |
45
46
  | Generate named browser imports or inspect the selected physical runtime | [`arcane import-map`](cli.md#arcane-import-map) and [browser runtime delivery](protocols.md#browser-runtime-delivery) |
46
47
  | Connect an application through the shared WebSocket transport | [Shared WebSocket clients](websocket-client.md) |
47
48
  | Choose browser, native, cloud, or cross-host behavior | [Availability and normalization](availability-and-normalization.md) |
@@ -51,12 +52,14 @@ alone does not make bare module names resolve in a browser.
51
52
  | Submit complete speech parts immediately and play them in order | [`SpeechPlayback`](runtime-modules.md#speechplaybackjs) and the [basic browser example](ai/browser-speech.md#play-a-complete-array-with-speechplayback) |
52
53
  | Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
53
54
  | Connect a browser to one Core transport | [Core browser client](core-client.md) |
55
+ | Cancel one Core speech synthesis request without changing its payload | [Speech request lifetime](core-client.md#events-and-request-lifetime) and [Core facade](sdk-api.md#createcorefacade) |
54
56
  | Compose native Core dispatch, service lifecycle, and stdio hosting | [Native Core runtime](core-runtime.md) |
55
57
  | Run application-selected llama.cpp, Ollama or ONNX through Core | [Local AI through Core](local-ai.md) |
56
58
  | Retain a Nemotron model and diarize caller-fed audio streams | [Native speaker diarization](diarization.md) |
57
59
  | Persist application-selected preferences through Core | [Core preferences service](core-preferences.md) |
58
60
  | Read complete selected Git text without changing a checkout | [Git text snapshots](git-text-snapshot.md) |
59
61
  | Build an installed-SDK portable Core payload | [Portable Core packaging](core-native-packaging.md) |
62
+ | Pass host-selected state defaults and unchanged explicit launch values to services | [Launch-time locations](core-native-packaging.md#launch-time-locations) |
60
63
  | Build a Windows executable without an OS checkout | [Windows executable packaging](core-native-packaging.md#windows-executable) |
61
64
  | Compose a Mac application with an architecture-matched host | [macOS application composition](core-native-packaging.md#macos-application-composition) |
62
65
  | Subscribe to native events | [Arcane event reference](core/arcane-events.md) |
@@ -542,7 +542,7 @@ validate the speech health response.
542
542
 
543
543
  ### Speech synthesis request
544
544
 
545
- `Arcane.speech.synthesize(request)` accepts the portable
545
+ `Arcane.speech.synthesize(request, options?)` accepts the portable
546
546
  `SpeechSynthesisRequest` fields below. Supply the documented primitive types and
547
547
  lowercase values; do not rely on host-specific coercion or empty-string
548
548
  defaulting.
@@ -555,6 +555,22 @@ defaulting.
555
555
  | `responseFormat` | `"opus" \| "wav"` | No | Default `"opus"` | Audio format. |
556
556
  | `speed` | `number` | No | Default `1`; 0.5-2 inclusive | Speaking speed. |
557
557
 
558
+ ### Speech synthesis controls
559
+
560
+ The optional second argument is `SpeechSynthesisControls`:
561
+
562
+ | Field | Type | Required | Default | Description |
563
+ |---|---|---:|---|---|
564
+ | `signal` | `AbortSignal` | No | No caller signal | Cancels this request through Core, separately from the complete synthesis payload. The registered service owns actual engine interruption. |
565
+
566
+ Existing one-argument calls retain their behavior and the 180-second timeout.
567
+ A pre-aborted signal dispatches no request. An in-flight abort sends
568
+ `request.cancel`, rejects with `ARCANE_REQUEST_ABORTED` and suppresses late
569
+ responses while unrelated requests continue. Other controls properties are
570
+ unused and do not enter the payload. `AI.fetchTTS(payload, signal)` forwards
571
+ the signal into this native call and reports cancellation as `AbortError` with
572
+ `ARCANE_AI_REQUEST_ABORTED`.
573
+
558
574
  ### Speech synthesis result
559
575
 
560
576
  | Field | Type | Required | Constraints / default | Description |
@@ -564,7 +580,7 @@ defaulting.
564
580
 
565
581
  ### Speech transcription request
566
582
 
567
- `Arcane.speech.transcribe(request)` accepts an exact
583
+ `Arcane.speech.transcribe(request, options?)` accepts an exact
568
584
  `SpeechTranscriptionRequest`.
569
585
 
570
586
  | Field | Type | Required | Constraints / default | Description |
@@ -573,6 +589,19 @@ defaulting.
573
589
  | `mimeType` | `string` | No | Default `"audio/webm"`; base type must be `audio/webm` | Media type; codec parameters may follow. |
574
590
  | `model` | `string` | No | Default `"whisper-small"`; `whisper(?:[._-][a-z0-9]+)*` | Whisper identifier. The fixed service rejects unsupported variants. |
575
591
 
592
+ ### Speech transcription controls
593
+
594
+ The optional second argument is `SpeechTranscriptionControls`:
595
+
596
+ | Field | Type | Required | Default | Description |
597
+ |---|---|---:|---|---|
598
+ | `signal` | `AbortSignal` | No | No caller signal | Cancels this request through Core. The facade forwards it separately from the complete audio request; the registered service owns actual engine interruption. |
599
+
600
+ Existing one-argument calls retain their behavior. The facade's request timeout
601
+ remains 180 seconds. A pre-aborted signal dispatches no request, and an
602
+ in-flight abort cancels the request and suppresses late responses. Other
603
+ properties of this controls record are unused; they do not enter the payload.
604
+
576
605
  ### Speech transcription result
577
606
 
578
607
  | Field | Type | Required | Constraints / default | Description |
@@ -48,6 +48,7 @@ This inventory covers every enumerable, non-method member exposed directly or th
48
48
  | `Arcane.filesystem` | Namespace | Native filesystem selection operations. |
49
49
  | `Arcane.storage` | Namespace | App-scoped native key/value storage. |
50
50
  | `Arcane.preferences` | Namespace | App-scoped preference reads and atomic writes. |
51
+ | `Arcane.window` | Namespace | SDK host window presentation for the calling document; adapter availability is independent of the OS-user appearance service. |
51
52
  | `Arcane.appearance` | Namespace | Native appearance state and application. |
52
53
  | `Arcane.session` | Namespace | Current Arcane session lifecycle. |
53
54
  | `Arcane.provisioning` | Namespace | Provisioning-plan operations. |
@@ -104,8 +105,8 @@ directly.
104
105
  | `Arcane.localAI.recover(request)` | `request`: [`LocalAIRecoveryRequest`](arcane-ai-contracts.md#local-ai-recovery-request) | [`Promise<LocalAIRecoveryResult>`](arcane-ai-contracts.md#local-ai-recovery-result) | Requests one privileged, exclusive, start-existing-service-only recovery of selected Arcane-managed local services. Requires `ai.inference` and explicit application admission. |
105
106
  | `Arcane.localAI.setParallelRequests(request)` | `request`: [`LocalAIParallelRequestsRequest`](arcane-ai-contracts.md#local-ai-parallel-request-configuration) | [`Promise<LocalAIParallelRequestsResult>`](arcane-ai-contracts.md#local-ai-parallel-request-configuration) | Solves one app-owned verified model/context's maximum directly from native model and memory/resource evidence without a product ceiling, clamps a positive request to that result (`0` requests the maximum), and changes the machine-wide managed Ollama setting only when needed. A tentative change uses one fresh stopped-service snapshot before the final value is written, then Core ensures, loads, and confirms the exact model. A restart unloads resident models. Automatic mutation currently requires the dedicated `ai.runtime.manage` capability on Microsoft NT desktop Core and operating-system authorization; Linux returns administrator-managed systemd guidance, and Android user-managed loopback sessions do not invoke it. |
106
107
  | `Arcane.speech.status()` | None | [`Promise<SpeechStatus>`](arcane-ai-contracts.md#speech-status) | Reads bounded, independent Kokoro and Whisper readiness through Core or an admitted Android native host. Requires `ai.inference`. |
107
- | `Arcane.speech.synthesize(request)` | `request`: [`SpeechSynthesisRequest`](arcane-ai-contracts.md#speech-synthesis-request) | [`Promise<SpeechSynthesisResult>`](arcane-ai-contracts.md#speech-synthesis-result) | Synthesizes bounded local speech through the native host and fixed loopback runtime. Requires `ai.inference`. |
108
- | `Arcane.speech.transcribe(request)` | `request`: [`SpeechTranscriptionRequest`](arcane-ai-contracts.md#speech-transcription-request) | [`Promise<SpeechTranscriptionResult>`](arcane-ai-contracts.md#speech-transcription-result) | Sends bounded canonical base64 bytes labeled `audio/webm` to local Whisper through the native host. The bridge validates encoding, label, and bounds; the fixed service parses the media container. Requires `ai.inference`. |
108
+ | `Arcane.speech.synthesize(request, options?)` | `request`: [`SpeechSynthesisRequest`](arcane-ai-contracts.md#speech-synthesis-request); `options?`: [`SpeechSynthesisControls`](arcane-ai-contracts.md#speech-synthesis-controls) | [`Promise<SpeechSynthesisResult>`](arcane-ai-contracts.md#speech-synthesis-result) | Sends the complete request to the selected native synthesis service; optional cancellation reaches Core's request lifetime. The registered host service owns audio generation and actual engine interruption. |
109
+ | `Arcane.speech.transcribe(request, options?)` | `request`: [`SpeechTranscriptionRequest`](arcane-ai-contracts.md#speech-transcription-request); `options?`: [`SpeechTranscriptionControls`](arcane-ai-contracts.md#speech-transcription-controls) | [`Promise<SpeechTranscriptionResult>`](arcane-ai-contracts.md#speech-transcription-result) | Sends the complete request to the selected native transcription service; optional cancellation reaches Core's request lifetime. The registered host service owns media decoding and actual engine interruption. |
109
110
  | `Arcane.ollama.version()` | None | [`Promise<OllamaVersionResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Gets the managed Ollama version for Settings, Terminal, and Shell diagnostics. |
110
111
  | `Arcane.ollama.models()` | None | [`Promise<OllamaModelsResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Lists the raw provider inventory envelope; it is not the application admission catalog. |
111
112
  | `Arcane.ollama.list()` | None | [`Promise<OllamaModelsResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Alias of the raw `models()` inventory. |
@@ -438,6 +439,16 @@ The canonical authority registry in `machine_bundles/arcane-os-machine-bundle/sr
438
439
 
439
440
  ## Filesystem, storage, preferences, and appearance
440
441
 
442
+ The SDK's additive `Arcane.window.setTheme(presentation, {signal}?)` controls
443
+ the current host window's `backgroundColor` and `textColor`, with per-field
444
+ accepted/unsupported results. See the [window contract](../core-client.md#current-window-theme)
445
+ for numeric RGBA, reset, cancellation, navigation and platform availability.
446
+ It does not call the OS-user `appearance.apply` service described below.
447
+
448
+ | SDK window method | Parameters | Return | Description |
449
+ |---|---|---|---|
450
+ | `Arcane.window.setTheme(presentation, options?)` | Optional `backgroundColor`/`textColor`: `{red,green,blue,alpha}` or `null`; `options.signal?` | `Promise<{platform,supported,applied,unsupported}>` | Applies supported colors only to the caller's native window, preserving omitted fields. Uses ordinary request cancellation and reports actual per-field acceptance. See the linked contract for adapter availability. |
451
+
441
452
  | Method | Parameters | Return | Description |
442
453
  |---|---|---|---|
443
454
  | `Arcane.filesystem.selectDirectory(options?)` | `options?`: `{title?, initialPath?}`; title up to 200 plain-text characters; initial path must be an existing absolute directory | `Promise<{cancelled:boolean, path:string\|null}>` | Opens the native directory picker after a user action. It returns one canonical existing directory or explicit cancellation; it does not enumerate or grant access to directory contents. Requires `filesystem.directory.select`. |
@@ -646,7 +646,7 @@ console.info({
646
646
 
647
647
  ### Overview
648
648
 
649
- `Arcane.speech.synthesize(request)` converts bounded text to local speech. Use
649
+ `Arcane.speech.synthesize(request, options?)` converts bounded text to local speech. Use
650
650
  it only after checking synthesis readiness, and create an audio URL from the
651
651
  returned bytes without logging the base64 payload.
652
652
 
@@ -683,27 +683,36 @@ owned active operation before retrying.
683
683
  ### Streaming, cancellation, and events
684
684
 
685
685
  Synthesis returns one complete audio result and emits no method event. The
686
- wrapper timeout is 180 seconds, no signal is exposed, and timeout or page
687
- teardown does not promise cancellation of Core or Android speech work.
686
+ wrapper timeout remains 180 seconds. Optional `{signal}` controls the Core
687
+ request lifetime separately from the complete synthesis payload. Pre-abort
688
+ sends no request; in-flight abort sends `request.cancel`, rejects with
689
+ `ARCANE_REQUEST_ABORTED` and suppresses late responses. Unrelated requests
690
+ continue. The registered native service owns actual synthesis interruption and
691
+ resource release; the facade supplies no engine. `AI.fetchTTS(payload, signal)`
692
+ forwards its signal through this call and normalizes cancellation to
693
+ `AbortError` with `ARCANE_AI_REQUEST_ABORTED`.
688
694
 
689
695
  ### Example
690
696
 
691
697
  ```javascript
692
- const result = await globalThis.Arcane.speech.synthesize({
693
- input: 'Your local speech service is ready.',
694
- voice: 'af_heart',
695
- responseFormat: 'opus',
696
- speed: 1
697
- });
698
-
699
- console.info(result.contentType, result.audioBase64.length);
698
+ async function synthesizeMoonEncore(input, signal) {
699
+ return globalThis.Arcane.speech.synthesize(
700
+ {
701
+ input,
702
+ voice: 'af_heart',
703
+ responseFormat: 'opus',
704
+ speed: 1
705
+ },
706
+ {signal}
707
+ );
708
+ }
700
709
  ```
701
710
 
702
711
  ## Arcane.speech.transcribe()
703
712
 
704
713
  ### Overview
705
714
 
706
- `Arcane.speech.transcribe(request)` sends bounded caller-supplied audio bytes,
715
+ `Arcane.speech.transcribe(request, options?)` sends bounded caller-supplied audio bytes,
707
716
  labeled as WebM, to the fixed local transcription service. Use it after local
708
717
  capture permission and recording have succeeded; the method itself does not
709
718
  capture audio or grant microphone permission.
@@ -739,18 +748,21 @@ work after `ANDROID_SPEECH_QUEUE_FULL`.
739
748
  ### Streaming, cancellation, and events
740
749
 
741
750
  Transcription resolves one complete text result and emits no method event. The
742
- wrapper timeout is 180 seconds and no signal is exposed. Core and Android do
743
- not promise cancellation when the renderer stops waiting.
751
+ wrapper timeout remains 180 seconds. Optional `{signal}` controls the Core
752
+ request lifetime separately from the complete audio payload. Pre-abort sends
753
+ no request; in-flight abort cancels that request and suppresses late responses.
754
+ The registered native service owns actual inference interruption and resource
755
+ release. `AI.fetchSTT(audio, signal)` forwards its signal through this call.
744
756
 
745
757
  ### Example
746
758
 
747
759
  ```javascript
748
- async function transcribeRecordedWebM(audioBase64) {
760
+ async function transcribeRecordedWebM(audioBase64, signal) {
749
761
  const result = await globalThis.Arcane.speech.transcribe({
750
762
  audioBase64: audioBase64,
751
763
  mimeType: 'audio/webm',
752
764
  model: 'whisper-small'
753
- });
765
+ }, {signal});
754
766
  return result.text;
755
767
  }
756
768
  ```
@@ -1038,6 +1038,20 @@ const canReadPreferences = access.methods.includes('preferences.list');
1038
1038
  console.log('Preference inventory admitted:', canReadPreferences);
1039
1039
  ```
1040
1040
 
1041
+ ## Arcane.window
1042
+
1043
+ The SDK client exposes `Arcane.window.setTheme(presentation, {signal}?)` for
1044
+ the window hosting the calling document. The optional `backgroundColor` and
1045
+ `textColor` accept numeric RGBA records or `null` for platform-default reset.
1046
+ The host reports each applied or unsupported field independently. Rendering
1047
+ and browser theme changes continue without waiting for this request.
1048
+
1049
+ Shared ThemeBootstrap forwards the app's computed semantic colors through
1050
+ this method when a native host is present. It does not change OS-user
1051
+ appearance. See the [current-window contract](../../../core-client.md#current-window-theme)
1052
+ for complete usage, accepted channel values, cancellation and current adapter
1053
+ availability. A facade method alone does not establish host support.
1054
+
1041
1055
  ## Arcane.appearance
1042
1056
 
1043
1057
  ### Overview
@@ -83,6 +83,80 @@ the 180,000 ms timeout and pass the complete request unchanged. A pre-aborted
83
83
  signal sends no synthesis request; an in-flight abort sends `request.cancel`
84
84
  and suppresses late responses. Actual synthesis interruption depends on the
85
85
  registered speech service and its engine; the facade supplies no speech engine.
86
+ `AI.fetchTTS(payload, signal)` carries its caller-owned signal into this native
87
+ call and normalizes cancellation to `AbortError` with
88
+ `ARCANE_AI_REQUEST_ABORTED`, preserving the original error as its cause.
89
+
90
+ `Arcane.speech.transcribe(request, {signal})` forwards cancellation through the
91
+ same request lifetime and retains its 180,000 ms timeout and complete request.
92
+ `AI.fetchSTT(audio, signal)` carries that signal into the native call. A
93
+ pre-aborted signal sends no transcription request; an in-flight abort sends
94
+ `request.cancel` and suppresses late responses. The registered transcription
95
+ service owns interruption and release of its actual engine operation.
96
+
97
+ ## Current-window theme
98
+
99
+ `Arcane.window.setTheme(presentation, {signal}?)` changes only the window
100
+ hosting the calling document. It uses the existing `window.setTheme` request
101
+ and response correlation; it neither calls `appearance.apply` nor changes the
102
+ operating-system user's appearance. The complete presentation object reaches
103
+ the host unchanged.
104
+
105
+ Each optional `backgroundColor` and `textColor` is either `null` to restore the
106
+ platform default or `{red, green, blue, alpha}`. All four channels are numbers:
107
+ finite sRGB red/green/blue from 0 through 255 and alpha from 0 through 1.
108
+ Omitting a field leaves its current value unchanged. There is no scheme field.
109
+
110
+ ```js
111
+ const host=globalThis.Arcane;
112
+ if(host?.runtime?.current().native&&host.window?.setTheme){
113
+ host.window.setTheme(
114
+ {
115
+ backgroundColor:{red:29,green:22,blue:19,alpha:1},
116
+ textColor:{red:248,green:239,blue:229,alpha:1}
117
+ }
118
+ ).then(
119
+ function reportWindowColors(result){
120
+ console.log('Moon observatory window colors:',result);
121
+ }
122
+ ).catch(
123
+ function reportWindowFailure(error){
124
+ console.error('Window color update failed.',error);
125
+ }
126
+ );
127
+ }
128
+ // Page rendering continues independently of the host request.
129
+ ```
130
+
131
+ The result is `{platform, supported, applied, unsupported}`. `applied` contains
132
+ only requested fields the platform accepted in this call, including `null`
133
+ for an accepted default reset. `unsupported` lists requested fields the adapter
134
+ cannot apply. `supported` is true when at least one field was applied. These
135
+ are accepted settings, not measured pixels, and the operation is not atomic.
136
+ Malformed arguments reject with `INVALID_ARGUMENT`; native API failures reject
137
+ with `ARCANE_WINDOW_THEME_FAILED`, preserving the native error and
138
+ `details.applied`/`details.unsupported` for earlier results in this call.
139
+
140
+ The Windows host maps background/text to DWM caption/text attributes supported
141
+ on Windows 11 build 22000 and later, retaining normal system window controls.
142
+ It rounds RGB channels to the nearest integer, with midpoint values rounded
143
+ up, and returns those actual accepted values. Translucent colors remain
144
+ unsupported without changing the existing field. Older Windows reports those
145
+ fields unsupported. This contract makes no promise about caption-button colors.
146
+ The current macOS and Linux hosts have no implementation of this method;
147
+ their ordinary unavailable-method response remains observable. A new facade
148
+ alone does not add the operation to an older native executable. Use the
149
+ matching published host artifact for the selected SDK version.
150
+
151
+ Cancellation uses the normal request lifetime and suppresses late responses;
152
+ it does not roll back a setting the host already accepted. Window settings
153
+ survive document navigation until a subsequent document changes them or the
154
+ window closes. Responses belong to their originating document.
155
+
156
+ The shared [ThemeBootstrap](runtime-modules.md#themebootstrapjs) automatically
157
+ forwards the app's computed `--background` and `--text-color` in native hosts.
158
+ Applications using that owner need no duplicate theme listener. Ordinary
159
+ browsers keep their CSS presentation without making a native request.
86
160
 
87
161
  ## Events and request lifetime
88
162
 
@@ -303,6 +303,70 @@ observes complete diagnostics and exit. Cancelling that operation closes its
303
303
  input, allowing the window and accepted Core service work to drain without a
304
304
  forced process termination.
305
305
 
306
+ ### Application icon and window colors
307
+
308
+ `appDescriptor.native.icon` selects the application's existing app-relative
309
+ image. Windows assembly converts PNG images, including a 512-pixel source,
310
+ into a derived `runtime/arcane-app.ico` with 16, 32, 48 and 256-pixel images.
311
+ Aspect ratio and transparency are preserved. An ICO selection keeps its
312
+ authored image set. The original application asset remains unchanged.
313
+
314
+ The provider embeds the selected images in the copied `Arcane.exe` and records
315
+ `manifest.host.icon` and `manifest.host.executableIcon:true`. It replaces the
316
+ executable's main icon group while preserving other groups, languages, resources
317
+ and file content. Conversion uses Node standard capabilities on every assembly
318
+ platform; it adds no application compiler, image library or Windows-only build
319
+ step. The selected host cache and Core executable remain unchanged.
320
+
321
+ JPEG/JPG, WebP and other existing descriptor formats retain ordinary assembly.
322
+ They currently report `native.icon.unsupported` through the builder's `onEvent`
323
+ callback, as does a selected executable whose PE layout cannot accommodate the
324
+ derived icon. The event identifies the app, icon, target, code and complete
325
+ message; `manifest.host.executableIcon:false` reports that the executable keeps
326
+ its prior icon. `host.icon` then points to the original selected image. This is
327
+ an explicit unsupported branding result, not a claim that a generic icon was
328
+ replaced. Unreadable files and malformed supported image data report their
329
+ ordinary filesystem or image-parser error.
330
+
331
+ The matching Windows launcher reads `host.icon` and loads it independently of
332
+ Core and page startup. ICO and standard Windows image codecs supply the window
333
+ and taskbar icon; JPEG/JPG can therefore work at runtime even while executable
334
+ embedding is unsupported. WebP is outside the host's standard image codecs;
335
+ runtime support is not promised. A runtime icon error preserves ordinary startup
336
+ and reaches complete developer diagnostics as `ARCANE_WINDOW_ICON_UNAVAILABLE`.
337
+ Hosts built before this option was introduced do not read the new icon field.
338
+
339
+ `Arcane.window.setTheme(presentation,{signal}?)` changes only the requesting
340
+ native window's chrome. The shared ThemeBootstrap supplies computed application
341
+ background and text colors without delaying application CSS or rendering.
342
+ Direct callers may supply `backgroundColor` and `textColor`, each either
343
+ `{red,green,blue,alpha}` or `null` to restore the platform default. RGB channels
344
+ are finite numbers from 0 through 255 and alpha is from 0 through 1. Omitted
345
+ fields remain unchanged. The complete presentation follows the existing Core
346
+ request transport; the window host handles `window.setTheme` before forwarding
347
+ other methods to Core. No system appearance setting changes.
348
+
349
+ Windows 11 build 22000 and later support titlebar background and text colors
350
+ through DWM. The normal titlebar and window controls remain in place. DWM accepts
351
+ opaque colors, so alpha values other than exactly 1 report that field as
352
+ unsupported. RGB values round to the nearest integer, with midpoint values
353
+ rounded upward. Older Windows versions report the requested fields unsupported.
354
+
355
+ The result is `{platform:'windows',supported,applied,unsupported}`. `applied`
356
+ contains only fields accepted during that call, including rounded channels or
357
+ `null` for a successful default reset. `supported` is true when at least one
358
+ field applied; `unsupported` lists requested fields the platform could not
359
+ apply. These results report platform acceptance rather than measured pixels.
360
+ An invalid argument uses `INVALID_ARGUMENT`; a platform API failure uses
361
+ `ARCANE_WINDOW_THEME_FAILED`, preserving complete native details and any
362
+ earlier changes in `error.details.applied` and `error.details.unsupported`.
363
+ The call is not atomic across fields. Existing request cancellation and document
364
+ lifetime semantics remain in effect; an already accepted native color change
365
+ is not rolled back by later cancellation. See [the Core client](core-client.md)
366
+ for the facade and browser behavior.
367
+
368
+ ### Launch configuration
369
+
306
370
  For explicit user-selected workspace/state locations, launch:
307
371
 
308
372
  ```sh
@@ -331,8 +395,8 @@ exit. It does not kill the child on an ordinary close.
331
395
  `ArcaneHost.cs` composes that process with a WebView2 window through
332
396
  `ArcaneHost.Run(options,onDiagnostic,onError)` or `ArcaneHostForm`. Options
333
397
  select the app root/start path, stable virtual HTTPS origin, profile directory,
334
- title, canonical classic-client source and Core process inputs. The form exposes
335
- `Ready`, `Completion` and `CloseAsync()`. Page navigation, transport connection,
398
+ title, optional `IconPath`, canonical classic-client source and Core process
399
+ inputs. The form exposes `Ready`, `Completion` and `CloseAsync()`. Page navigation, transport connection,
336
400
  Core readiness and service readiness are separate states. Accepted work drains
337
401
  before the window closes; complete engineering errors go to the supplied
338
402
  diagnostic/error callbacks. Callbacks must not synchronously wait for the