arcane-os 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -5,7 +5,7 @@
5
5
  --arcane-speech-padding-block-start: .65rem;
6
6
  --arcane-speech-padding-inline: .75rem;
7
7
  display: grid;
8
- grid-template-columns: auto minmax(0, 1fr) max-content max-content;
8
+ grid-template-columns: auto minmax(0, 1fr) max-content;
9
9
  align-items: center;
10
10
  gap: var(--arcane-space-3, .75rem);
11
11
  box-sizing: border-box;
@@ -94,16 +94,8 @@
94
94
  color: var(--arcane-danger, var(--danger-color));
95
95
  }
96
96
 
97
- #endButton {
98
- grid-column: 3;
99
- justify-self: end;
100
- background: var(--arcane-danger, var(--danger-color));
101
- border-color: var(--arcane-danger, var(--danger-color));
102
- color: var(--arcane-action-text, var(--button-text-color, var(--text-color)));
103
- }
104
-
105
97
  #muteButton {
106
- grid-column: 4;
98
+ grid-column: 3;
107
99
  justify-self: end;
108
100
  float: none;
109
101
  background: var(--arcane-secondary, var(--secondary-color));
@@ -125,11 +117,6 @@
125
117
  grid-column: 2 / span 2;
126
118
  }
127
119
 
128
- #endButton {
129
- grid-row: 1;
130
- grid-column: 2;
131
- }
132
-
133
120
  #muteButton {
134
121
  grid-row: 1;
135
122
  grid-column: 3;
@@ -142,10 +129,10 @@
142
129
  <button id="sttActivationButton" type="button" hidden disabled>Start transcription</button>
143
130
  <button id="stopButton" type="button" class="hidden" disabled>Send</button>
144
131
  <p id="speechStatus" class="speech_status" role="status" aria-live="polite"></p>
145
- <button id="endButton" type="button" class="end">End</button>
146
132
  <button id="muteButton" type="button" class="mute" aria-pressed="true" aria-label="Voice unavailable" disabled>Voice unavailable</button>
147
133
 
148
134
  <script type="module">
135
+ const completeValue = (value) => value;
149
136
  const {
150
137
  createArcaneEventSource,
151
138
  projectArcaneDOMEvent
@@ -187,7 +174,6 @@
187
174
  const recordButton = shadowRoot.querySelector('#recordButton');
188
175
  const sttActivationButton = shadowRoot.querySelector('#sttActivationButton');
189
176
  const stopButton = shadowRoot.querySelector('#stopButton');
190
- const endButton = shadowRoot.querySelector('#endButton');
191
177
  const muteButton = shadowRoot.querySelector('#muteButton');
192
178
  const speechStatus = shadowRoot.querySelector('#speechStatus');
193
179
  const silenceDuration = 0;
@@ -260,17 +246,23 @@
260
246
 
261
247
  function publicSpeechErrorFields(error, boundaryCode) {
262
248
  let causeCode = '';
249
+ let message = '';
263
250
  try {
264
251
  causeCode = typeof error?.code === 'string'
265
- ? error.code.trim()
252
+ ? error.code
253
+ : '';
254
+ message = typeof error?.message === 'string'
255
+ ? error.message
266
256
  : '';
267
257
  } catch {}
268
- return Object.freeze(
258
+ return completeValue(
269
259
  {
260
+ error,
270
261
  code: boundaryCode,
271
262
  ...(causeCode && causeCode !== boundaryCode
272
263
  ? { causeCode }
273
- : {})
264
+ : {}),
265
+ ...(message ? { message } : {})
274
266
  }
275
267
  );
276
268
  }
@@ -327,7 +319,7 @@
327
319
  }
328
320
  );
329
321
 
330
- const lifecycleListenerOptions = Object.freeze(
322
+ const lifecycleListenerOptions = completeValue(
331
323
  { signal: runtimeStateAbortController.signal }
332
324
  );
333
325
  recordButton.addEventListener('pointerdown', handleRecordPointerDown, lifecycleListenerOptions);
@@ -339,7 +331,6 @@
339
331
  recordButton.addEventListener('blur', handleRecordBlur, lifecycleListenerOptions);
340
332
  stopButton.addEventListener('click', stop, lifecycleListenerOptions);
341
333
  muteButton.addEventListener('click', muteToggle, lifecycleListenerOptions);
342
- endButton.addEventListener('click', submitCompletionCommand, lifecycleListenerOptions);
343
334
  window.addEventListener(
344
335
  'pagehide',
345
336
  destroy,
@@ -352,8 +343,8 @@
352
343
  signal: runtimeStateAbortController.signal
353
344
  }
354
345
  );
355
- if (!host.initialMuted && ttsRole.state === 'ready') {
356
- host.muted = false;
346
+ if (!host.initialMuted) {
347
+ applyConfiguredMutedState(false);
357
348
  }
358
349
  synchronizeAIMutedState();
359
350
  renderControls();
@@ -385,7 +376,7 @@
385
376
  );
