arcane-os 0.68.0 → 0.69.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 +16 -0
- package/README.md +9 -4
- package/browser-runtime/ai/browser-speech-providers.mjs +5 -3
- package/docs/reference/README.md +2 -0
- package/docs/reference/ai/browser-speech.md +49 -9
- package/docs/reference/core-native-packaging.md +41 -0
- package/docs/reference/inventory/package-api.json +3 -3
- package/docs/reference/runtime-components.md +4 -2
- package/docs/reference/runtime-modules.md +23 -13
- package/docs/reference/sdk-api.md +23 -0
- package/package.json +1 -1
- package/runtime/arcane/components/theme-switcher.html +1 -1
- package/runtime/arcane/modules/AI.js +5 -4
- package/runtime/arcane/modules/AIProviderRuntime.js +7 -3
- package/schemas/arcane-app.schema.json +10 -0
- package/src/app-descriptor.mjs +23 -1
- package/src/core/hosts/windows/ArcaneHost.cs +46 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +21 -0
- package/src/native/portable-provider.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.69.0
|
|
4
|
+
|
|
5
|
+
- Add explicit `textFormat:'plain'` speech input through prepared speech,
|
|
6
|
+
direct TTS requests and the browser Kokoro provider. Preserve complete input,
|
|
7
|
+
whitespace and formatting characters, original-part cache identity and
|
|
8
|
+
cancellation while leaving default narration behavior unchanged. This input
|
|
9
|
+
option does not change the selected engine's synthesis limits.
|
|
10
|
+
- Add optional `native.window` width, height and resizable configuration.
|
|
11
|
+
Forward logical client dimensions through native manifests; the matching
|
|
12
|
+
Windows host applies initial DPI-aware usable-screen fitting while preserving
|
|
13
|
+
omitted defaults and subsequent application/user sizing. Other platform
|
|
14
|
+
adapters retain their existing sizing behavior.
|
|
15
|
+
- Preserve selected theme-switcher colors during hover by applying ordinary
|
|
16
|
+
hover fill only to unselected enabled buttons. Keep complete labels, focus,
|
|
17
|
+
disabled states, tokens and per-instance configuration unchanged.
|
|
18
|
+
|
|
3
19
|
## 0.68.0
|
|
4
20
|
|
|
5
21
|
- Add per-instance theme-switcher `configure({modes,labels})` and initial
|
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.
|
|
23
|
+
This checkout defines the `0.69.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;
|
|
@@ -469,11 +469,16 @@ keeping apostrophes, commas, and hyphens that join Unicode letters or numbers
|
|
|
469
469
|
inside the same segment.
|
|
470
470
|
Mute, stop, provider transition, and cancellation still govern the whole queue.
|
|
471
471
|
|
|
472
|
-
|
|
472
|
+
TTS narration removes repeated same formatting marks (`*`, `#`,
|
|
473
473
|
`_`, backtick, `~`) from its outbound speech-input copy before synthesis.
|
|
474
474
|
Streaming calls also recognize a run split across chunks. Single marks and
|
|
475
|
-
ordinary repeated punctuation remain literal.
|
|
476
|
-
|
|
475
|
+
ordinary repeated punctuation remain literal. Complete synthesis calls may
|
|
476
|
+
select `textFormat:'plain'` to preserve exact input: supply it in `prepareTTS`
|
|
477
|
+
options or the payload of `fetchTTS`, provider-runtime TTS requests, or direct
|
|
478
|
+
Kokoro requests. The SDK consumes this control before engine/host dispatch;
|
|
479
|
+
omission keeps the default cleanup. Streaming narration and `SpeechPlayback`
|
|
480
|
+
retain their existing formatting behavior. This option does not change the
|
|
481
|
+
selected engine's input capacity or establish complete upstream synthesis. Original
|
|
477
482
|
messages and caller payloads remain unchanged for display, storage, and model
|
|
478
483
|
input. The `arcane-os/speech-text` entrypoint exports the same
|
|
479
484
|
`MarkdownSpeech` streaming class and `stripSpeechFormatting()` one-pass helper
|
|
@@ -906,7 +906,7 @@ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
|
|
|
906
906
|
if (shared) {
|
|
907
907
|
descriptors = genericPayloadDescriptors(
|
|
908
908
|
payload,
|
|
909
|
-
["model", "voice", "input", "responseFormat", "speed"],
|
|
909
|
+
["model", "voice", "input", "textFormat", "responseFormat", "speed"],
|
|
910
910
|
["model", "input", "responseFormat"],
|
|
911
911
|
"Shared speech synthesis payload",
|
|
912
912
|
"tts-synthesis",
|
|
@@ -927,7 +927,7 @@ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
|
|
|
927
927
|
} else {
|
|
928
928
|
descriptors = genericPayloadDescriptors(
|
|
929
929
|
payload,
|
|
930
|
-
["model", "voice", "text", "speed"],
|
|
930
|
+
["model", "voice", "text", "textFormat", "speed"],
|
|
931
931
|
["text"],
|
|
932
932
|
"Browser Kokoro synthesis payload",
|
|
933
933
|
"tts-synthesis",
|
|
@@ -936,7 +936,9 @@ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
|
|
|
936
936
|
textValue = descriptors.text.value;
|
|
937
937
|
}
|
|
938
938
|
const suppliedText = is.string(textValue) ? textValue : "";
|
|
939
|
-
const text = speechInputPrepared === true
|
|
939
|
+
const text = speechInputPrepared === true || descriptors.textFormat?.value === "plain"
|
|
940
|
+
? suppliedText
|
|
941
|
+
: stripSpeechFormatting(suppliedText);
|
|
940
942
|
const voiceValue = Object.hasOwn(descriptors, "voice")
|
|
941
943
|
? descriptors.voice.value
|
|
942
944
|
: authority.defaultVoice;
|
package/docs/reference/README.md
CHANGED
|
@@ -49,6 +49,8 @@ alone does not make bare module names resolve in a browser.
|
|
|
49
49
|
| Import a shipped renderer module | [Runtime module catalog](runtime-modules.md) |
|
|
50
50
|
| Use a shared entity | [Runtime entity modules](runtime-entities.md) and [exact export contracts](core/arcane-entities.md) |
|
|
51
51
|
| Load a reusable HTML component | [Runtime component catalog](runtime-components.md) |
|
|
52
|
+
| Present Light / Dark / System choices with complete labels | [Theme-switcher configuration](runtime-components.md#theme-switcherhtml) |
|
|
53
|
+
| Use optional Core preferences and appearance without masking real failures | [PreferenceStore](runtime-modules.md#preferencestorejs), [SystemAppearance](runtime-modules.md#systemappearancejs), and [Core lookup errors](core-runtime.md#state-and-frames) |
|
|
52
54
|
| 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) |
|
|
53
55
|
| Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
|
|
54
56
|
| Connect a browser to one Core transport | [Core browser client](core-client.md) |
|
|
@@ -805,14 +805,17 @@ does not select another page or speak background work.
|
|
|
805
805
|
|
|
806
806
|
### Preparation inputs and reuse
|
|
807
807
|
|
|
808
|
-
`ai.prepareTTS({parts,storage,identity,signal,onState})` accepts an ordered array
|
|
808
|
+
`ai.prepareTTS({parts,textFormat,storage,identity,signal,onState})` accepts an ordered array
|
|
809
809
|
of strings or `{input,voice?,speed?,pauseAfterMs?}` records. An omitted voice
|
|
810
810
|
uses the selected model default; omitted speed uses `ai.voiceSpeed`; omitted
|
|
811
811
|
pause is zero. The SDK snapshots the selected speech configuration and current
|
|
812
812
|
punctuation/word-cadence options for that preparation. A part's pause applies
|
|
813
813
|
after its final extracted segment. Complete original input remains available
|
|
814
|
-
for semantic comparison;
|
|
815
|
-
copy and occurs once before segmentation.
|
|
814
|
+
for semantic comparison; default Markdown cleanup affects only the speech
|
|
815
|
+
copy and occurs once before segmentation. Set `textFormat:'plain'` to preserve
|
|
816
|
+
each supplied input exactly through that segmentation. The prepared segment
|
|
817
|
+
text participates in semantic reuse, so a changed synthesis input does not
|
|
818
|
+
reuse audio produced from the cleaned version.
|
|
816
819
|
|
|
817
820
|
`storage` is optional. When supplied, `{db,table,key}` names the caller's ready
|
|
818
821
|
DBOPFS instance and its application-owned table/key. The SDK stores complete
|
|
@@ -977,7 +980,7 @@ speech method retain their existing behavior.
|
|
|
977
980
|
## Automatic speech-input formatting cleanup
|
|
978
981
|
|
|
979
982
|
The shared streaming, audio-file, and `SpeechPlayback` paths remove repeated
|
|
980
|
-
same formatting marks from the outbound speech-input copy
|
|
983
|
+
same formatting marks from the outbound speech-input copy by default:
|
|
981
984
|
|
|
982
985
|
```javascript
|
|
983
986
|
ai.streamTTS('## Heading\n**Hello');
|
|
@@ -994,10 +997,13 @@ waits for its next character or the final flush.
|
|
|
994
997
|
|
|
995
998
|
`end:true`, `finishTTS()`, or cancellation clears pending streaming formatting
|
|
996
999
|
state. `fetchTTS()`, provider-runtime TTS requests, direct Kokoro provider
|
|
997
|
-
requests, and `SpeechPlayback` apply the same cleanup to complete input.
|
|
998
|
-
|
|
1000
|
+
requests, and `SpeechPlayback` apply the same default cleanup to complete input.
|
|
1001
|
+
Complete-input APIs can explicitly select `textFormat:'plain'` as described
|
|
1002
|
+
below. The streaming `streamTTS()` options still ignore `textFormat`;
|
|
1003
|
+
streaming narration and `SpeechPlayback` keep their existing cleanup.
|
|
999
1004
|
SDK-internal delegation carries `{speechInputPrepared:true}` outside the speech
|
|
1000
|
-
payload
|
|
1005
|
+
payload after its owner selects text treatment, preventing a second
|
|
1006
|
+
non-idempotent cleanup pass or cleanup of explicitly plain input.
|
|
1001
1007
|
Applications omit that internal metadata. Displayed messages, saved history,
|
|
1002
1008
|
model input, caller payload objects, language, voice, synthesis capacity, and
|
|
1003
1009
|
playback timing are not changed by the filter.
|
|
@@ -1023,6 +1029,39 @@ console.log(speechText.append('## Head', false)); // ' Head'
|
|
|
1023
1029
|
console.log(speechText.append('ing', true)); // ing
|
|
1024
1030
|
```
|
|
1025
1031
|
|
|
1032
|
+
### Exact complete synthesis input
|
|
1033
|
+
|
|
1034
|
+
Use the public `textFormat:'plain'` selection for already-authored speech or an
|
|
1035
|
+
exact generated preview. It belongs in `prepareTTS` options, or alongside the
|
|
1036
|
+
`input`/`text` field of a complete synthesis payload:
|
|
1037
|
+
|
|
1038
|
+
```javascript
|
|
1039
|
+
function preparePreview(ai, preview, signal) {
|
|
1040
|
+
return ai.prepareTTS({parts: [preview], textFormat: 'plain', signal});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function fetchPreview(ai, preview, signal) {
|
|
1044
|
+
return ai.fetchTTS({input: preview, textFormat: 'plain'}, signal);
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
The same payload field is supported by `AI.prepareTTSPlayback()` for audio-file
|
|
1049
|
+
providers, `AIProviderRuntime.request('tts', {operation:'synthesize', payload,
|
|
1050
|
+
localOnly, signal})`, its `synthesize(payload, options)` alias, and direct
|
|
1051
|
+
Kokoro `request()` calls using either `{text,...}` or `{model,input,...}`.
|
|
1052
|
+
The SDK consumes `textFormat` before engine/host dispatch. The original nonblank input,
|
|
1053
|
+
including whitespace and repeated formatting marks, remains unchanged; voice,
|
|
1054
|
+
speed, selected model, cancellation and audio ordering retain their existing
|
|
1055
|
+
owners. Only `'plain'` selects exact input; omission and other values retain
|
|
1056
|
+
the existing narration cleanup. Applications never supply the internal
|
|
1057
|
+
`speechInputPrepared` argument.
|
|
1058
|
+
|
|
1059
|
+
This selection governs SDK text treatment, not the selected engine's input
|
|
1060
|
+
capacity or pronunciation. [Kokoro.js 1.2.1](https://github.com/hexgrad/kokoro/blob/664c76a704021239ba59c84dcbaa4d3dece01fe9/kokoro.js/src/kokoro.js)
|
|
1061
|
+
still tokenizes with truncation in both `generate()` and `stream()`; plain input
|
|
1062
|
+
does not establish complete audio for an oversized engine input. The SDK's existing complete-text forwarding
|
|
1063
|
+
fixture does not prove upstream synthesis completeness.
|
|
1064
|
+
|
|
1026
1065
|
## Choose a device or reduce memory use
|
|
1027
1066
|
|
|
1028
1067
|
Local Whisper and Kokoro roles accept
|
|
@@ -1610,10 +1649,11 @@ const result = await kokoro.request({
|
|
|
1610
1649
|
The voice belongs to the caller-selected inventory; omission uses that model's
|
|
1611
1650
|
default voice. The provider-native result is
|
|
1612
1651
|
`{audio:Float32Array,sampleRate,voice}`. The provider/2 shared request form accepts
|
|
1613
|
-
`{model,input,responseFormat:'wav',voice?,speed?}` and returns
|
|
1652
|
+
`{model,input,textFormat?,responseFormat:'wav',voice?,speed?}` and returns
|
|
1614
1653
|
`{audio:Uint8Array,contentType:'audio/wav'}`. High-level `AI.fetchTTS()` wraps
|
|
1615
1654
|
that provider result in a WAV `Blob`. Returned provider records remain ordinary
|
|
1616
|
-
mutable values.
|
|
1655
|
+
mutable values. Both payload forms accept `textFormat:'plain'` to retain exact
|
|
1656
|
+
input; omission keeps the default speech-formatting cleanup.
|
|
1617
1657
|
|
|
1618
1658
|
## Lifecycle and cancellation
|
|
1619
1659
|
|
|
@@ -372,6 +372,47 @@ observes complete diagnostics and exit. Cancelling that operation closes its
|
|
|
372
372
|
input, allowing the window and accepted Core service work to drain without a
|
|
373
373
|
forced process termination.
|
|
374
374
|
|
|
375
|
+
### Initial native window size
|
|
376
|
+
|
|
377
|
+
An application can select its initial client area and resizing behavior in
|
|
378
|
+
`arcane-app.json`, independently of its page layout:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"native": {
|
|
383
|
+
"type": "app",
|
|
384
|
+
"icon": "img/icon.png",
|
|
385
|
+
"order": 100,
|
|
386
|
+
"bundledApps": [],
|
|
387
|
+
"window": {"width": 1280, "height": 800, "resizable": true}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
This is the `native` portion of the existing app descriptor. Each `window`
|
|
393
|
+
member is optional. `width` and `height` are positive integral client dimensions
|
|
394
|
+
in logical pixels at 96 DPI; `resizable` is a boolean. Omitting a member retains
|
|
395
|
+
the host's existing choice, and omitting `window` preserves existing startup.
|
|
396
|
+
The native registry projection preserves the record; the browser package
|
|
397
|
+
projection gains no window controls. Portable and Windows assembly copy the
|
|
398
|
+
record to `arcane-native.json` as `window`, without changing page or service
|
|
399
|
+
content.
|
|
400
|
+
|
|
401
|
+
The matching Windows host applies the dimensions once on initial load, using
|
|
402
|
+
the display DPI and fitting the outer window within the selected monitor's
|
|
403
|
+
usable work area. This fit does not impose a persistent maximum size. Explicit
|
|
404
|
+
`resizable:false` uses a fixed frame and disables the maximize control; `true`
|
|
405
|
+
permits ordinary resizing and maximizing. Later app/user resizing remains
|
|
406
|
+
untouched. A composing launcher's `Load` handler runs after this initial sizing
|
|
407
|
+
and can restore its own saved window state. The SDK adds no saved-bounds store.
|
|
408
|
+
|
|
409
|
+
Direct Windows host composition uses nullable `ArcaneHostOptions` members
|
|
410
|
+
`InitialClientWidth`, `InitialClientHeight`, and `Resizable` with the same
|
|
411
|
+
semantics. Older precompiled Windows hosts do not read these options; use the
|
|
412
|
+
matching SDK host archive when rebuilding an application. The portable record
|
|
413
|
+
is available to platform adapters; this addition implements its Windows host
|
|
414
|
+
behavior and leaves window sizing on other platforms unchanged.
|
|
415
|
+
|
|
375
416
|
### Application icon and window colors
|
|
376
417
|
|
|
377
418
|
`appDescriptor.native.icon` selects the application's existing app-relative
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.
|
|
3
|
+
"sdkVersion": "0.68.0",
|
|
4
4
|
"environment": {
|
|
5
5
|
"runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
@@ -1891,7 +1891,7 @@
|
|
|
1891
1891
|
"summary": "Default binding for the canonical PreferenceStore runtime class in Node and managed browsers.",
|
|
1892
1892
|
"availability": "Node with injected adapters, or browser/native WebView storage",
|
|
1893
1893
|
"protocol": "Node ESM / authenticated managed browser import map",
|
|
1894
|
-
"normalization": "
|
|
1894
|
+
"normalization": "Canonical runtime binding. Default adapter recognizes only exact absent preferences namespace/matching method or existing Android unsupported capability; single operations retry once through existing app-scoped local storage after disposal/cancellation checks, preserving complete values and context. An advertised rejected batch is never retried serially; its original rejection remains before later local operations. Explicit adapters and partial-service/genuine failures retain their behavior."
|
|
1895
1895
|
},
|
|
1896
1896
|
{
|
|
1897
1897
|
"id": "preference-store:PREFERENCE_STORE_ERROR_CODES",
|
|
@@ -4318,7 +4318,7 @@
|
|
|
4318
4318
|
"summary": "Creates an app-neutral Core dispatcher with explicit services, request lifetimes, events and graceful drain.",
|
|
4319
4319
|
"availability": "Node on Windows, Linux and macOS native hosts; Android requires its host adaptation",
|
|
4320
4320
|
"protocol": "arcane/1 dispatcher and service lifecycle",
|
|
4321
|
-
"normalization": "Starts services independently; runtime.getService(name) and context.getService(name) return the same registered object after only its shared startup, without a renderer or global barrier. Lookup is readiness, not a lifetime lease; new lookups reject during shutdown. Preserves complete results/errors, cancels request-lifetime work and drains accepted service work; runtime.replay publishes current dispatcher/service state without restart and preserves reentrant lifecycle publication order"
|
|
4321
|
+
"normalization": "Starts services independently; runtime.getService(name) and context.getService(name) return the same registered object after only its shared startup, without a renderer or global barrier. Lookup is readiness, not a lifetime lease; new lookups reject during shutdown. Preserves complete results/errors, cancels request-lifetime work and drains accepted service work; runtime.replay publishes current dispatcher/service state without restart and preserves reentrant lifecycle publication order. Missing dispatch retains METHOD_NOT_ALLOWED with exact method/namespace and core-namespace-unavailable or core-method-unavailable; named services, registered methods and built-in namespaces establish presence. Handler/startup/permission errors remain unchanged."
|
|
4322
4322
|
},
|
|
4323
4323
|
{
|
|
4324
4324
|
"id": "core-stdio:encodeCoreFrame",
|
|
@@ -2018,8 +2018,10 @@ Shared dependencies: [`Theme.js`](runtime-entities.md#themejs).
|
|
|
2018
2018
|
Selects and refreshes system, light, dark, or custom theme mode. Native buttons
|
|
2019
2019
|
retain complete readable labels and wrap when their component runs out of room,
|
|
2020
2020
|
independently of viewport width. Each button has a `2.75rem` minimum target in
|
|
2021
|
-
both dimensions. Selected buttons retain `aria-pressed
|
|
2022
|
-
|
|
2021
|
+
both dimensions. Selected buttons retain `aria-pressed`, their selected colors
|
|
2022
|
+
and an inset outline while hovered; ordinary hover fill applies only to
|
|
2023
|
+
unselected, enabled choices. Disabled choices and keyboard focus have distinct
|
|
2024
|
+
presentation.
|
|
2023
2025
|
|
|
2024
2026
|
### Public surface
|
|
2025
2027
|
|
|
@@ -170,7 +170,7 @@ default `AI`; read-only `providerRuntime`, `speechActivationPending`, `browserSp
|
|
|
170
170
|
`streamRequest()`, `streamMessage()`, `fetchRequest()`, `fetch()`,
|
|
171
171
|
read-only `ttsSegmentation`, `configureTTSSegmentation()`,
|
|
172
172
|
`streamTTS(text='',end=false,options={})`,
|
|
173
|
-
`prepareTTS({parts,storage,identity,signal,onState})`,
|
|
173
|
+
`prepareTTS({parts,textFormat,storage,identity,signal,onState})`,
|
|
174
174
|
`playPreparedTTS(prepared,{signal,onState})`,
|
|
175
175
|
`finishTTS()`, `prepareTTSPlayback()`, `fetchTTS()`, `fetchSTT()`, `stopAudio()`, `resumeAudio()`,
|
|
176
176
|
`playAudio()`; consumes `user-entity-loaded` and `arcane-ollama-ready`,
|
|
@@ -393,14 +393,17 @@ synthesis payload to `Arcane.speech.synthesize(request, {signal})`; Core aborts
|
|
|
393
393
|
are normalized to `AbortError` with `ARCANE_AI_REQUEST_ABORTED`. Actual engine
|
|
394
394
|
interruption belongs to the registered service. It does not independently
|
|
395
395
|
choose a provider, cloud fallback, model, runtime, or
|
|
396
|
-
voice policy for the application.
|
|
397
|
-
marks from a cloned outbound input before delegation; the caller's
|
|
398
|
-
unchanged.
|
|
396
|
+
voice policy for the application. By default, calls remove repeated same
|
|
397
|
+
formatting marks from a cloned outbound input before delegation; the caller's
|
|
398
|
+
payload stays unchanged. Public payload field `textFormat:'plain'` preserves
|
|
399
|
+
the complete input, including whitespace and repeated marks. The SDK consumes
|
|
400
|
+
that field before provider/host dispatch. Omission and other values retain the
|
|
401
|
+
default cleanup. The third `preparation` argument is reserved for SDK-internal
|
|
399
402
|
delegation, where `{speechInputPrepared:true}` prevents a second cleanup pass;
|
|
400
403
|
applications omit it. `streamTTS(text='',end=false,options={})` and
|
|
401
404
|
`finishTTS()` use the selected playback boundary. The third-argument options below
|
|
402
|
-
are available in SDK `0.5.12`.
|
|
403
|
-
SDK `0.5.16`
|
|
405
|
+
are available in SDK `0.5.12`. In these streaming options, the `textFormat`
|
|
406
|
+
compatibility extra added in SDK `0.5.16` remains ignored:
|
|
404
407
|
|
|
405
408
|
| Field | Default | Meaning |
|
|
406
409
|
| --- | --- | --- |
|
|
@@ -408,7 +411,7 @@ SDK `0.5.16` is ignored beginning in `0.5.17`:
|
|
|
408
411
|
| `speed` | Current `ai.voiceSpeed` | A supplied positive speed is captured for those segments and forwarded to `fetchTTS()`. It does not change `ai.voiceSpeed`. |
|
|
409
412
|
| `pauseAfterMs` | `0` | Finite, nonnegative milliseconds placed after the final extracted segment on the existing audio clock. Invalid values throw `RangeError`; no pause is inserted between this call's other segments. |
|
|
410
413
|
| `waitForPlayback` | `false` | Omission retains the preparation promise. With `true`, the promise resolves after every extracted segment reaches a terminal playback state: `true` when all naturally end, or `false` after terminal cancellation or failure. |
|
|
411
|
-
| `textFormat` | Ignored compatibility extra |
|
|
414
|
+
| `textFormat` | Ignored streaming compatibility extra | Streaming narration retains automatic cleanup. The public plain selection belongs to complete synthesis payloads or `prepareTTS` options. |
|
|
412
415
|
|
|
413
416
|
Audio-file voice and speed use the existing `fetchTTS()` validation and error path.
|
|
414
417
|
The automatic cleanup changes only the outbound speech-input copy; displayed,
|
|
@@ -418,7 +421,7 @@ text, single marks, ordinary punctuation, and other characters remain literal.
|
|
|
418
421
|
A trailing candidate mark waits for the next character so a repeated run split
|
|
419
422
|
across chunks is still omitted. Ordinary prose streams immediately.
|
|
420
423
|
`end:true`, `finishTTS()`, muted terminal calls, and `stopAudio()` clear pending
|
|
421
|
-
formatting state. `textFormat` is not an opt-out.
|
|
424
|
+
formatting state. Streaming `textFormat` is not an opt-out.
|
|
422
425
|
|
|
423
426
|
Voice, speed, pause, and playback overrides belong to the segments
|
|
424
427
|
extracted in that invocation, including any text buffered by an earlier call.
|
|
@@ -458,12 +461,16 @@ last audio buffer ends, without waiting out that pause. Completion describes
|
|
|
458
461
|
the playback lifecycle, not proof that a listener heard the sound. See the
|
|
459
462
|
[complete-passage example](ai/browser-speech.md#queue-complete-passages-and-wait-for-playback).
|
|
460
463
|
|
|
461
|
-
`prepareTTS({parts,storage,identity,signal,onState})` returns an immediate
|
|
464
|
+
`prepareTTS({parts,textFormat,storage,identity,signal,onState})` returns an immediate
|
|
462
465
|
`{segments,state,ready,getAudio(index),cancel()}` handle for detached complete
|
|
463
466
|
speech preparation. Parts are strings or `{input,voice?,speed?,pauseAfterMs?}`
|
|
464
467
|
records. It retains the full source for semantic matching, snapshots the
|
|
465
468
|
selected speech configuration and segmentation, and applies automatic
|
|
466
|
-
formatting cleanup once to the speech copy
|
|
469
|
+
formatting cleanup once to the speech copy by default. `textFormat:'plain'`
|
|
470
|
+
preserves each complete input before the same ordered segmentation. Prepared
|
|
471
|
+
segment text participates in reuse, keeping cleaned and exact synthesis inputs
|
|
472
|
+
distinct when they differ. This SDK text-treatment choice does not change the
|
|
473
|
+
selected engine's own input capacity. Generation uses the existing
|
|
467
474
|
bounded provider queue without adding playback. Optional
|
|
468
475
|
`storage:{db,table,key}` saves complete audio files and their MIME metadata in
|
|
469
476
|
the caller's ready DBOPFS instance; the separate JSON-compatible `identity`
|
|
@@ -1032,9 +1039,12 @@ records are the closed `{llm,stt,tts}`, `{stt,tts}`, or
|
|
|
1032
1039
|
`{providers,routes,expectedProviders}` shapes described below.
|
|
1033
1040
|
`configureFromTuple()` accepts exactly six provider/model preference entries.
|
|
1034
1041
|
|
|
1035
|
-
|
|
1036
|
-
formatting marks from a cloned outbound payload's `input` or `text` field.
|
|
1037
|
-
|
|
1042
|
+
By default, direct TTS `request()` and `synthesize()` remove repeated same
|
|
1043
|
+
formatting marks from a cloned outbound payload's `input` or `text` field.
|
|
1044
|
+
Public payload field `textFormat:'plain'` preserves that input exactly. The SDK
|
|
1045
|
+
consumes this field before queuing/dispatch; queued requests retain their
|
|
1046
|
+
selected text treatment. Omission and other values keep the default cleanup.
|
|
1047
|
+
The caller's payload and request records remain unchanged. The optional
|
|
1038
1048
|
`preparation` argument is reserved for SDK-owned delegation;
|
|
1039
1049
|
`{speechInputPrepared:true}` prevents a second pass after another SDK speech
|
|
1040
1050
|
boundary has already cleaned the copy. Applications omit that argument. LLM
|
|
@@ -1906,6 +1906,20 @@ default class PreferenceStore extends EventTarget
|
|
|
1906
1906
|
subpath and managed browser key both resolve directly to the canonical runtime
|
|
1907
1907
|
module. Deep protocol: [Browser runtime delivery](protocols.md#browser-runtime-delivery).
|
|
1908
1908
|
|
|
1909
|
+
The default adapter recognizes an absent optional preferences namespace only
|
|
1910
|
+
from the exact Core lookup tuple `code:'METHOD_NOT_ALLOWED'`,
|
|
1911
|
+
`reason:'core-namespace-unavailable'`, `namespace:'preferences'`, and the
|
|
1912
|
+
matching `method:'preferences.<operation>'`. Single `get`, `set`, and `delete`
|
|
1913
|
+
operations then retry once through the existing app-scoped local adapter after
|
|
1914
|
+
checking disposal and cancellation, preserving complete values and operation
|
|
1915
|
+
context. The existing Android unsupported-capability route is unchanged.
|
|
1916
|
+
An advertised `setMany` batch rejected for that exact absence retains its
|
|
1917
|
+
original rejection and performs no serial retry; subsequent operations use the
|
|
1918
|
+
selected local adapter.
|
|
1919
|
+
Partial-service, permission, persistence, transport and cancellation failures
|
|
1920
|
+
remain errors. Explicit adapters retain their own behavior. See the
|
|
1921
|
+
[complete PreferenceStore contract](runtime-modules.md#preferencestorejs).
|
|
1922
|
+
|
|
1909
1923
|
### Example
|
|
1910
1924
|
|
|
1911
1925
|
```javascript
|
|
@@ -7325,6 +7339,15 @@ without restarting services. Its snapshot preserves complete service errors
|
|
|
7325
7339
|
and active request state; see [runtime replay](core-runtime.md#state-and-frames)
|
|
7326
7340
|
for authoritative readiness and reentrant lifecycle ordering.
|
|
7327
7341
|
|
|
7342
|
+
An unregistered method returns `METHOD_NOT_ALLOWED` with its exact `method`,
|
|
7343
|
+
first dotted `namespace`, and `reason:'core-namespace-unavailable'` when that
|
|
7344
|
+
namespace has neither a named service nor any registered method. Otherwise
|
|
7345
|
+
the reason is `core-method-unavailable`; built-in `system`, `app` and `version`
|
|
7346
|
+
namespaces count as present. This is a lookup result before handler or
|
|
7347
|
+
permission decisions. Handler/startup errors and older unmarked errors do not
|
|
7348
|
+
establish namespace absence. The dispatcher does not choose a replacement
|
|
7349
|
+
service; optional browser adapters own their documented response to absence.
|
|
7350
|
+
|
|
7328
7351
|
### Availability and normalization
|
|
7329
7352
|
|
|
7330
7353
|
**Node on Windows, Linux and macOS native hosts; Android requires its native host
|
package/package.json
CHANGED
|
@@ -6336,7 +6336,7 @@ class AI {
|
|
|
6336
6336
|
return result;
|
|
6337
6337
|
}
|
|
6338
6338
|
|
|
6339
|
-
prepareTTS({parts, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6339
|
+
prepareTTS({parts, textFormat, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6340
6340
|
if(this.#providerSpeechModel()?.speech?.playback==='native'){
|
|
6341
6341
|
const error=new Error('Native browser speech has no audio file to prepare or store; use prepareTTSPlayback.');
|
|
6342
6342
|
error.code='ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE';
|
|
@@ -6384,7 +6384,7 @@ class AI {
|
|
|
6384
6384
|
if(!is.finite(pauseAfterMs) || pauseAfterMs < 0) {
|
|
6385
6385
|
throw new RangeError('Prepared speech pauses must be nonnegative milliseconds.');
|
|
6386
6386
|
}
|
|
6387
|
-
const cleaned = stripSpeechFormatting(source.input);
|
|
6387
|
+
const cleaned = textFormat === 'plain' ? source.input : stripSpeechFormatting(source.input);
|
|
6388
6388
|
const {segments: outputs} = this.#segmentSpeechText(cleaned, true, segmentation);
|
|
6389
6389
|
for(const [index, input] of outputs.entries()) {
|
|
6390
6390
|
if(!input.trim()) continue;
|
|
@@ -7175,6 +7175,7 @@ class AI {
|
|
|
7175
7175
|
'model',
|
|
7176
7176
|
'voice',
|
|
7177
7177
|
'input',
|
|
7178
|
+
'textFormat',
|
|
7178
7179
|
'responseFormat',
|
|
7179
7180
|
'speed'
|
|
7180
7181
|
]);
|
|
@@ -7183,7 +7184,7 @@ class AI {
|
|
|
7183
7184
|
||!acceptedKeys.has(key)
|
|
7184
7185
|
||!Object.hasOwn(descriptors[key],'value')){
|
|
7185
7186
|
const error=new TypeError(
|
|
7186
|
-
'AI.fetchTTS accepts only model, voice, input, responseFormat, and speed data properties.'
|
|
7187
|
+
'AI.fetchTTS accepts only model, voice, input, textFormat, responseFormat, and speed data properties.'
|
|
7187
7188
|
);
|
|
7188
7189
|
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
7189
7190
|
throw error;
|
|
@@ -7208,7 +7209,7 @@ class AI {
|
|
|
7208
7209
|
error.code='ARCANE_AI_TTS_INPUT_INVALID';
|
|
7209
7210
|
throw error;
|
|
7210
7211
|
}
|
|
7211
|
-
const input=speechInputPrepared===true
|
|
7212
|
+
const input=speechInputPrepared===true||descriptors.textFormat?.value==='plain'
|
|
7212
7213
|
?suppliedInput
|
|
7213
7214
|
:stripSpeechFormatting(suppliedInput);
|
|
7214
7215
|
const selection=this.#providerRuntime.selection('tts');
|
|
@@ -3386,15 +3386,19 @@ export class AIProviderRuntime {
|
|
|
3386
3386
|
if (options.signal?.aborted) {
|
|
3387
3387
|
return Promise.reject(normalizedAbort());
|
|
3388
3388
|
}
|
|
3389
|
-
if (role === 'tts' && !queued
|
|
3389
|
+
if (role === 'tts' && !queued) {
|
|
3390
3390
|
const payload = options.payload;
|
|
3391
3391
|
const field = Object.hasOwn(payload ?? {}, 'input') ? 'input' : 'text';
|
|
3392
3392
|
const text = Object.getOwnPropertyDescriptor(payload ?? {}, field)?.value;
|
|
3393
3393
|
if (is.string(text)) {
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3394
|
+
const textFormat = Object.getOwnPropertyDescriptor(payload, 'textFormat')?.value;
|
|
3395
|
+
const input = speechInputPrepared === true || textFormat === 'plain'
|
|
3396
|
+
? text
|
|
3397
|
+
: stripSpeechFormatting(text);
|
|
3398
|
+
if (input !== text || Object.hasOwn(payload, 'textFormat')) {
|
|
3396
3399
|
const payloadProperties = Object.getOwnPropertyDescriptors(payload);
|
|
3397
3400
|
payloadProperties[field] = {...payloadProperties[field], value: input};
|
|
3401
|
+
delete payloadProperties.textFormat;
|
|
3398
3402
|
const requestProperties = Object.getOwnPropertyDescriptors(options);
|
|
3399
3403
|
requestProperties.payload = {
|
|
3400
3404
|
...requestProperties.payload,
|
|
@@ -132,6 +132,16 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"localAI": { "$ref": "#/$defs/localAI" },
|
|
135
|
+
"window": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"description": "Initial native client dimensions in logical pixels at 96 DPI, and whether the user can resize the window. Omitted fields retain the host defaults.",
|
|
139
|
+
"properties": {
|
|
140
|
+
"width": { "type": "integer", "minimum": 1 },
|
|
141
|
+
"height": { "type": "integer", "minimum": 1 },
|
|
142
|
+
"resizable": { "type": "boolean" }
|
|
143
|
+
}
|
|
144
|
+
},
|
|
135
145
|
"bundledApps": {
|
|
136
146
|
"type": "array",
|
|
137
147
|
"uniqueItems": true,
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -235,8 +235,26 @@ function validateDocumentCatalog(value){
|
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function validateNativeWindow(value){
|
|
239
|
+
if(value===undefined)return undefined;
|
|
240
|
+
assertOnlyKeys(value,new Set(['width','height','resizable']),'descriptor.native.window');
|
|
241
|
+
for(const dimension of ['width','height']){
|
|
242
|
+
if(value[dimension]!==undefined&&(!is.integer(value[dimension])||value[dimension]<=0)){
|
|
243
|
+
fail(`descriptor.native.window.${dimension} must be a positive integer.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if(value.resizable!==undefined&&!is.boolean(value.resizable)){
|
|
247
|
+
fail('descriptor.native.window.resizable must be a boolean.');
|
|
248
|
+
}
|
|
249
|
+
return completeValue({
|
|
250
|
+
...(value.width===undefined?{}:{width:value.width}),
|
|
251
|
+
...(value.height===undefined?{}:{height:value.height}),
|
|
252
|
+
...(value.resizable===undefined?{}:{resizable:value.resizable})
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
238
256
|
function validateNative(value,appId){
|
|
239
|
-
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services']),'descriptor.native');
|
|
257
|
+
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window']),'descriptor.native');
|
|
240
258
|
if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
|
|
241
259
|
const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
|
|
242
260
|
if(!is.integer(value.order)||value.order<0){
|
|
@@ -246,6 +264,7 @@ function validateNative(value,appId){
|
|
|
246
264
|
pattern:APP_ID_PATTERN
|
|
247
265
|
});
|
|
248
266
|
const documentCatalog=validateDocumentCatalog(value.documentCatalog);
|
|
267
|
+
const window=validateNativeWindow(value.window);
|
|
249
268
|
let services;
|
|
250
269
|
if(value.services!==undefined){
|
|
251
270
|
if(!is.array(value.services))fail('descriptor.native.services must be an array.');
|
|
@@ -259,6 +278,7 @@ function validateNative(value,appId){
|
|
|
259
278
|
icon,
|
|
260
279
|
order:value.order,
|
|
261
280
|
bundledApps,
|
|
281
|
+
...(window===undefined?{}:{window}),
|
|
262
282
|
...(services===undefined?{}:{services}),
|
|
263
283
|
...(value.localAI===undefined?{}:{localAI:normalizeLocalAIConfig(value.localAI)}),
|
|
264
284
|
...(documentCatalog?{documentCatalog}:{})
|
|
@@ -395,6 +415,7 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
395
415
|
...(value.native.bundledApps.length?{bundledApps:[...value.native.bundledApps]}:{}),
|
|
396
416
|
...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
|
|
397
417
|
...(value.native.services===undefined?{}:{services:value.native.services}),
|
|
418
|
+
...(value.native.window===undefined?{}:{window:value.native.window}),
|
|
398
419
|
...(value.native.localAI===undefined?{}:{localAI:value.native.localAI}),
|
|
399
420
|
include:[...value.package.include]
|
|
400
421
|
};
|
|
@@ -440,6 +461,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
440
461
|
order:is.integer(native.order)?native.order:100,
|
|
441
462
|
bundledApps:[...(native.bundledApps??[])],
|
|
442
463
|
...(native.services===undefined?{}:{services:native.services}),
|
|
464
|
+
...(native.window===undefined?{}:{window:native.window}),
|
|
443
465
|
...((packageManifest.localAI??native.localAI)===undefined?{}:{localAI:packageManifest.localAI??native.localAI}),
|
|
444
466
|
...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
|
|
445
467
|
},
|
|
@@ -26,6 +26,10 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
26
26
|
public string CoreExecutable { get; set; }
|
|
27
27
|
public string CoreArguments { get; set; }
|
|
28
28
|
public string CoreWorkingDirectory { get; set; }
|
|
29
|
+
/// <summary>Optional initial client dimensions in logical pixels at 96 DPI.</summary>
|
|
30
|
+
public double? InitialClientWidth { get; set; }
|
|
31
|
+
public double? InitialClientHeight { get; set; }
|
|
32
|
+
public bool? Resizable { get; set; }
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
public static class ArcaneHost
|
|
@@ -128,6 +132,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
128
132
|
{
|
|
129
133
|
if (options == null) throw new ArgumentNullException("options");
|
|
130
134
|
if (onDiagnostic == null) throw new ArgumentNullException("onDiagnostic");
|
|
135
|
+
ValidateInitialDimension(options.InitialClientWidth, "InitialClientWidth");
|
|
136
|
+
ValidateInitialDimension(options.InitialClientHeight, "InitialClientHeight");
|
|
131
137
|
this.options = options;
|
|
132
138
|
this.onDiagnostic = onDiagnostic;
|
|
133
139
|
this.onError = onError;
|
|
@@ -135,6 +141,11 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
135
141
|
completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
136
142
|
Text = options.Title;
|
|
137
143
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
144
|
+
if (options.Resizable.HasValue)
|
|
145
|
+
{
|
|
146
|
+
FormBorderStyle = options.Resizable.Value ? FormBorderStyle.Sizable : FormBorderStyle.FixedSingle;
|
|
147
|
+
MaximizeBox = options.Resizable.Value;
|
|
148
|
+
}
|
|
138
149
|
webView = new WebView2 { Dock = DockStyle.Fill };
|
|
139
150
|
Controls.Add(webView);
|
|
140
151
|
}
|
|
@@ -145,6 +156,9 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
145
156
|
|
|
146
157
|
protected override void OnLoad(EventArgs args)
|
|
147
158
|
{
|
|
159
|
+
// Apply the initial choice once. A composing launcher's Load handler
|
|
160
|
+
// can then restore its own saved state; later user sizing is untouched.
|
|
161
|
+
if (!started && !closing) ApplyInitialWindowSize();
|
|
148
162
|
base.OnLoad(args);
|
|
149
163
|
if (started || closing) return;
|
|
150
164
|
started = true;
|
|
@@ -152,6 +166,38 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
152
166
|
initialization.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
153
167
|
}
|
|
154
168
|
|
|
169
|
+
private static void ValidateInitialDimension(double? value, string name)
|
|
170
|
+
{
|
|
171
|
+
if (value.HasValue && (Double.IsNaN(value.Value) || Double.IsInfinity(value.Value)
|
|
172
|
+
|| value.Value <= 0 || Math.Truncate(value.Value) != value.Value))
|
|
173
|
+
throw new ArgumentOutOfRangeException(name, "Select a positive integral logical client dimension.");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private void ApplyInitialWindowSize()
|
|
177
|
+
{
|
|
178
|
+
if ((!options.InitialClientWidth.HasValue && !options.InitialClientHeight.HasValue)
|
|
179
|
+
|| WindowState != FormWindowState.Normal) return;
|
|
180
|
+
|
|
181
|
+
Rectangle workArea = Screen.FromControl(this).WorkingArea;
|
|
182
|
+
int availableWidth = Math.Max(1, workArea.Width - (Width - ClientSize.Width));
|
|
183
|
+
int availableHeight = Math.Max(1, workArea.Height - (Height - ClientSize.Height));
|
|
184
|
+
using (Graphics display = CreateGraphics())
|
|
185
|
+
{
|
|
186
|
+
double width = options.InitialClientWidth.HasValue
|
|
187
|
+
? options.InitialClientWidth.Value * display.DpiX / 96.0 : ClientSize.Width;
|
|
188
|
+
double height = options.InitialClientHeight.HasValue
|
|
189
|
+
? options.InitialClientHeight.Value * display.DpiY / 96.0 : ClientSize.Height;
|
|
190
|
+
// Fit before converting to native integer coordinates. This is
|
|
191
|
+
// an initial screen fit, not a persistent maximum window size.
|
|
192
|
+
ClientSize = new Size(
|
|
193
|
+
(int)Math.Max(1, Math.Min(availableWidth, Math.Round(width))),
|
|
194
|
+
(int)Math.Max(1, Math.Min(availableHeight, Math.Round(height))));
|
|
195
|
+
}
|
|
196
|
+
Location = new Point(
|
|
197
|
+
Math.Max(workArea.Left, Math.Min(Left, workArea.Right - Width)),
|
|
198
|
+
Math.Max(workArea.Top, Math.Min(Top, workArea.Bottom - Height)));
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
private async Task InitializeAsync()
|
|
156
202
|
{
|
|
157
203
|
try
|
|
@@ -48,6 +48,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
48
48
|
Dictionary<string, object> manifest = ReadRecord(Path.Combine(directory, "arcane-native.json"));
|
|
49
49
|
Dictionary<string, object> app = RequiredRecord(manifest, "app");
|
|
50
50
|
Dictionary<string, object> client = RequiredRecord(manifest, "client");
|
|
51
|
+
Dictionary<string, object> windowOptions = manifest.ContainsKey("window") ? RequiredRecord(manifest, "window") : null;
|
|
51
52
|
string appId = RequiredString(app, "id");
|
|
52
53
|
object title;
|
|
53
54
|
string name = app.TryGetValue("displayName", out title) && title is string ? (string)title : appId;
|
|
@@ -71,6 +72,9 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
71
72
|
OriginHost = "arcane.local",
|
|
72
73
|
ProfileDirectory = Path.Combine(profileParent, "WebView2"),
|
|
73
74
|
Title = name,
|
|
75
|
+
InitialClientWidth = OptionalWindowDimension(windowOptions, "width"),
|
|
76
|
+
InitialClientHeight = OptionalWindowDimension(windowOptions, "height"),
|
|
77
|
+
Resizable = OptionalWindowResizable(windowOptions),
|
|
74
78
|
IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
|
|
75
79
|
ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
|
|
76
80
|
CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
|
|
@@ -164,6 +168,23 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
164
168
|
return text;
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
private static double? OptionalWindowDimension(Dictionary<string, object> window, string name)
|
|
172
|
+
{
|
|
173
|
+
object value;
|
|
174
|
+
if (window == null || !window.TryGetValue(name, out value)) return null;
|
|
175
|
+
if (value is int || value is long || value is decimal || value is double)
|
|
176
|
+
return Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture);
|
|
177
|
+
throw new FormatException("The native manifest needs a number at window." + name + ".");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private static bool? OptionalWindowResizable(Dictionary<string, object> window)
|
|
181
|
+
{
|
|
182
|
+
object value;
|
|
183
|
+
if (window == null || !window.TryGetValue("resizable", out value)) return null;
|
|
184
|
+
if (value is bool) return (bool)value;
|
|
185
|
+
throw new FormatException("The native manifest needs a boolean at window.resizable.");
|
|
186
|
+
}
|
|
187
|
+
|
|
167
188
|
private static string QuoteArgument(string value)
|
|
168
189
|
{
|
|
169
190
|
// Windows command-line escaping is transport-local. The child
|
|
@@ -196,6 +196,7 @@ export function createPortableNativeProvider({services, packagedWeb = false, web
|
|
|
196
196
|
target,
|
|
197
197
|
webRoot: 'app',
|
|
198
198
|
start: release.manifest?.app?.start ?? './index.html',
|
|
199
|
+
...(appDescriptor.native?.window === undefined ? {} : {window: {...appDescriptor.native.window}}),
|
|
199
200
|
core: {
|
|
200
201
|
entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
|
|
201
202
|
...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
|