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 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.68.0` SDK contract. Development applications declare
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
- Every TTS call automatically removes repeated same formatting marks (`*`, `#`,
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. No application option is needed;
476
- an existing `textFormat` extra is ignored and cannot disable cleanup. Original
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 ? suppliedText : stripSpeechFormatting(suppliedText);
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;
@@ -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; automatic Markdown cleanup affects only the speech
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 automatically:
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. An
998
- existing `textFormat` extra is ignored and cannot disable or select cleanup.
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 only after one cleanup pass, preventing a second non-idempotent pass.
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.67.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": "Binding identity and namespace come directly from the canonical runtime module"
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` and an inset outline;
2022
- disabled choices and keyboard focus have distinct presentation.
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. Every call removes repeated same formatting
397
- marks from a cloned outbound input before delegation; the caller's payload stays
398
- unchanged. The third `preparation` argument is reserved for SDK-internal
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`. The `textFormat` compatibility extra added in
403
- SDK `0.5.16` is ignored beginning in `0.5.17`:
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 | Repeated same formatting marks are removed automatically from every TTS call. This value no longer selects or disables cleanup. |
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. Generation uses the existing
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
- Every direct TTS `request()` or `synthesize()` call removes repeated same
1036
- formatting marks from a cloned outbound payload's `input` or `text` field. The
1037
- caller's payload and request records remain unchanged. The optional
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.68.0",
3
+ "version": "0.69.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",
@@ -38,7 +38,7 @@
38
38
  overflow-wrap:anywhere;
39
39
  }
40
40
 
41
- button:hover:not(:disabled) {
41
+ button:hover:not(:disabled):not([aria-pressed="true"]) {
42
42
  background:color-mix(in srgb,currentColor 14%,transparent);
43
43
  }
44
44
 
@@ -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 && speechInputPrepared !== true) {
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 input = stripSpeechFormatting(text);
3395
- if (input !== text) {
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,
@@ -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})