386
377
 
387
378
  function unavailableRole(role) {
388
- return Object.freeze(
379
+ return completeValue(
389
380
  {
390
381
  role,
391
382
  state: 'unavailable',
@@ -458,6 +449,13 @@
458
449
  if (microphoneWasAvailable && !host.availability.microphone) {
459
450
  cancelSTTOperation('microphone-unavailable');
460
451
  }
452
+ if (
453
+ !microphoneWasAvailable
454
+ && host.availability.microphone
455
+ && localStatus?.owner === 'microphone'
456
+ ) {
457
+ localStatus = null;
458
+ }
461
459
  applyRoleTransitions(previousSTTRole, previousTTSRole);
462
460
  return { ...host.availability };
463
461
  }
@@ -482,9 +480,12 @@
482
480
  if (muted) {
483
481
  requestUserMute();
484
482
  } else {
485
- pendingUnmute = false;
486
- host.muted = ttsRole.state !== 'ready';
487
- synchronizeAIMutedState();
483
+ pendingUnmute = true;
484
+ host.muted = true;
485
+ if (!settleSuccessfulUnmute()) {
486
+ synchronizeAIMutedState();
487
+ continuePendingUnmute();
488
+ }
488
489
  }
489
490
  renderControls();
490
491
  renderStatus();
@@ -495,6 +496,12 @@
495
496
  const previousTTSRole = ttsRole;
496
497
  sttRole = snapshot.roles.stt;
497
498
  ttsRole = snapshot.roles.tts;
499
+ if (sttRole.state === 'error' && previousSTTRole.state !== 'error') {
500
+ console.error('Arcane transcription runtime failed.', sttRole.error);
501
+ }
502
+ if (ttsRole.state === 'error' && previousTTSRole.state !== 'error') {
503
+ console.error('Arcane voice runtime failed.', ttsRole.error);
504
+ }
498
505
  clearSettledTTSIntent();
499
506
  applyRoleTransitions(previousSTTRole, previousTTSRole);
500
507
  }
@@ -524,6 +531,7 @@
524
531
  }
525
532
 
526
533
  settleSuccessfulUnmute();
534
+ continuePendingUnmute();
527
535
 
528
536
  synchronizeAIMutedState();
529
537
  renderControls();
@@ -742,19 +750,21 @@
742
750
  if (!progress) {
743
751
  return fallback;
744
752
  }
745
-
746
- const amount = progress.total === null
747
- ? `${progress.completed} ${progress.unit}`
748
- : `${progress.completed} of ${progress.total} ${progress.unit}`;
749
- const heartbeat = progress.heartbeat ? ', active heartbeat' : '';
750
- return `${progress.phase}, ${amount}${heartbeat}`;
753
+ return progress.phase;
751
754
  }
752
755
 
753
756
  function visibleErrorMessage(error, fallback) {
754
- const message = typeof error?.message === 'string'
755
- ? error.message.trim()
756
- : '';
757
- return (message || fallback).slice(0, 240);
757
+ try {
758
+ if (error?.userSafe === true) {
759
+ const message = typeof error.userMessage === 'string'
760
+ ? error.userMessage
761
+ : error.message;
762
+ if (typeof message === 'string' && message.trim()) {
763
+ return message;
764
+ }
765
+ }
766
+ } catch {}
767
+ return fallback;
758
768
  }
759
769
 
760
770
  async function observeMicrophonePermission() {
@@ -880,11 +890,6 @@
880
890
  stop();
881
891
  }
882
892
 
