arcane-os 0.2.2 → 0.2.3

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.
@@ -61,7 +61,7 @@ A component file does not register its own custom element. The `<html-import>` h
61
61
  | [`theme-editor.html`](#theme-editorhtml) | Edits, previews, saves, and resets semantic custom theme tokens. | `configure()`<br>`getTheme()`<br>`setTheme()`<br>`setBusy()`<br>`setStatus()` | `theme-editor-ready`<br>`theme-preview`<br>`theme-save`<br>`theme-reset` | Fully normalized Theme values |
62
62
  | [`theme-switcher.html`](#theme-switcherhtml) | Selects and refreshes system, light, dark, or custom theme mode. | `setMode()`<br>`refresh()` | No component-specific event | Preference/native appearance behavior mixed; no component-ready contract |
63
63
  | [`unified-inbox.html`](#unified-inboxhtml) | Displays provider-neutral communication threads with active/loading state. | `configure()`<br>`setThreads()`<br>`setActive()`<br>`setLoading()` | `unified-inbox-ready`<br>`inbox-refresh`<br>`thread-select` | DOM-normalized |
64
- | [`voice-transcription.html`](#voice-transcriptionhtml) | Records segmented microphone audio, transcribes, persists, and completes a combined transcript. | `configure()`<br>`startRecording()`<br>`stopRecording()`<br>`save()`<br>`completeTranscription()/complete()`<br>`clear()`<br>`reset()`<br>`destroy()` | `voice-transcription-ready`<br>`voice-transcription-state`<br>`voice-transcription-segment`<br>`voice-transcription-change`<br>`voice-transcription-complete`<br>`speech-transcription-complete` | State/text normalized; media/provider behavior mixed |
64
+ | [`voice-transcription.html`](#voice-transcriptionhtml) | Records segmented microphone audio only after authoritative STT admission, exposes explicit selected-STT activation, transcribes with cancellation, persists, and completes a combined transcript. | `configure()`<br>`requestSTTActivation()`<br>`startRecording()`<br>`stopRecording()`<br>`save()`<br>`completeTranscription()/complete()`<br>`clear()`<br>`reset()`<br>`destroy()` | `voice-transcription-ready`<br>`voice-transcription-state`<br>`voice-transcription-segment`<br>`voice-transcription-change`<br>`voice-transcription-complete`<br>`speech-transcription-complete`<br>`speech-transcription-cancelled`<br>`speech-stt-activation-request`<br>`speech-stt-activation-error` | Sticky runtime STT readiness, explicit activation, request cancellation, and state/text are normalized; media/provider behavior remains external |
65
65
  | [`weather-widget.html`](#weather-widgethtml) | Displays normalized current and daily weather with refresh intent. | `setWeather()`<br>`clear()` | `weather-widget-ready`<br>`weather-refresh` | Display normalized; provider supplied externally |
66
66
  | [`web-navigator.html`](#web-navigatorhtml) | Guards embedded/external navigation and surfaces allow/block/open intents. | `configure()`<br>`navigate()`<br>`currentUrl()` | `web-navigator-ready`<br>`web-navigate`<br>`web-navigation-blocked`<br>`web-open-external` | Navigation intent/decision normalized; browser navigation result platform-native |
67
67
 
@@ -866,7 +866,8 @@ Events: `speech-ready`, `speech-transcription-complete`,
866
866
  `speech-stt-activation-error`, and `speech-tts-lifecycle-error`.
867
867
 
868
868
  Shared dependencies: [`AI.js`](runtime-modules.md#aijs),
869
- [`AIRuntimeState.js`](runtime-modules.md#airuntimestatejs), and
869
+ [`AIRuntimeState.js`](runtime-modules.md#airuntimestatejs),
870
+ [`ComponentContracts.js`](runtime-modules.md#componentcontractsjs), and
870
871
  [`DBLS.js`](runtime-modules.md#dblsjs).
871
872
 
872
873
  The Hold to talk control remains disabled unless sticky STT state is `ready`,
@@ -1113,19 +1114,81 @@ Events: `unified-inbox-ready`, `inbox-refresh`, `thread-select`.
1113
1114
 
1114
1115
  ### Overview
1115
1116
 
1116
- Records segmented microphone audio, transcribes, persists, and completes a combined transcript.
1117
+ Records segmented microphone audio only after authoritative STT admission,
1118
+ exposes explicit selected-STT activation, transcribes with cancellation,
1119
+ persists, and completes a combined transcript.
1117
1120
 
1118
1121
  ### Public surface
1119
1122
 
1120
- Methods/properties: `configure()`, `startRecording()`, `stopRecording()`, `save()`, `completeTranscription()/complete()`, `clear()`, `reset()`, `destroy()`.
1123
+ Methods/properties: `configure()`, `requestSTTActivation()`, `startRecording()`,
1124
+ `stopRecording()`, `save()`, `completeTranscription()/complete()`, `clear()`,
1125
+ `reset()`, `destroy()`.
1121
1126
 
1122
- Events: `voice-transcription-ready`, `voice-transcription-state`, `voice-transcription-segment`, `voice-transcription-change`, `voice-transcription-complete`, `speech-transcription-complete`.
1127
+ Events: `voice-transcription-ready`, `voice-transcription-state`,
1128
+ `voice-transcription-segment`, `voice-transcription-change`,
1129
+ `voice-transcription-complete`, `speech-transcription-complete`,
1130
+ `speech-transcription-cancelled`, `speech-stt-activation-request`, and
1131
+ `speech-stt-activation-error`.
1123
1132
 
1124
- Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`ComponentContracts.js`](runtime-modules.md#componentcontractsjs), [`AI.js`](runtime-modules.md#aijs).
1133
+ Shared dependencies: [`MD.js`](runtime-modules.md#mdjs),
1134
+ [`ComponentContracts.js`](runtime-modules.md#componentcontractsjs),
1135
+ [`AIRuntimeState.js`](runtime-modules.md#airuntimestatejs), and
1136
+ [`AI.js`](runtime-modules.md#aijs).
1125
1137
 
1126
1138
  ### Availability and normalization
1127
1139
 
1128
- **Browser and supported native WebViews.** State/text normalized; media/provider behavior mixed. HTMLImport + DOM; injected Arcane/provider modules where listed. Native methods remain subject to the bound app's capabilities. [Deep protocol details](protocols.md).
1140
+ **Browser and supported native WebViews.** The component subscribes
1141
+ synchronously to sticky `AIRuntimeState.roles.stt`; its recording Start button
1142
+ and public `startRecording()` both fail closed unless that role is exactly
1143
+ `ready` and not busy. A configured `transcribe(file,context)` callback remains
1144
+ request plumbing rather than readiness authority. Its context now includes the
1145
+ owned `signal` additively. The default route calls
1146
+ `AI.fetchSTT(file,undefined,signal)` so the callback position remains valid and
1147
+ readiness loss or destruction can abort delivery.
1148
+
1149
+ For a selected `unloaded`, `loading`, `unloading`, or `error` role, the component
1150
+ keeps recording Start disabled and presents the same keyboard-operable Start
1151
+ transcription/Try again or Cancel loading control as `speech.html`. Both use the
1152
+ shared `createSTTActivationController()` contract. User operation emits the
1153
+ cancelable `speech-stt-activation-request` event with frozen `{intent,state}`;
1154
+ `preventDefault()` suppresses the callback. The default
1155
+ `requestSTTActivation(intent)` publishes the frozen
1156
+ `{role:'stt',action:'load'|'unload',reason:'user'}` intent. Callback failure emits
1157
+ `speech-stt-activation-error` with frozen `{request,error,message}`. Import and
1158
+ state observation emit no activation request and never begin a model download.
1159
+
1160
+ If sticky readiness is lost during microphone acquisition, capture, or the STT
1161
+ request, the component invalidates the session, aborts the owned request signal,
1162
+ releases media, discards late completion, and emits
1163
+ `speech-transcription-cancelled` with frozen `{reason:'runtime-unready'}`. A
1164
+ current provider cancellation returns the component workflow to `idle` and
1165
+ emits the same event with `reason:'stt-provider-request-cancelled'`; destroying
1166
+ an active capture or transcription emits `reason:'component-destroyed'`.
1167
+ Stale teardown results remain suppressed. A
1168
+ current save failure, including `AbortError`, enters the visible `error` state.
1169
+ Readiness loss after transcription has finished does not invalidate an already
1170
+ pending application save or completion callback: its settlement remains
1171
+ observable, while any new recording remains gated by current sticky readiness.
1172
+ Assigning `value` or calling `configure({initialValue})` explicitly supersedes
1173
+ an in-flight microphone start, STT request, save, or completion. The component
1174
+ advances its generation, aborts owned STT delivery, releases owned media, emits
1175
+ `speech-transcription-cancelled` with
1176
+ `reason:'transcript-replaced'`, and suppresses
1177
+ late settlement before publishing the assigned transcript. Assigning transcript
1178
+ text during active recording preserves that recording and changes the transcript
1179
+ to which the captured segment will be appended.
1180
+ `destroy()` aborts the state subscription, cancels active work, removes the
1181
+ activation listener, sets component `ready` to `false`, and returns `true`;
1182
+ repeated destruction is
1183
+ idempotent. Destruction is terminal: Start, activation, and Complete remain
1184
+ disabled and status remains unavailable. `voice-transcription-state` detail is
1185
+ the frozen `{message,state,stt}` record, where `state` remains the component
1186
+ workflow and `stt` is the authoritative immutable role record. Transcript
1187
+ completion stays available when STT is unavailable before destruction.
1188
+ State/text, explicit activation, and request cancellation are normalized;
1189
+ provider/model authority and media behavior remain external. HTMLImport + DOM;
1190
+ injected Arcane/provider modules where listed.
1191
+ Native methods remain subject to the bound app's capabilities. [Deep protocol details](protocols.md).
1129
1192
 
1130
1193
  ### Example
1131
1194
 
@@ -41,7 +41,7 @@ Apps import renderer ESM from `/arcane/modules/<file>`. Classic scripts, the OPF
41
41
  | [`CommunicationHub.js`](#communicationhubjs) | esm | Fans out provider refresh/send operations and aggregates normalized threads/messages. | Cross-host with injected providers | Normalized aggregates; refresh contains per-provider failures. |
42
42
  | [`CommunicationPreferences.js`](#communicationpreferencesjs) | esm | Stores app-scoped, non-secret communication provider preferences. | Browser / native WebView hybrid | Normalized preference record; storage failures mixed. |
43
43
  | [`CommunicationProviderRegistry.js`](#communicationproviderregistryjs) | esm | Registers and queries validated provider definitions, channels, and required methods. | Cross-host | Strict normalized registry. |
44
- | [`ComponentContracts.js`](#componentcontractsjs) | esm | Owns normalized configuration/value contracts shared by chart, dashboard, Markdown, and voice components. | Cross-host | Fully normalized labels, rows, definitions, visibility, formats, editor and voice options. |
44
+ | [`ComponentContracts.js`](#componentcontractsjs) | esm | Owns normalized configuration/value contracts and shared explicit STT activation behavior for chart, dashboard, Markdown, and voice components. | Cross-host | Fully normalized labels, rows, definitions, visibility, formats, editor and voice options, plus capability-neutral STT activation intent and presentation state. |
45
45
  | [`ConfiguredAIChatSession.js`](#configuredaichatsessionjs) | esm | Owns bounded in-memory AI turns, context construction, response-length instruction, and atomic history commit. | Native bridge by default; cross-host with injected chat | Normalized session/result; provider rejection preserved. |
46
46
  | [`ConversationActionItems.js`](#conversationactionitemsjs) | esm | Normalizes, creates, updates, remembers, selects, and formats bounded conversation action items. | Cross-host | Fully normalized status/base/presentation contract. |
47
47
  | [`ConversationClosingReport.js`](#conversationclosingreportjs) | esm | Defines the closing-report tool, instruction, result normalizer, call classifier, and formatter. | Cross-host | Fully normalized report contract. |
@@ -259,11 +259,10 @@ request ownership is active.
259
259
 
260
260
  ### Availability and normalization
261
261
 
262
- **Cross-host runtime with provider-specific execution.** Published SDK `0.2.1`
263
- ships the browser-WASM LLM and browser Whisper/Kokoro adapters but predates the
264
- legacy speech adapters described above. Current source also supplies the narrow
265
- AI.js legacy OpenAI/Ollama/Core-speech adapters; other native, Core, or cloud
266
- adapters may be supplied externally only when they implement the same
262
+ **Cross-host runtime with provider-specific execution.** Published SDK `0.2.2`
263
+ ships the browser-WASM LLM and browser Whisper/Kokoro adapters and supplies the
264
+ narrow AI.js legacy OpenAI/Ollama/Core-speech adapters; other native, Core, or
265
+ cloud adapters may be supplied externally only when they implement the same
267
266
  `arcane-ai-provider/2` boundary. A
268
267
  provider must prove a matching `arcane-ai-model-authority/1` inspection before load.
269
268
  `localOnly` routing fails closed; it never selects a cloud or non-local route as
@@ -792,17 +791,40 @@ console.log(Object.keys(module));
792
791
 
793
792
  ### Overview
794
793
 
795
- Owns normalized configuration/value contracts shared by chart, dashboard, Markdown, and voice components.
794
+ Owns normalized configuration/value contracts and shared explicit STT activation
795
+ behavior for chart, dashboard, Markdown, and voice components.
796
796
 
797
797
  ### Public surface
798
798
 
799
- Six constant sets and twelve normalization/formatting helpers.
799
+ Constant sets plus normalization, formatting, and explicit STT activation
800
+ helpers. `createSTTActivationController({host,button,onChange,EventClass=CustomEvent})`
801
+ consumes only normalized
802
+ [`AIRuntimeState`](#airuntimestatejs) `stt` role records. Its frozen controller
803
+ exposes `action`, `error`, `label`, `pending`, `selected`, `status`, `title`, and
804
+ `visible` getters plus `request(action)`, `synchronize(role)`, and `destroy()`.
805
+ `host` supplies `dispatchEvent(event)` and `requestSTTActivation(intent)`;
806
+ `button` supplies `addEventListener()` and `removeEventListener()`; and
807
+ `onChange()` is called whenever presentation should be rendered again. Browser
808
+ callers use the default `CustomEvent`; non-DOM callers must inject a compatible
809
+ `EventClass` constructor.
800
810
 
801
- Exact exports: `CHART_LABELS`, `DASHBOARD_LABELS`, `MARKDOWN_FORMATS`, `MARKDOWN_LABELS`, `VOICE_LABELS`, `VOICE_MESSAGES`, `appendTranscription`, `applyMarkdownFormat`, `effectiveDashboardVisibility`, `normalizeChartOptions`, `normalizeChartRows`, `normalizeDashboardDefinitions`, `normalizeDashboardOptions`, `normalizeDashboardVisibility`, `normalizeMarkdownFormats`, `normalizeMarkdownOptions`, `normalizeVoiceOptions`.
811
+ `request('load'|'unload')` emits the cancelable
812
+ `speech-stt-activation-request` event with frozen `{intent,state}` before it
813
+ invokes `host.requestSTTActivation(intent)`. Callback failure emits
814
+ `speech-stt-activation-error` with frozen `{request,error,message}`. Syncing
815
+ sticky state only changes the controller's observation and presentation; it
816
+ never emits a lifecycle intent, chooses a provider, or starts a download.
817
+ `destroy()` removes its button listener and suppresses late callback effects.
818
+
819
+ Exact exports: `CHART_LABELS`, `DASHBOARD_LABELS`, `MARKDOWN_FORMATS`, `MARKDOWN_LABELS`, `VOICE_LABELS`, `VOICE_MESSAGES`, `appendTranscription`, `applyMarkdownFormat`, `createSTTActivationController`, `effectiveDashboardVisibility`, `normalizeChartOptions`, `normalizeChartRows`, `normalizeDashboardDefinitions`, `normalizeDashboardOptions`, `normalizeDashboardVisibility`, `normalizeMarkdownFormats`, `normalizeMarkdownOptions`, `normalizeVoiceOptions`.
802
820
 
803
821
  ### Availability and normalization
804
822
 
805
- **Cross-host.** Fully normalized labels, rows, definitions, visibility, formats, editor and voice options. Transport: In-process only. [Deep protocol details](protocols.md).
823
+ **Cross-host with an injected event constructor outside DOM hosts.** Fully
824
+ normalized labels, rows, definitions, visibility, formats, editor and voice
825
+ options, plus capability-neutral STT activation intent and presentation state.
826
+ Provider authority and lifecycle execution remain with the configured runtime
827
+ owner. Transport: In-process only. [Deep protocol details](protocols.md).
806
828
 
807
829
  ### Example
808
830
 
@@ -1083,7 +1105,7 @@ source body as implicit authority, and never persists a caller-owned body.
1083
1105
  **Browser or compatible host with an injected DBOPFS adapter.** The adapter
1084
1106
  keeps the existing `get`, `set`, `getAllKeys`, and `delete` method names; Node
1085
1107
  can use the same class only through an explicitly imported runtime module and a
1086
- compatible storage adapter; SDK `0.2.1` publishes no Node package subpath or
1108
+ compatible storage adapter; SDK `0.2.2` publishes no Node package subpath or
1087
1109
  Node storage implementation for it. Bootstrap uses a bounded concurrent
1088
1110
  generation, commits its manifest last, cleans partial data on failure, and
1089
1111
  rejects case-colliding IDs. Search
@@ -17,7 +17,7 @@ This table is the Node `package.json#exports` map: it defines package
17
17
  entrypoints for SDK/tooling code. It is distinct from the generated browser
18
18
  import map that resolves application-facing `arcane/*` modules and the focused
19
19
  EventManager entry. See [browser runtime delivery](protocols.md#browser-runtime-delivery)
20
- for that 91-entry physical-runtime contract in SDK `0.2.1`.
20
+ for that 91-entry physical-runtime contract in SDK `0.2.2`.
21
21
 
22
22
  | Specifier | Purpose |
23
23
  | --- | --- |
@@ -47,7 +47,7 @@ Protocol mechanics are intentionally kept in the [deep protocol guide](protocols
47
47
 
48
48
  ## Canonical member inventory
49
49
 
50
- SDK `0.2.1` exports exactly 169 distinct JavaScript members across 13
50
+ SDK `0.2.2` exports exactly 169 distinct JavaScript members across 13
51
51
  JavaScript entrypoints. The complete export map has 23 subpaths: the other 10
52
52
  are the runtime manifest, eight JSON Schemas, and package metadata. Runtime
53
53
  projection modules in the managed browser map are cataloged separately in
@@ -814,7 +814,7 @@ same deterministic map. The package root also contains the public
814
814
  {
815
815
  schemaVersion: 1,
816
816
  kind: 'arcane-app-runtime-projection',
817
- sdkVersion: '0.2.1',
817
+ sdkVersion: '0.2.2',
818
818
  pathPrefix: 'arcane/',
819
819
  fileCount,
820
820
  totalBytes,
@@ -2698,7 +2698,7 @@ The import-map operation also reports the stable operation-specific strings
2698
2698
  `ARCANE_IMPORT_MAP_COLLISION`; package assembly can additionally report
2699
2699
  `ARCANE_IMPORT_MAP_CLEANUP_FAILED`. They are normalized `ArcaneError.code`
2700
2700
  values, but are not properties added to this frozen general registry in SDK
2701
- `0.2.1`.
2701
+ `0.2.2`.
2702
2702
 
2703
2703
  ### Value and import
2704
2704
 
@@ -3364,7 +3364,7 @@ workspace it additionally returns the exact installed package authority:
3364
3364
  packageSource,
3365
3365
  canonicalPackageRoot,
3366
3366
  packageName: 'arcane-os',
3367
- packageVersion: '0.2.1',
3367
+ packageVersion: '0.2.2',
3368
3368
  runtimeRoot,
3369
3369
  browserRuntimeRoot,
3370
3370
  runtimeManifest,
@@ -3374,9 +3374,9 @@ workspace it additionally returns the exact installed package authority:
3374
3374
  ```
3375
3375
 
3376
3376
  The dependency can be named `arcane-os` or be one exact npm alias for
3377
- `npm:arcane-os@0.2.1`. The selected installation must still be one direct,
3377
+ `npm:arcane-os@0.2.2`. The selected installation must still be one direct,
3378
3378
  physical, non-link package directory whose manifest identifies exactly as
3379
- `arcane-os@0.2.1`; duplicate canonical/alias declarations fail closed.
3379
+ `arcane-os@0.2.2`; duplicate canonical/alias declarations fail closed.
3380
3380
  `allowMissingManagedImportMap` is an internal packaging/development seam. An
3381
3381
  ordinary caller should leave it `false`.
3382
3382
 
@@ -3553,7 +3553,7 @@ const toolchain = createToolchain({
3553
3553
 
3554
3554
  // Only this explicit call refreshes the managed map and configured HTML entry.
3555
3555
  const result = await toolchain.importMap();
3556
- console.log(result.importMap.entryCount); // 91 in SDK 0.2.1
3556
+ console.log(result.importMap.entryCount); // 91 in SDK 0.2.2
3557
3557
  console.log(result.importMap.documentPaths, result.importMap.documentCount);
3558
3558
  ```
3559
3559
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "builder": "arcane-sdk-runtime-v1",
4
- "sdkVersion": "0.2.2",
4
+ "sdkVersion": "0.2.3",
5
5
  "source": {
6
6
  "authority": "sdk-canonical",
7
7
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
@@ -14,8 +14,8 @@
14
14
  }
15
15
  },
16
16
  "fileCount": 160,
17
- "totalBytes": 3643083,
18
- "contentSha256": "0b09b1fd548152416f583d5e117197c7886b5a9e63071e43493577b4090e4a8c",
17
+ "totalBytes": 3655536,
18
+ "contentSha256": "f96a27bd79a809201708f73ca729bc60ab02696daf619365f77267a28b47b4b3",
19
19
  "files": [
20
20
  {
21
21
  "path": "arcane/components/app-bar.html",
@@ -159,8 +159,8 @@
159
159
  },
160
160
  {
161
161
  "path": "arcane/components/speech.html",
162
- "bytes": 49596,
163
- "sha256": "2a8b2929d28699ab9011f358b09b5fd2d53a0703542b2fee5cda8334261c6c86"
162
+ "bytes": 41300,
163
+ "sha256": "0ed5dd29c37747702f114114ad66bedd419fd39bed35fc12362c63c6357a41d9"
164
164
  },
165
165
  {
166
166
  "path": "arcane/components/summary-strip.html",
@@ -199,8 +199,8 @@
199
199
  },
200
200
  {
201
201
  "path": "arcane/components/voice-transcription.html",
202
- "bytes": 14471,
203
- "sha256": "7fab0ca767c252f661f7a3a5e09cf7325692b64a1d03e5731dc5d4f83c80c124"
202
+ "bytes": 26991,
203
+ "sha256": "8140b918791df5043be9254d52bf7864fe91759ab6e5e60af2364d6b4a8fcb7b"
204
204
  },
205
205
  {
206
206
  "path": "arcane/components/weather-widget.html",
@@ -494,8 +494,8 @@
494
494
  },
495
495
  {
496
496
  "path": "arcane/modules/ComponentContracts.js",
497
- "bytes": 20507,
498
- "sha256": "4115d4ce8081e1dbaeb0a1a2fa3201ecddabf05a471f76e4deb7a071f9c31d20"
497
+ "bytes": 28736,
498
+ "sha256": "f5ba011094835b45258ee833a90bfa17980e860ef9ceac88aa56ecad3abe9d53"
499
499
  },
500
500
  {
501
501
  "path": "arcane/modules/ConfiguredAIChatSession.js",
@@ -156,6 +156,9 @@
156
156
  requestAIRuntimeIntent,
157
157
  subscribeAIRuntimeState
158
158
  } = await import('../modules/AIRuntimeState.js');
159
+ const {
160
+ createSTTActivationController
161
+ } = await import('../modules/ComponentContracts.js');
159
162
 
160
163
  const host = this;
161
164
  const shadowRoot = host.shadowRoot;
@@ -254,191 +257,6 @@
254
257
  )
255
258
  );
256
259
 
257
- function createSTTActivationController({
258
- host,
259
- button,
260
- onChange,
261
- EventClass = CustomEvent
262
- }) {
263
- let role = null;
264
- let requestPending = false;
265
- let requestError = '';
266
- let requestGeneration = 0;
267
- let destroyed = false;
268
-
269
- function visibleError(error, fallback) {
270
- const message = typeof error?.message === 'string'
271
- ? error.message.trim()
272
- : '';
273
- return (message || fallback).slice(0, 240);
274
- }
275
-
276
- function cancellation(error) {
277
- return error?.name === 'AbortError'
278
- || [
279
- 'ARCANE_AI_REQUEST_ABORTED',
280
- 'ARCANE_AI_OPERATION_SUPERSEDED'
281
- ]
282
- .includes(error?.code);
283
- }
284
-
285
- function selected() {
286
- return typeof role?.providerId === 'string'
287
- && role.providerId.length > 0
288
- && typeof role?.modelId === 'string'
289
- && role.modelId.length > 0;
290
- }
291
-
292
- function action() {
293
- if (!selected()) {
294
- return null;
295
- }
296
- if (role.state === 'loading') {
297
- return 'unload';
298
- }
299
- if (!role.busy && ['unloaded', 'error'].includes(role.state)) {
300
- return 'load';
301
- }
302
- return null;
303
- }
304
-
305
- async function request(nextAction) {
306
- if (destroyed || requestPending || action() !== nextAction) {
307
- return false;
308
- }
309
- const generation = ++requestGeneration;
310
- requestError = '';
311
- const intent = Object.freeze(
312
- {
313
- role: 'stt',
314
- action: nextAction,
315
- reason: 'user'
316
- }
317
- );
318
- const activationRequest = Object.freeze({intent, state: role});
319
- const activationEvent = new EventClass(
320
- 'speech-stt-activation-request',
321
- {
322
- bubbles: true,
323
- composed: true,
324
- cancelable: true,
325
- detail: activationRequest
326
- }
327
- );
328
- requestPending = true;
329
- const accepted = host.dispatchEvent(activationEvent);
330
- if (!accepted
331
- || destroyed
332
- || generation !== requestGeneration
333
- || action() !== nextAction) {
334
- if (!destroyed && generation === requestGeneration) {
335
- requestPending = false;
336
- onChange();
337
- }
338
- return false;
339
- }
340
- onChange();
341
- try {
342
- await host.requestSTTActivation(intent);
343
- if (destroyed || generation !== requestGeneration) {
344
- return false;
345
- }
346
- return true;
347
- } catch (error) {
348
- if (destroyed || generation !== requestGeneration) {
349
- return false;
350
- }
351
- if (nextAction === 'load'
352
- && cancellation(error)
353
- && ['unloaded', 'unloading'].includes(role?.state)) {
354
- return false;
355
- }
356
- const message = visibleError(
357
- error,
358
- `The transcription ${nextAction} request failed.`
359
- );
360
- requestError = message;
361
- host.dispatchEvent(
362
- new EventClass(
363
- 'speech-stt-activation-error',
364
- {
365
- bubbles: true,
366
- composed: true,
367
- detail: Object.freeze(
368
- {
369
- request: activationRequest,
370
- error,
371
- message
372
- }
373
- )
374
- }
375
- )
376
- );
377
- return false;
378
- } finally {
379
- if (!destroyed && generation === requestGeneration) {
380
- requestPending = false;
381
- onChange();
382
- }
383
- }
384
- }
385
-
386
- function activateSelectedSTT() {
387
- const nextAction = action();
388
- if (nextAction) {
389
- void request(nextAction);
390
- }
391
- }
392
-
393
- function synchronize(nextRole) {
394
- if (destroyed) {
395
- return;
396
- }
397
- if (role?.state !== nextRole.state
398
- || role?.providerId !== nextRole.providerId
399
- || role?.modelId !== nextRole.modelId
400
- || role?.loaded !== nextRole.loaded
401
- || role?.busy !== nextRole.busy
402
- || role?.operationId !== nextRole.operationId) {
403
- requestGeneration += 1;
404
- requestError = '';
405
- requestPending = false;
406
- }
407
- role = nextRole;
408
- }
409
-
410
- function destroy() {
411
- if (destroyed) {
412
- return;
413
- }
414
- destroyed = true;
415
- requestGeneration += 1;
416
- requestPending = false;
417
- button.removeEventListener('click', activateSelectedSTT);
418
- }
419
-
420
- button.addEventListener('click', activateSelectedSTT);
421
- return Object.freeze(
422
- {
423
- get action() {
424
- return action();
425
- },
426
- get error() {
427
- return requestError;
428
- },
429
- get pending() {
430
- return requestPending;
431
- },
432
- get selected() {
433
- return selected();
434
- },
435
- request,
436
- synchronize,
437
- destroy
438
- }
439
- );
440
- }
441
-
442
260
  function unavailableRole(role) {
443
261
  return Object.freeze(
444
262
  {
@@ -598,10 +416,7 @@
598
416
  function renderControls() {
599
417
  const transcriptionReady = sttRole.state === 'ready';
600
418
  const microphoneReady = host.availability.microphone;
601
- const activationVisible = sttActivationController.selected
602
- && sttRole.state !== 'ready'
603
- && ['unloaded', 'loading', 'unloading', 'error']
604
- .includes(sttRole.state);
419
+ const activationVisible = sttActivationController.visible;
605
420
  recordButton.hidden = activationVisible;
606
421
  recordButton.disabled = !transcriptionReady
607
422
  || !microphoneReady
@@ -620,8 +435,8 @@
620
435
  || sttActivationController.pending
621
436
  || sttRole.state === 'unloading'
622
437
  || sttActivationController.action === null;
623
- sttActivationButton.textContent = transcriptionActivationLabel();
624
- sttActivationButton.title = transcriptionActivationTitle();
438
+ sttActivationButton.textContent = sttActivationController.label;
439
+ sttActivationButton.title = sttActivationController.title;
625
440
  sttActivationButton.setAttribute(
626
441
  'aria-label',
627
442
  sttActivationButton.title
@@ -636,41 +451,6 @@
636
451
  renderTTSControl();
637
452
  }
638
453
 
639
- function transcriptionActivationLabel() {
640
- if (sttActivationController.pending) {
641
- return 'Requesting…';
642
- }
643
- if (sttRole.state === 'loading') {
644
- return 'Cancel loading';
645
- }
646
- if (sttRole.state === 'unloading') {
647
- return 'Canceling…';
648
- }
649
- if (sttRole.state === 'error') {
650
- return 'Try again';
651
- }
652
- return 'Start transcription';
653
- }
654
-
655
- function transcriptionActivationTitle() {
656
- if (sttActivationController.pending) {
657
- return 'Requesting transcription activation.';
658
- }
659
- if (sttRole.state === 'loading') {
660
- return 'Cancel loading the selected transcription service.';
661
- }
662
- if (sttRole.state === 'unloading') {
663
- return 'The selected transcription service is being released.';
664
- }
665
- if (sttRole.state === 'error') {
666
- return sttActivationController.error || visibleErrorMessage(
667
- sttRole.error,
668
- 'Retry loading the selected transcription service.'
669
- );
670
- }
671
- return 'Start the selected transcription service.';
672
- }
673
-
674
454
  function transcriptionButtonTitle() {
675
455
  if (destroyed || sttRole.state === 'disposed') {
676
456
  return 'Transcription is unavailable.';
@@ -776,34 +556,10 @@
776
556
  }
777
557
 
778
558
  function describeSTTRole() {
779
- if (sttActivationController.error) {
780
- return `Transcription activation error: ${sttActivationController.error}.`;
781
- }
782
- if (sttActivationController.pending) {
783
- return 'Transcription activation requested.';
784
- }
785
- if (sttRole.state === 'ready') {
786
- if (!host.availability.microphone) {
787
- return 'Microphone unavailable.';
788
- }
789
- return sttRole.busy ? 'Transcription busy.' : 'Transcription ready.';
790
- }
791
- if (sttRole.state === 'loading') {
792
- return `Transcription ${formatRoleProgress(sttRole.progress, 'loading')}; Cancel is available.`;
793
- }
794
- if (sttRole.state === 'unloading') {
795
- return `Transcription ${formatRoleProgress(sttRole.progress, 'releasing')}.`;
796
- }
797
- if (sttRole.state === 'error') {
798
- return `Transcription error: ${visibleErrorMessage(sttRole.error, 'Unknown error')}.`;
799
- }
800
- if (sttRole.state === 'disposed') {
801
- return 'Transcription disposed.';
802
- }
803
- if (sttRole.state === 'unloaded' && sttActivationController.selected) {
804
- return 'Transcription selected and waiting to load.';
559
+ if (sttRole.state === 'ready' && !host.availability.microphone) {
560
+ return 'Microphone unavailable.';
805
561
  }
806
- return 'Transcription unavailable.';
562
+ return sttActivationController.status;
807
563
  }
808
564
 
809
565
  function describeTTSRole() {