883
- function submitCompletionCommand(event) {
884
- event.target.disabled = true;
885
- void transcribe('End now, run all tool functions.');
886
- }
887
-
888
893
  function muteToggle() {
889
894
  if (pendingTTSIntent === 'load' || ttsRole.state === 'loading') {
890
895
  requestUserMute();
@@ -982,6 +987,25 @@
982
987
  return true;
983
988
  }
984
989
 
990
+ function continuePendingUnmute() {
991
+ if (!pendingUnmute || destroyed || !selectedRole(ttsRole)) {
992
+ return false;
993
+ }
994
+ if (
995
+ pendingTTSIntent === 'load'
996
+ || activeTTSIntent?.action === 'load'
997
+ || ttsRole.state === 'loading'
998
+ ) {
999
+ return false;
1000
+ }
1001
+ if (ttsRole.state !== 'unloaded') {
1002
+ return false;
1003
+ }
1004
+
1005
+ void requestUserUnmute();
1006
+ return true;
1007
+ }
1008
+
985
1009
  async function requestTTSIntent(action, reason) {
986
1010
  if (destroyed) {
987
1011
  return false;
@@ -991,7 +1015,7 @@
991
1015
  }
992
1016
  const generation = ++ttsIntentGeneration;
993
1017
  const operationId = nextSpeechOperationId(`tts-${action}`);
994
- const intent = Object.freeze({ action, generation, operationId });
1018
+ const intent = completeValue({ action, generation, operationId });
995
1019
  activeTTSIntent = intent;
996
1020
  ttsOperationId = operationId;
997
1021
  pendingTTSIntent = action === 'load' && ttsRole.state === 'ready'
@@ -1054,6 +1078,7 @@
1054
1078
  `ARCANE_SPEECH_TTS_${action.toUpperCase()}_REJECTED`
1055
1079
  );
1056
1080
  const reason = `tts-${action}-rejected`;
1081
+ console.error(`Arcane voice ${action} request failed.`, error);
1057
1082
  dispatchSpeechEvent(
1058
1083
  'speech-tts-lifecycle-error',
1059
1084
  {
@@ -1084,43 +1109,58 @@
1084
1109
  if (destroyed) {
1085
1110
  return false;
1086
1111
  }
1087
- const playbackRejected = boundary === 'playback';
1088
- const message = visibleErrorMessage(
1089
- error,
1090
- playbackRejected
1091
- ? 'Speech playback failed.'
1092
- : 'Speech synthesis failed.'
1093
- );
1112
+ const failures = {
1113
+ synthesis: {
1114
+ code: 'ARCANE_SPEECH_TTS_SYNTHESIS_REQUEST_REJECTED',
1115
+ message: 'Speech synthesis failed.',
1116
+ reason: 'tts-synthesis-rejected'
1117
+ },
1118
+ decode: {
1119
+ code: 'ARCANE_SPEECH_TTS_DECODE_REJECTED',
1120
+ message: 'Speech audio could not be decoded.',
1121
+ reason: 'tts-decode-rejected'
1122
+ },
1123
+ 'playback-start': {
1124
+ code: 'ARCANE_SPEECH_TTS_PLAYBACK_START_REJECTED',
1125
+ message: 'Speech playback could not start.',
1126
+ reason: 'tts-playback-start-rejected'
1127
+ },
1128
+ 'playback-resume': {
1129
+ code: 'ARCANE_SPEECH_TTS_PLAYBACK_RESUME_REJECTED',
1130
+ message: 'Speech playback could not resume.',
1131
+ reason: 'tts-playback-resume-rejected'
1132
+ }
1133
+ };
1134
+ const normalizedBoundary = Object.hasOwn(failures, boundary)
1135
+ ? boundary
1136
+ : 'synthesis';
1137
+ const failure = failures[normalizedBoundary];
1138
+ const message = visibleErrorMessage(error, failure.message);
1094
1139
  host.muted = true;
1095
1140
  stopTTSPlayback();
1096
1141
  localStatus = {
1097
- message: `${playbackRejected ? 'Speech playback' : 'Speech synthesis'} failed: ${message}`,
1142
+ message,
1098
1143
  tone: 'error'
1099
1144
  };
1100
- const errorFields = publicSpeechErrorFields(
1101
- error,
1102
- playbackRejected
1103
- ? 'ARCANE_SPEECH_TTS_PLAYBACK_RESUME_REJECTED'
1104
- : 'ARCANE_SPEECH_TTS_SYNTHESIS_REQUEST_REJECTED'
1105
- );
1106
- const reason = playbackRejected
1107
- ? 'tts-playback-resume-rejected'
1108
- : 'tts-synthesis-rejected';
1145
+ const errorFields = publicSpeechErrorFields(error, failure.code);
1146
+ console.error(`Arcane ${normalizedBoundary} speech operation failed.`, error);
1109
1147
  dispatchSpeechEvent(
1110
1148
  'speech-synthesis-error',
1111
1149
  {
1150
+ boundary: normalizedBoundary,
1112
1151
  ...errorFields,
1113
1152
  error,
1114
1153
  message,
1115
- reason
1154
+ reason: failure.reason
1116
1155
  },
1117
1156
  {
1118
1157
  bubbles: true,
1119
1158
  composed: true,
1120
- operationId: nextSpeechOperationId('synthesis'),
1159
+ operationId: nextSpeechOperationId(`tts-${normalizedBoundary}`),
1121
1160
  publicDetail: {
1161
+ boundary: normalizedBoundary,
1122
1162
  ...errorFields,
1123
- reason
1163
+ reason: failure.reason
1124
1164
  }
1125
1165
  }
1126
1166
  );
@@ -1156,7 +1196,7 @@
1156
1196
 
1157
1197
  void Promise.resolve(aiRuntime.resumeAudio()).catch(
1158
1198
  function reportTTSResumeFailure(error) {
1159
- reportTTSError(error, 'playback');
1199
+ reportTTSError(error, 'playback-resume');
1160
1200
  }
1161
1201
  );
1162
1202
  }
@@ -1170,7 +1210,7 @@
1170
1210
  {
1171
1211
  operationId,
1172
1212
  publicDetail: {
1173
- characterCount: transcription.length
1213
+ text: transcription
1174
1214
  }
1175
1215
  }
1176
1216
  );
@@ -1214,11 +1254,14 @@
1214
1254
  ['NotAllowedError', 'NotFoundError', 'NotReadableError', 'SecurityError']
1215
1255
  .includes(error?.name)
1216
1256
  ) {
1217
- setAvailability({ microphone: false });
1218
1257
  localStatus = {
1219
1258
  message: 'Microphone access is unavailable. Text chat is still available.',
1220
- tone: 'error'
1259
+ tone: 'error',
1260
+ owner: 'microphone'
1221
1261
  };
1262
+ if (microphonePermissionStatus?.state === 'denied') {
1263
+ setAvailability({ microphone: false });
1264
+ }
1222
1265
  dispatchSpeechEvent(
1223
1266
  'speech-microphone-unavailable',
1224
1267
  { reason: error.name },
@@ -1466,6 +1509,7 @@
1466
1509
  'ARCANE_SPEECH_STT_TRANSCRIPTION_REQUEST_REJECTED'
1467
1510
  );
1468
1511
  const reason = 'stt-transcription-rejected';
1512
+ console.error('Arcane speech transcription failed.', error);
1469
1513
  dispatchSpeechEvent(
1470
1514
  'speech-transcription-error',
1471
1515
  {
@@ -1,9 +1,9 @@
1
1
  <link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>
3
3
  :host{display:block;height:100%;min-height:18rem;--terminal-bg:var(--background,rgb(244, 246, 251));--terminal-fg:var(--text-color,rgb(23, 34, 56));--terminal-muted:var(--muted-text-color,rgb(95, 107, 130));--terminal-accent:var(--focus-color,rgb(118, 87, 213));--terminal-danger:var(--danger-color,rgb(165, 56, 56));--terminal-font:ui-monospace,"Cascadia Code","SFMono-Regular",Consolas,monospace;--terminal-font-size:14px;--terminal-line-height:1.55;--terminal-opacity:1}
4
- *{box-sizing:border-box}.workspace{background:color-mix(in srgb,var(--terminal-bg) calc(var(--terminal-opacity)*100%),transparent);border:1px solid color-mix(in srgb,var(--terminal-accent) 20%,transparent);border-radius:.85rem;color:var(--terminal-fg);display:grid;grid-template-rows:auto auto minmax(0,1fr) auto;height:100%;overflow:hidden;box-shadow:0 1.5rem 5rem rgba(0,0,0,.28)}
4
+ *{box-sizing:border-box}.workspace{background:color-mix(in srgb,var(--terminal-bg) calc(var(--terminal-opacity)*100%),transparent);border:1px solid color-mix(in srgb,var(--terminal-accent) 20%,transparent);border-radius:.85rem;color:var(--terminal-fg);display:grid;grid-template-rows:auto auto minmax(0,1fr) auto;height:100%;overflow:visible;box-shadow:0 1.5rem 5rem rgba(0,0,0,.28)}
5
5
  .tabs,.toolbar{align-items:center;background:color-mix(in srgb,var(--terminal-bg) 88%,white 12%);display:flex;gap:.35rem;min-width:0;padding:.45rem .65rem}.tabs{border-bottom:1px solid color-mix(in srgb,var(--terminal-fg) 10%,transparent)}
6
- .tab{align-items:center;background:transparent;border:1px solid transparent;border-radius:.45rem;color:var(--terminal-muted);display:flex;font:600 .78rem/1 var(--terminal-font);gap:.35rem;max-width:13rem;padding:.55rem .65rem}.tab[aria-selected="true"]{background:color-mix(in srgb,var(--terminal-accent) 12%,transparent);border-color:color-mix(in srgb,var(--terminal-accent) 30%,transparent);color:var(--terminal-fg)}.tab-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close{border:0;background:transparent;color:inherit;cursor:pointer;padding:0 .15rem}.new-session,.tool{background:transparent;border:1px solid transparent;border-radius:.4rem;color:var(--terminal-muted);cursor:pointer;font:600 .78rem/1 var(--terminal-font);padding:.55rem}.new-session:hover,.tool:hover{border-color:color-mix(in srgb,var(--terminal-accent) 25%,transparent);color:var(--terminal-fg)}
6
+ .tab{align-items:center;background:transparent;border:1px solid transparent;border-radius:.45rem;color:var(--terminal-muted);display:flex;font:600 .78rem/1 var(--terminal-font);gap:.35rem;padding:.55rem .65rem}.tab[aria-selected="true"]{background:color-mix(in srgb,var(--terminal-accent) 12%,transparent);border-color:color-mix(in srgb,var(--terminal-accent) 30%,transparent);color:var(--terminal-fg)}.tab-label{overflow-wrap:anywhere;white-space:normal}.tab-close{border:0;background:transparent;color:inherit;cursor:pointer;padding:0 .15rem}.new-session,.tool{background:transparent;border:1px solid transparent;border-radius:.4rem;color:var(--terminal-muted);cursor:pointer;font:600 .78rem/1 var(--terminal-font);padding:.55rem}.new-session:hover,.tool:hover{border-color:color-mix(in srgb,var(--terminal-accent) 25%,transparent);color:var(--terminal-fg)}
7
7
  .toolbar{border-bottom:1px solid color-mix(in srgb,var(--terminal-fg) 8%,transparent);justify-content:flex-end}.connection{align-items:center;display:flex;gap:.4rem;margin-right:auto;color:var(--terminal-muted);font:600 .72rem/1 var(--terminal-font);text-transform:uppercase;letter-spacing:.08em}.connection::before{background:var(--terminal-muted);border-radius:50%;content:"";height:.48rem;width:.48rem}.connection[data-state="running"]::before{background:var(--terminal-accent);box-shadow:0 0 .7rem var(--terminal-accent)}.connection[data-state="error"]::before,.connection[data-state="exited"]::before{background:var(--terminal-danger)}
8
8
  .search{background:var(--terminal-bg);border:1px solid color-mix(in srgb,var(--terminal-fg) 16%,transparent);border-radius:.4rem;color:var(--terminal-fg);font:inherit;max-width:15rem;padding:.4rem .55rem}.search[hidden]{display:none}
9
9
  .output{font:var(--terminal-font-size)/var(--terminal-line-height) var(--terminal-font);overflow:auto;overscroll-behavior:contain;padding:1rem 1.1rem;scrollbar-color:color-mix(in srgb,var(--terminal-accent) 30%,transparent) transparent;white-space:pre-wrap;word-break:break-word}.entry{min-height:1em}.entry.command{color:var(--terminal-accent);font-weight:650}.entry.system{color:var(--terminal-muted)}.entry.error{color:var(--terminal-danger)}mark{background:color-mix(in srgb,var(--terminal-accent) 38%,transparent);color:inherit}
@@ -50,11 +50,11 @@
50
50
  function addSession(session){sessions.set(session.id,{...session,entries:session.entries||[]});if(!activeId)activeId=session.id;renderTabs();render();setState(session.state||'starting');}
51
51
  function removeSession(id){sessions.delete(id);if(activeId===id)activeId=sessions.keys().next().value||'';renderTabs();render();}
52
52
  function activateSession(id){if(!sessions.has(id))return;activeId=id;renderTabs();render();setState(sessions.get(id).state||'running');input.focus();emit('terminal-session-select',{sessionId:id});}
53
- function append(text,kind='output',sessionId=activeId){const session=sessions.get(sessionId);if(!session)return;session.entries.push({text:String(text??''),kind});if(session.entries.length>4000)session.entries.splice(0,session.entries.length-4000);if(sessionId===activeId)render(true);}
53
+ function append(text,kind='output',sessionId=activeId){const session=sessions.get(sessionId);if(!session)return;session.entries.push({text:String(text??''),kind});if(sessionId===activeId)render(true);}
54
54
  function clear(sessionId=activeId){const session=sessions.get(sessionId);if(session)session.entries=[];render();emit('terminal-clear',{sessionId});}
55
55
  function setState(state,id=activeId){const session=sessions.get(id);if(session)session.state=state;if(id===activeId){connection.dataset.state=state;connection.textContent=state;}}
56
56
  function setTheme(theme={}){for(const [key,value] of Object.entries(theme)){const name=`--terminal-${key.replace(/[A-Z]/g,match=>`-${match.toLowerCase()}`)}`;host.style.setProperty(name,String(value));}}
57
- function submit(){const line=input.value.trimEnd();if(!line.trim())return;if(history.at(-1)!==line)history.push(line);historyIndex=history.length;append(`${promptLabel.textContent} ${line}\n`,'command');input.value='';resizeInput();emit('terminal-submit',{sessionId:activeId,line,history:[...history]});}
57
+ function submit(){const line=input.value;if(!line.trim())return;if(history.at(-1)!==line)history.push(line);historyIndex=history.length;append(`${promptLabel.textContent} ${line}\n`,'command');input.value='';resizeInput();emit('terminal-submit',{sessionId:activeId,line,history:[...history]});}
58
58
  function recall(direction){if(!history.length)return;historyIndex=Math.max(0,Math.min(history.length,historyIndex+direction));input.value=historyIndex===history.length?'':history[historyIndex];resizeInput();input.setSelectionRange(input.value.length,input.value.length);}
59
59
  function complete(){const prefix=input.value.split(/\s/).at(-1).toLowerCase();const matches=completions.filter(value=>value.toLowerCase().startsWith(prefix));if(matches.length===1)input.value=input.value.slice(0,-prefix.length)+matches[0];else if(matches.length>1)append(`${matches.join(' ')}\n`,'system');resizeInput();}
60
60
  function renderTabs(){const fragment=document.createDocumentFragment();for(const session of sessions.values()){const button=document.createElement('button');button.className='tab';button.type='button';button.setAttribute('role','tab');button.setAttribute('aria-selected',String(session.id===activeId));button.innerHTML=`<span class="tab-label"></span><span class="tab-close" aria-label="Close session">×</span>`;button.querySelector('.tab-label').textContent=session.title||session.shell||'Terminal';button.onclick=event=>{if(event.target.classList.contains('tab-close'))emit('terminal-session-close',{sessionId:session.id});else activateSession(session.id);};fragment.append(button);}const add=document.createElement('button');add.className='new-session';add.type='button';add.textContent='+';add.title='New terminal session';add.onclick=()=>emit('terminal-session-new');fragment.append(add);tabs.replaceChildren(fragment);}
@@ -6,7 +6,7 @@
6
6
  <header class="arcane-card__header intro"><h2 class="arcane-section-heading">Create your own skin</h2><p class="arcane-help">Choose a small set of safe design tokens. Compatible Arcane apps use the same saved skin.</p></header>
7
7
  <div class="arcane-card__body editor">
8
8
  <div class="identity">
9
- <div class="arcane-field"><label for="themeName">Skin name</label><input id="themeName" name="name" maxlength="40" required></div>
9
+ <div class="arcane-field"><label for="themeName">Skin name</label><input id="themeName" name="name" required></div>
10
10
  <div class="arcane-field"><label for="themeScheme">Starting contrast</label><select id="themeScheme" name="scheme"><option value="light">Light</option><option value="dark">Dark</option></select></div>
11
11
  </div>
12
12
  <div id="colors" class="colors"></div>
@@ -1,6 +1,6 @@
1
1
  <link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>
3
- :host{display:block;min-width:0}.toolbar{display:grid;gap:.75rem;padding:1rem;border-bottom:1px solid var(--border-color,#ccd4df)}.toolbar-row{display:flex;gap:.5rem}.toolbar input{min-width:0;width:100%}.filters{display:flex;gap:.4rem;overflow:auto}.filters button{white-space:nowrap}.list{display:grid;max-height:calc(100vh - 17rem);overflow:auto}.thread{display:grid;gap:.35rem;width:100%;padding:1rem;border:0;border-bottom:1px solid var(--border-color,#ccd4df);color:inherit;background:transparent;text-align:left}.thread:hover,.thread[aria-current="true"]{background:var(--secondary-color,#e8edf4)}.thread:focus-visible{outline:.18rem solid var(--focus-color,#7657d5);outline-offset:-.2rem}.line{align-items:center;display:flex;gap:.5rem;justify-content:space-between}.title{overflow:hidden;font-weight:750;text-overflow:ellipsis;white-space:nowrap}.preview{color:var(--muted-text-color,#607080);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta{align-items:center;display:flex;gap:.4rem;font-size:.75rem}.badge{border:1px solid var(--border-color,#ccd4df);border-radius:999px;padding:.15rem .45rem;text-transform:uppercase}.unread{display:grid;place-items:center;min-width:1.4rem;height:1.4rem;border-radius:999px;color:var(--button-text-color,#fff);background:var(--focus-color,#7657d5);font-weight:800}.empty{padding:2rem;text-align:center}.empty p{color:var(--muted-text-color,#607080)}
3
+ :host{display:block;min-width:0}.toolbar{display:grid;gap:.75rem;padding:1rem;border-bottom:1px solid var(--border-color,#ccd4df)}.toolbar-row{display:flex;gap:.5rem}.toolbar input{min-width:0;width:100%}.filters{display:flex;flex-wrap:wrap;gap:.4rem}.filters button{overflow-wrap:anywhere}.list{display:grid}.thread{display:grid;gap:.35rem;width:100%;padding:1rem;border:0;border-bottom:1px solid var(--border-color,#ccd4df);color:inherit;background:transparent;text-align:left}.thread:hover,.thread[aria-current="true"]{background:var(--secondary-color,#e8edf4)}.thread:focus-visible{outline:.18rem solid var(--focus-color,#7657d5);outline-offset:-.2rem}.line{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem;justify-content:space-between}.title{font-weight:750;overflow-wrap:anywhere}.preview{color:var(--muted-text-color,#607080);overflow-wrap:anywhere}.meta{align-items:center;display:flex;flex-wrap:wrap;gap:.4rem;font-size:.75rem}.badge{border:1px solid var(--border-color,#ccd4df);border-radius:999px;padding:.15rem .45rem;text-transform:uppercase}.unread{display:grid;place-items:center;min-width:1.4rem;height:1.4rem;border-radius:999px;color:var(--button-text-color,#fff);background:var(--focus-color,#7657d5);font-weight:800}.empty{padding:2rem;text-align:center}.empty p{color:var(--muted-text-color,#607080)}
4
4
  </style>
5
5
  <section aria-label="Unified inbox">
6
6
  <div class="toolbar"><div class="toolbar-row"><input id="search" type="search" placeholder="Search conversations" aria-label="Search conversations"><button id="refresh" class="arcane-button arcane-button--tertiary" type="button">Refresh</button></div><div id="filters" class="filters" aria-label="Channel filters"></div></div>
@@ -20,6 +20,6 @@ root.querySelector('#refresh').addEventListener('click',()=>emit('inbox-refresh'
20
20
  search.addEventListener('input',render);
21
21
  function renderFilters(values){const names=['all',...new Set(values||[])];filters.replaceChildren(...names.map(name=>{const button=document.createElement('button');button.type='button';button.className='arcane-button arcane-button--tertiary';button.textContent=name==='all'?'All':name.toUpperCase();button.setAttribute('aria-pressed',String(channel===name));button.addEventListener('click',()=>{channel=name;renderFilters(values);render()});return button}))}
22
22
  function render(){const term=search.value.trim().toLowerCase();const visible=threads.filter(item=>(channel==='all'||item.channel===channel)&&(!term||`${item.title} ${item.preview} ${(item.participants||[]).join(' ')}`.toLowerCase().includes(term)));empty.hidden=visible.length>0;list.replaceChildren(...visible.map(makeThread));}
23
- function makeThread(item){const button=document.createElement('button');button.type='button';button.className='thread';button.setAttribute('aria-current',String(item.id===activeId));const top=document.createElement('span'),title=document.createElement('span'),time=document.createElement('time'),preview=document.createElement('span'),meta=document.createElement('span'),badge=document.createElement('span'),provider=document.createElement('span');top.className='line';title.className='title';title.textContent=item.title;time.dateTime=item.updatedAt;time.textContent=new Intl.DateTimeFormat(undefined,{month:'short',day:'numeric'}).format(new Date(item.updatedAt));preview.className='preview';preview.textContent=item.preview||'No preview';meta.className='meta';badge.className='badge';badge.textContent=item.channel;provider.textContent=providers[item.providerId]?.label||item.providerId;top.append(title,time);meta.append(badge,provider);if(item.unreadCount){const unread=document.createElement('span');unread.className='unread';unread.textContent=String(item.unreadCount);meta.append(unread)}button.append(top,preview,meta);button.addEventListener('click',()=>emit('thread-select',{thread:item},{threadId:String(item.id||''),channel:String(item.channel||'')}));return button;}
23
+ function makeThread(item){const button=document.createElement('button');button.type='button';button.className='thread';button.setAttribute('aria-current',String(item.id===activeId));const top=document.createElement('span'),title=document.createElement('span'),time=document.createElement('time'),preview=document.createElement('span'),meta=document.createElement('span'),badge=document.createElement('span'),provider=document.createElement('span');top.className='line';title.className='title';title.textContent=item.title;time.dateTime=item.updatedAt;time.textContent=new Intl.DateTimeFormat(undefined,{month:'short',day:'numeric'}).format(new Date(item.updatedAt));preview.className='preview';preview.textContent=item.preview||'No preview';meta.className='meta';badge.className='badge';badge.textContent=item.channel;provider.textContent=providers[item.providerId]?.label||item.providerId;top.append(title,time);meta.append(badge,provider);if(item.unreadCount){const unread=document.createElement('span');unread.className='unread';unread.textContent=String(item.unreadCount);meta.append(unread)}button.append(top,preview,meta);const detail={thread:item};button.addEventListener('click',()=>emit('thread-select',detail,detail));return button;}
24
24
  host.ready=true;emit('unified-inbox-ready',{}, {ready:true});
25
25
  </script>
@@ -60,6 +60,7 @@
60
60
  </section>
61
61
 
62
62
  <script type="module">
63
+ const completeValue=(value)=>value;
63
64
  const {
64
65
  createArcaneEventSource,
65
66
  projectArcaneDOMEvent
@@ -203,7 +204,7 @@
203
204
  }
204
205
  );
205
206
 
206
- const lifecycleListenerOptions=Object.freeze(
207
+ const lifecycleListenerOptions=completeValue(
207
208
  {signal:runtimeStateAbortController.signal}
208
209
  );
209
210
  startButton.addEventListener('click',startRecording,lifecycleListenerOptions);
@@ -225,7 +226,7 @@
225
226
  );
226
227
 
227
228
  function unavailableRole(role){
228
- return Object.freeze(
229
+ return completeValue(
229
230
  {
230
231
  role,
231
232
  state:'unavailable',
@@ -465,8 +466,8 @@
465
466
  ||destroyed){
466
467
  return false;
467
468
  }
468
- const segment=typeof result==='string'?result.trim():'';
469
- if(!segment){
469
+ const segment=typeof result==='string'?result:'';
470
+ if(segment.length===0){
470
471
  setState('idle',options.messages.noSpeech);
471
472
  return false;
472
473
  }
@@ -551,8 +552,8 @@
551
552
  composed:true,
552
553
  operationId,
553
554
  publicDetail:{
554
- segmentCharacterCount:detail.text.length,
555
- transcriptCharacterCount:detail.transcript.length
555
+ text:detail.text,
556
+ transcript:detail.transcript
556
557
  }
557
558
  }
558
559
  );
@@ -567,8 +568,8 @@
567
568
  composed:true,
568
569
  operationId,
569
570
  publicDetail:{
570
- segmentCharacterCount:detail.text.length,
571
- transcriptCharacterCount:detail.transcript.length
571
+ text:detail.text,
572
+ transcript:detail.transcript
572
573
  }
573
574
  }
574
575
  );
@@ -637,7 +638,7 @@
637
638
  }
638
639
 
639
640
  async function completeStream(){
640
- if(destroyed||!transcript.trim()||!['idle','error'].includes(state)){
641
+ if(destroyed||transcript.length===0||!['idle','error'].includes(state)){
641
642
  return false;
642
643
  }
643
644
  const generation=sessionGeneration;
@@ -665,7 +666,7 @@
665
666
  bubbles:true,
666
667
  composed:true,
667
668
  publicDetail:{
668
- transcriptCharacterCount:completionTranscript.length
669
+ transcript:completionTranscript
669
670
  }
670
671
  }
671
672
  );
@@ -743,7 +744,7 @@
743
744
  bubbles:true,
744
745
  composed:true,
745
746
  publicDetail:{
746
- transcriptCharacterCount:transcript.length
747
+ transcript
747
748
  }
748
749
  }
749
750
  );
@@ -765,7 +766,7 @@
765
766
  transcriptElement.querySelectorAll('a').forEach(
766
767
  link=>{
767
768
  link.target='_blank';
768
- link.rel='noopener noreferrer';
769
+ link.removeAttribute('rel');
769
770
  }
770
771
  );
771
772
  }
@@ -820,6 +821,9 @@
820
821
  function synchronizeAIRuntimeState(snapshot){
821
822
  const previousRole=sttRole;
822
823
  sttRole=snapshot.roles.stt;
824
+ if(sttRole.state==='error'&&previousRole.state!=='error'){
825
+ console.error('Arcane transcription runtime failed.',sttRole.error);
826
+ }
823
827
  sttActivationController.synchronize(sttRole);
824
828
  if(previousRole.state==='ready'&&sttRole.state!=='ready'){
825
829
  if(cancelSTTOperation('runtime-unready')){
@@ -853,7 +857,7 @@
853
857
  stateMessage=message;
854
858
  dispatchVoiceEvent(
855
859
  'speech-transcription-cancelled',
856
- Object.freeze({reason}),
860
+ completeValue({reason}),
857
861
  {
858
862
  bubbles:true,
859
863
  composed:true,
@@ -886,7 +890,7 @@
886
890
  startButton.setAttribute('aria-label',startButton.title);
887
891
  stopButton.disabled=state!=='recording';
888
892
  completeButton.disabled=destroyed
889
- ||!transcript.trim()
893
+ ||transcript.length===0
890
894
  ||!['idle','error'].includes(state);
891
895
  sttActivationButton.hidden=destroyed||!sttActivationController.visible;
892
896
  sttActivationButton.disabled=destroyed
@@ -905,7 +909,7 @@
905
909
  );
906
910
  dispatchVoiceEvent(
907
911
  'voice-transcription-state',
908
- Object.freeze(
912
+ completeValue(
909
913
  {
910
914
  message:status.innerText,
911
915
  state,
@@ -915,10 +919,7 @@
915
919
  {
916
920
  bubbles:true,
917
921
  composed:true,
918
- publicDetail:{
919
- state,
920
- sttState:sttRole.state
921
- }
922
+ publicDetail:{message:status.innerText,state,stt:sttRole}
922
923
  }
923
924
  );
924
925
  }
@@ -958,8 +959,17 @@
958
959
  }
959
960
 
960
961
  function visibleErrorMessage(error,fallback){
961
- const message=typeof error?.message==='string'?error.message.trim():'';
962
- return (message||fallback).slice(0,240);
962
+ try{
963
+ if(error?.userSafe===true){
964
+ const message=typeof error.userMessage==='string'
965
+ ?error.userMessage
966
+ :error.message;
967
+ if(typeof message==='string'&&message.trim()){
968
+ return message;
969
+ }
970
+ }
971
+ }catch{}
972
+ return fallback;
963
973
  }
964
974
 
965
975
  function getOptions(){
@@ -1,13 +1,12 @@
1
1
  export default class Calculation{
2
2
  constructor({expression,result,createdAt=new Date()}={}){
3
- this.expression=String(expression??'').trim();
4
- if(!this.expression||this.expression.length>512) throw new TypeError('Calculation expressions must contain 1-512 characters.');
3
+ this.expression=String(expression??'');
4
+ if(!this.expression.trim()) throw new TypeError('Calculation expressions must contain text.');
5
5
  this.result=Number(result);
6
6
  if(!Number.isFinite(this.result)) throw new RangeError('Calculation result must be finite.');
7
7
  const instant=new Date(createdAt);
8
8
  if(Number.isNaN(instant.valueOf())) throw new TypeError('Calculation time is invalid.');
9
9
  this.createdAt=instant.toISOString();
10
- Object.freeze(this);
11
10
  }
12
11
  toJSON(){return {...this};}
13
12
  }