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
@@ -4,6 +4,8 @@
4
4
  display: block;
5
5
  height: 100%;
6
6
  min-width: 0;
7
+ min-inline-size:0;
8
+ max-inline-size:100%;
7
9
  min-height: 0;
8
10
  --arcane-chat-gap: var(--arcane-space-3, .75rem);
9
11
  --arcane-chat-radius: var(--arcane-radius-large, 1rem);
@@ -15,12 +17,15 @@
15
17
  justify-content: flex-end;
16
18
  box-sizing: border-box;
17
19
  width: min(100%, 72rem);
20
+ min-width:0;
21
+ min-inline-size:0;
22
+ max-inline-size:100%;
18
23
  height: 100%;
19
24
  min-height: 0;
20
25
  margin-inline: auto;
21
26
  padding: clamp(.65rem, 1.5vw, 1.25rem);
22
27
  gap: var(--arcane-chat-gap);
23
- overflow: hidden;
28
+ overflow: visible;
24
29
  }
25
30
 
26
31
  .chat_area .thinking{
@@ -129,15 +134,34 @@
129
134
  border:0;
130
135
  }
131
136
 
137
+ .chat_session_status{
138
+ display:block;
139
+ flex:0 0 auto;
140
+ min-height:1.25em;
141
+ padding-inline:.25rem;
142
+ color:var(--text-color);
143
+ font-size:.82rem;
144
+ overflow-wrap:anywhere;
145
+ }
146
+
147
+ .chat_session_status[data-state="error"]{
148
+ color:var(--danger-color,var(--text-color));
149
+ }
150
+
132
151
  .chat_output {
133
152
  flex:1 1 auto;
153
+ display:flow-root;
134
154
  box-sizing: border-box;
135
155
  width: 100%;
136
156
  min-height:0;
157
+ min-inline-size:0;
158
+ max-inline-size:100%;
137
159
  margin: 0;
138
160
  padding: clamp(1rem, 2vw, 1.5rem);
139
161
  list-style: none;
162
+ overflow-x: hidden;
140
163
  overflow-y: auto;
164
+ overscroll-behavior: contain;
141
165
  scrollbar-gutter: stable;
142
166
  border: 1px solid var(--arcane-border, var(--border-color));
143
167
  border-radius: var(--arcane-chat-radius);
@@ -148,12 +172,16 @@
148
172
  .chat_output > li {
149
173
  box-sizing: border-box;
150
174
  width: auto;
175
+ min-width: 0;
176
+ min-inline-size:0;
177
+ max-inline-size:min(92%,48rem);
151
178
  max-width: min(92%, 48rem);
152
179
  margin: 0 0 .85rem;
153
180
  padding: .85rem 1rem;
154
181
  clear: both;
155
182
  border: 1px solid var(--arcane-border, var(--border-color));
156
183
  border-radius: calc(var(--arcane-chat-radius) - .1rem);
184
+ overflow-x: hidden;
157
185
  overflow-wrap: anywhere;
158
186
  }
159
187
 
@@ -173,6 +201,93 @@
173
201
  text-align: right;
174
202
  }
175
203
 
204
+ .message_header{
205
+ display:block;
206
+ margin:0 0 .65rem;
207
+ }
208
+
209
+ .message_timestamp{
210
+ display:block;
211
+ margin-top:.65rem;
212
+ color:currentColor;
213
+ font-size:.72rem;
214
+ line-height:1.2;
215
+ opacity:.7;
216
+ text-align:right;
217
+ }
218
+
219
+ .message_tool_calls{
220
+ display:grid;
221
+ box-sizing:border-box;
222
+ width:100%;
223
+ min-width:0;
224
+ min-inline-size:0;
225
+ max-inline-size:100%;
226
+ max-width:100%;
227
+ gap:.55rem;
228
+ margin-top:.75rem;
229
+ }
230
+
231
+ .message_tool_call{
232
+ box-sizing:border-box;
233
+ width:100%;
234
+ min-width:0;
235
+ min-inline-size:0;
236
+ max-inline-size:100%;
237
+ max-width:100%;
238
+ padding:.65rem;
239
+ border:1px solid currentColor;
240
+ border-radius:var(--arcane-radius-small,.3rem);
241
+ overflow-x:hidden;
242
+ overflow-wrap:anywhere;
243
+ }
244
+
245
+ .message_tool_message{
246
+ margin:.45rem 0 0;
247
+ white-space:pre-wrap;
248
+ overflow-wrap:anywhere;
249
+ }
250
+
251
+ .message_tool_details{
252
+ box-sizing:border-box;
253
+ width:100%;
254
+ min-width:0;
255
+ min-inline-size:0;
256
+ max-inline-size:100%;
257
+ max-width:100%;
258
+ margin-top:.55rem;
259
+ overflow-x:hidden;
260
+ }
261
+
262
+ .message_tool_details summary{
263
+ cursor:pointer;
264
+ overflow-wrap:anywhere;
265
+ }
266
+
267
+ .message_tool_call pre{
268
+ box-sizing:border-box;
269
+ width:100%;
270
+ min-width:0;
271
+ min-inline-size:0;
272
+ max-inline-size:100%;
273
+ max-width:100%;
274
+ margin:.45rem 0 0;
275
+ white-space:pre-wrap;
276
+ word-break:break-word;
277
+ overflow-x:auto;
278
+ overflow-wrap:anywhere;
279
+ }
280
+
281
+ .message_tool_call code{
282
+ display:block;
283
+ box-sizing:border-box;
284
+ min-inline-size:0;
285
+ max-inline-size:100%;
286
+ white-space:inherit;
287
+ word-break:inherit;
288
+ overflow-wrap:anywhere;
289
+ }
290
+
176
291
  .chat_output li ul,
177
292
  .chat_output li ol {
178
293
  display: block;
@@ -230,6 +345,7 @@
230
345
  }
231
346
 
232
347
  .ai_activation_copy {
348
+ flex:1 1 auto;
233
349
  min-width:0;
234
350
  }
235
351
 
@@ -244,6 +360,18 @@
244
360
  font-size:.9rem;
245
361
  }
246
362
 
363
+ .ai_activation_progress{
364
+ display:block;
365
+ width:100%;
366
+ height:.6rem;
367
+ margin-block-start:.7rem;
368
+ accent-color:var(--arcane-action,var(--primary-color));
369
+ }
370
+
371
+ .ai_activation_progress[hidden]{
372
+ display:none;
373
+ }
374
+
247
375
  .ai_activation button {
248
376
  flex:0 0 auto;
249
377
  min-height:2.65rem;
@@ -271,7 +399,6 @@
271
399
  box-sizing: border-box;
272
400
  width: 100%;
273
401
  min-height: 3rem;
274
- max-height: 10rem;
275
402
  margin: 0;
276
403
  padding: .82rem .9rem;
277
404
  border: 1px solid var(--arcane-border, var(--border-color));
@@ -281,8 +408,8 @@
281
408
  font: inherit;
282
409
  font-size: 1rem;
283
410
  line-height: 1.35;
284
- resize: none;
285
- overflow-y: auto;
411
+ resize: vertical;
412
+ overflow: visible;
286
413
  }
287
414
 
288
415
  .chat_input:focus-visible {
@@ -380,6 +507,16 @@
380
507
  outline-offset: .18rem;
381
508
  }
382
509
 
510
+ :host([presentation="basic"]) .chat_input_area {
511
+ grid-template-columns:minmax(0,1fr) auto;
512
+ }
513
+
514
+ :host([presentation="basic"]) #chat_upload,
515
+ :host([presentation="basic"]) #file_upload_input,
516
+ :host([presentation="basic"]) .controls {
517
+ display:none;
518
+ }
519
+
383
520
  @media (max-width: 44rem) {
384
521
  .chat_area {
385
522
  padding: var(--arcane-space-2, .5rem);
@@ -453,8 +590,7 @@
453
590
  background: var(--border-color);
454
591
  padding:.4em;
455
592
  border: 1px solid var(--accent-color);
456
- max-height: 15em;
457
- overflow: auto;
593
+ overflow: visible;
458
594
  }
459
595
  </style>
460
596
 
@@ -479,6 +615,13 @@
479
615
  >Starting the elapsed timer.</span>
480
616
  </section>
481
617
  <ul type='text' class='chat_output' id='chat_output' tabindex='-1' aria-label='Conversation transcript'></ul>
618
+ <output
619
+ id="chat_session_status"
620
+ class="chat_session_status"
621
+ role="status"
622
+ aria-live="polite"
623
+ data-state="idle"
624
+ >Chat session is not connected.</output>
482
625
  <section
483
626
  id="ai_activation"
484
627
  class="ai_activation"
@@ -490,11 +633,17 @@
490
633
  <span class="ai_activation_copy">
491
634
  <strong id="ai_activation_title">Language model not active</strong>
492
635
  <span id="ai_activation_status">Start the selected language model to enable chat.</span>
636
+ <progress
637
+ id="ai_activation_progress"
638
+ class="ai_activation_progress"
639
+ aria-label="Language model activation progress"
640
+ hidden
641
+ ></progress>
493
642
  </span>
494
643
  <button type="button" id="ai_activation_button">Start language model</button>
495
644
  </section>
496
645
  <div class="chat_input_area">
497
- <textarea type='text' class='chat_input' id='chat_input'></textarea>
646
+ <textarea type='text' class='chat_input' id='chat_input' placeholder='Message' aria-label='Message'></textarea>
498
647
  <button type="button" id="chat_upload" class="chat_upload" title="Upload files" aria-label="Upload files">
499
648
  <img class='upload-icon' src="./arcane/img/upload.svg" />
500
649
  </button>
@@ -503,7 +652,7 @@
503
652
  <img class='send-icon' src="./arcane/img/send.svg" />
504
653
  </button>
505
654
  </div>
506
- <html-import id="speech" href="./arcane/components/speech.html?v=3"></html-import>
655
+ <span id="speech_mount"></span>
507
656
  <section class="controls">
508
657
  <select id="languages">
509
658
  <option value="english" selected>English</option>
@@ -690,12 +839,27 @@
690
839
  }=await import('arcane-os/event-manager');
691
840
 
692
841
  const host=this;
842
+ const componentURL=new URL(
843
+ host.getAttribute('href')||'./arcane/components/chat.html',
844
+ document.baseURI
845
+ );
846
+ const speechMount=host.shadowRoot.querySelector('#speech_mount');
847
+ const speechImport=document.createElement('html-import');
848
+ speechImport.id='speech';
849
+ speechImport.setAttribute(
850
+ 'href',
851
+ new URL('./speech.html?v=3',componentURL).href
852
+ );
853
+ speechMount.replaceWith(speechImport);
693
854
  const events=createArcaneEventSource(
694
855
  host,
695
856
  {
696
857
  source:'arcane.component.chat',
697
858
  eventTypes:[
698
859
  'chat-ready',
860
+ 'chat-session-bound',
861
+ 'chat-session-message',
862
+ 'chat-session-error',
699
863
  'chat-send-message',
700
864
  'chat-send-error',
701
865
  'chat-file-uploaded',
@@ -714,7 +878,8 @@
714
878
  const textArea=shadowRoot.querySelector('#chat_input');
715
879
  const chatOutput = shadowRoot.querySelector('#chat_output');
716
880
  const send = shadowRoot.querySelector('#chat_submit');
717
- const speech = shadowRoot.querySelector('#speech');
881
+ const chatSessionStatus=shadowRoot.querySelector('#chat_session_status');
882
+ const speech = speechImport;
718
883
  const uploadBtn = shadowRoot.querySelector('#chat_upload');
719
884
  const fileInput = shadowRoot.querySelector('#file_upload_input');
720
885
  const conversationTimeboxPanel=shadowRoot.querySelector('#conversation_timebox');
@@ -723,17 +888,20 @@
723
888
  const aiActivationPanel=shadowRoot.querySelector('#ai_activation');
724
889
  const aiActivationTitle=shadowRoot.querySelector('#ai_activation_title');
725
890
  const aiActivationStatus=shadowRoot.querySelector('#ai_activation_status');
891
+ const aiActivationProgress=shadowRoot.querySelector('#ai_activation_progress');
726
892
  const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
727
893
  const [
728
894
  {default:MD},
729
895
  {default:FileEntity},
730
896
  {ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
731
- {requestAIRuntimeIntent,subscribeAIRuntimeState}
897
+ {requestAIRuntimeIntent,subscribeAIRuntimeState},
898
+ {createPersistentAIChatSession}
732
899
  ]=await Promise.all([
733
900
  import('../modules/MD.js'),
734
901
  import('../entities/File.js'),
735
902
  import('../modules/ConversationTimebox.js'),
736
- import('../modules/AIRuntimeState.js')
903
+ import('../modules/AIRuntimeState.js'),
904
+ import('../modules/PersistentAIChatSession.js')
737
905
  ]);
738
906
 
739
907
  host.language=host.language||'English';
@@ -746,6 +914,7 @@
746
914
  let latestAIRuntimeRoles=null;
747
915
  const hostSubmissionBarrier=new ConversationSubmissionBarrier();
748
916
  const aiRuntimeStateAbortController=new AbortController();
917
+ const AI_TTS_FAILURE_EVENT='ai-tts-failure';
749
918
  let programmaticSubmissionQueue=Promise.resolve(true);
750
919
  let conversationTimeboxController=null;
751
920
  let conversationTimeboxUnsubscribe=null;
@@ -754,10 +923,23 @@
754
923
  let hostSubmissionGeneration=0;
755
924
  let eventOperationSequence=0;
756
925
  let destroyed=false;
926
+ let boundChatSession=null;
927
+ let boundChatAI=null;
928
+ let sessionBindingPending=false;
929
+ let sessionMessagePending=false;
930
+ let activeSessionMessageToken=null;
931
+ let sessionBindingGeneration=0;
932
+ let sessionMessageSequence=0;
933
+ let pendingStructuralToolCall=null;
934
+ let pendingStructuralToolMessage='';
757
935
  const activeSubmissionOwnerships=new Set();
758
- const conversationTimeboxRetryDelays=Object.freeze([0,500,1_500]);
759
- const chatReasons=Object.freeze({
936
+ const conversationTimeboxRetryDelays=[0,500,1_500];
937
+ const chatReasons={
760
938
  ready:'chat-ready',
939
+ sessionBindingCompleted:'session-binding-completed',
940
+ sessionBindingRejected:'session-binding-rejected',
941
+ sessionMessageCompleted:'session-message-completed',
942
+ sessionMessageRejected:'session-message-rejected',
761
943
  messageSubmissionRequested:'message-submission-requested',
762
944
  messageSubmissionCancelled:'message-submission-cancelled',
763
945
  callerSignalAborted:'caller-signal-aborted',
@@ -771,8 +953,12 @@
771
953
  languageModelActivationRejected:'language-model-activation-rejected',
772
954
  speechSynthesisRejected:'speech-synthesis-rejected',
773
955
  conversationTimeboxDeliveryRejected:'conversation-timebox-delivery-rejected'
774
- });
775
- const chatErrorCodes=Object.freeze({
956
+ };
957
+ const chatErrorCodes={
958
+ destroyed:'ARCANE_CHAT_DESTROYED',
959
+ sessionAlreadyBound:'ARCANE_CHAT_SESSION_ALREADY_BOUND',
960
+ sessionBindingRejected:'ARCANE_CHAT_SESSION_BINDING_REJECTED',
961
+ sessionMessageRejected:'ARCANE_CHAT_SESSION_MESSAGE_REJECTED',
776
962
  messageSubmissionAborted:'ARCANE_CHAT_MESSAGE_SUBMISSION_ABORTED',
777
963
  languageModelActivationRejected:'ARCANE_CHAT_LANGUAGE_MODEL_ACTIVATION_REQUEST_REJECTED',
778
964
  hostMessageSubmissionRejected:'ARCANE_CHAT_HOST_MESSAGE_SUBMISSION_REJECTED',
@@ -780,7 +966,7 @@
780
966
  languageChangeCallbackRejected:'ARCANE_CHAT_LANGUAGE_CHANGE_CALLBACK_REJECTED',
781
967
  speechSynthesisRejected:'ARCANE_CHAT_SPEECH_SYNTHESIS_REQUEST_REJECTED',
782
968
  conversationTimeboxDeliveryRejected:'ARCANE_CHAT_CONVERSATION_TIMEBOX_DELIVERY_REJECTED'
783
- });
969
+ };
784
970
 
785
971
  function nextChatOperationId(kind){
786
972
  eventOperationSequence+=1;
@@ -826,15 +1012,528 @@
826
1012
 
827
1013
  function publicErrorFields(error,boundaryCode){
828
1014
  let causeCode='';
1015
+ let message='';
829
1016
  try{
830
- causeCode=typeof error?.code==='string'?error.code.trim():'';
1017
+ causeCode=typeof error?.code==='string'?error.code:'';
1018
+ message=typeof error?.message==='string'?error.message:'';
831
1019
  }catch{}
832
- return Object.freeze({
1020
+ return {
1021
+ error,
833
1022
  code:boundaryCode,
834
- ...(causeCode&&causeCode!==boundaryCode?{causeCode}:{})
1023
+ ...(causeCode&&causeCode!==boundaryCode?{causeCode}:{}),
1024
+ ...(message?{message}:{})
1025
+ };
1026
+ }
1027
+
1028
+ function chatError(message,code){
1029
+ const error=new Error(message);
1030
+ error.code=code;
1031
+ return error;
1032
+ }
1033
+
1034
+ function isPlainRecord(value){
1035
+ return Boolean(value)
1036
+ &&typeof value==='object'
1037
+ &&!Array.isArray(value)
1038
+ &&Object.getPrototypeOf(value)===Object.prototype;
1039
+ }
1040
+
1041
+ function visibleErrorMessage(error,fallback){
1042
+ try{
1043
+ // Only errors deliberately classified by their owner may provide UI copy.
1044
+ // Provider and protocol diagnostics remain complete in console/events.
1045
+ if(error?.userSafe===true){
1046
+ const message=typeof error.userMessage==='string'
1047
+ ?error.userMessage
1048
+ :error.message;
1049
+ if(typeof message==='string'&&message.trim()){
1050
+ return message;
1051
+ }
1052
+ }
1053
+ }catch{}
1054
+ return fallback;
1055
+ }
1056
+
1057
+ function visibleProgressText(value,fallback,field){
1058
+ if(typeof value==='string'){
1059
+ return value||fallback;
1060
+ }
1061
+ if(value&&typeof value==='object'){
1062
+ console.error(`Arcane chat progress ${field} is not text.`,value);
1063
+ return visibleErrorMessage(value,fallback);
1064
+ }
1065
+ if(value!==undefined&&value!==null&&value!==''){
1066
+ const error=new TypeError(
1067
+ `Arcane chat progress ${field} must be text.`,
1068
+ {cause:value}
1069
+ );
1070
+ console.error(`Arcane chat progress ${field} is not text.`,error);
1071
+ }
1072
+ return fallback;
1073
+ }
1074
+
1075
+ function setSessionStatus(state,message){
1076
+ const status={state:String(state),message:String(message)};
1077
+ host.sessionStatus=status;
1078
+ chatSessionStatus.dataset.state=status.state;
1079
+ chatSessionStatus.value=status.message;
1080
+ chatSessionStatus.textContent=status.message;
1081
+ return status;
1082
+ }
1083
+
1084
+ function compatibleChatSession(session){
1085
+ return Boolean(session)
1086
+ &&typeof session==='object'
1087
+ &&typeof session.ready==='function'
1088
+ &&typeof session.history==='function'
1089
+ &&typeof session.send==='function';
1090
+ }
1091
+
1092
+ function scrollTranscriptToBottom(){
1093
+ chatOutput.scrollTop=chatOutput.scrollHeight;
1094
+ return true;
1095
+ }
1096
+
1097
+ function transcriptTime(timestamp){
1098
+ const time=chatOutput.ownerDocument.createElement('time');
1099
+ time.className='message_timestamp';
1100
+ const value=timestamp instanceof Date
1101
+ ?timestamp
1102
+ :new Date(timestamp);
1103
+ if(timestamp!==undefined&&timestamp!==null&&Number.isFinite(value.getTime())){
1104
+ time.dateTime=value.toISOString();
1105
+ time.textContent=value.toLocaleTimeString([],{
1106
+ hour:'2-digit',
1107
+ minute:'2-digit',
1108
+ hourCycle:'h23'
1109
+ });
1110
+ time.title=value.toLocaleString();
1111
+ }else{
1112
+ time.textContent='Time unavailable';
1113
+ time.setAttribute('aria-label','Message time unavailable');
1114
+ }
1115
+ return time;
1116
+ }
1117
+
1118
+ function normalizeVisibleToolCalls(value){
1119
+ if(value===undefined) return [];
1120
+ if(!Array.isArray(value)){
1121
+ const failure=new TypeError('Chat session tool calls must be an array.');
1122
+ failure.code='AI_CHAT_INVALID_TOOL_CALL';
1123
+ throw failure;
1124
+ }
1125
+ return value.map((call,index)=>{
1126
+ if(
1127
+ !call
1128
+ ||typeof call!=='object'
1129
+ ||Array.isArray(call)
1130
+ ||call.type!=='function'
1131
+ ||!call.function
1132
+ ||typeof call.function!=='object'
1133
+ ||Array.isArray(call.function)
1134
+ ||typeof call.id!=='string'
1135
+ ||!call.id.trim()
1136
+ ||typeof call.function.name!=='string'
1137
+ ||!call.function.name.trim()
1138
+ ||typeof call.function.arguments!=='string'
1139
+ ){
1140
+ const failure=new TypeError(`Chat session tool call ${index+1} is invalid.`);
1141
+ failure.code='AI_CHAT_INVALID_TOOL_CALL';
1142
+ throw failure;
1143
+ }
1144
+ structuralToolMessage(call);
1145
+ return call;
835
1146
  });
836
1147
  }
837
1148
 
1149
+ function structuralToolMessage(call){
1150
+ let argumentsRecord;
1151
+ try{
1152
+ argumentsRecord=JSON.parse(call.function.arguments);
1153
+ }catch(error){
1154
+ const failure=new TypeError(
1155
+ 'Structural tool call arguments must encode a JSON object.',
1156
+ {cause:error}
1157
+ );
1158
+ failure.code='AI_CHAT_INVALID_TOOL_CALL';
1159
+ throw failure;
1160
+ }
1161
+ if(!isPlainRecord(argumentsRecord)){
1162
+ const failure=new TypeError(
1163
+ 'Structural tool call arguments must encode a JSON object.'
1164
+ );
1165
+ failure.code='AI_CHAT_INVALID_TOOL_CALL';
1166
+ throw failure;
1167
+ }
1168
+ if(typeof argumentsRecord.message!=='string'||!argumentsRecord.message.trim()){
1169
+ const failure=new TypeError(
1170
+ 'Structural tool call arguments must include a nonempty user-facing message.'
1171
+ );
1172
+ failure.code='AI_CHAT_TOOL_MESSAGE_REQUIRED';
1173
+ throw failure;
1174
+ }
1175
+ return argumentsRecord.message;
1176
+ }
1177
+
1178
+ function setPendingStructuralToolCall(call=null){
1179
+ if(call===null){
1180
+ pendingStructuralToolCall=null;
1181
+ pendingStructuralToolMessage='';
1182
+ return null;
1183
+ }
1184
+ pendingStructuralToolMessage=structuralToolMessage(call);
1185
+ pendingStructuralToolCall={
1186
+ id:call.id,
1187
+ type:'function',
1188
+ function:{
1189
+ name:call.function.name,
1190
+ arguments:call.function.arguments
1191
+ }
1192
+ };
1193
+ return pendingStructuralToolCall;
1194
+ }
1195
+
1196
+ function sameStructuralToolCall(left,right){
1197
+ return Boolean(left&&right)
1198
+ &&left.id===right.id
1199
+ &&left.type===right.type
1200
+ &&left.function?.name===right.function?.name
1201
+ &&left.function?.arguments===right.function?.arguments;
1202
+ }
1203
+
1204
+ function pendingStructuralToolSummary(){
1205
+ return pendingStructuralToolCall
1206
+ ?{
1207
+ id:pendingStructuralToolCall.id,
1208
+ name:pendingStructuralToolCall.function.name,
1209
+ message:pendingStructuralToolMessage
1210
+ }
1211
+ :null;
1212
+ }
1213
+
1214
+ function appendVisibleToolCall(item,call){
1215
+ const toolCalls=item.querySelector('.message_tool_calls')
1216
+ ??item.ownerDocument.createElement('section');
1217
+ if(!toolCalls.classList.contains('message_tool_calls')){
1218
+ toolCalls.className='message_tool_calls';
1219
+ toolCalls.setAttribute('aria-label','Tool calls');
1220
+ item.insertBefore(toolCalls,item.querySelector('.message_timestamp'));
1221
+ }
1222
+ const existing=[...toolCalls.children].find(
1223
+ candidate=>candidate.dataset.toolCallId===call.id
1224
+ );
1225
+ if(existing) return existing;
1226
+ const entry=item.ownerDocument.createElement('article');
1227
+ entry.className='message_tool_call';
1228
+ entry.dataset.toolCallId=call.id;
1229
+ const heading=item.ownerDocument.createElement('strong');
1230
+ heading.textContent=`Tool: ${call.function.name}`;
1231
+ const userMessage=item.ownerDocument.createElement('p');
1232
+ userMessage.className='message_tool_message';
1233
+ userMessage.textContent=structuralToolMessage(call);
1234
+ const details=item.ownerDocument.createElement('details');
1235
+ details.className='message_tool_details';
1236
+ const summary=item.ownerDocument.createElement('summary');
1237
+ summary.textContent='Tool call details';
1238
+ const argumentsBlock=item.ownerDocument.createElement('pre');
1239
+ const code=item.ownerDocument.createElement('code');
1240
+ code.textContent=call.function.arguments;
1241
+ argumentsBlock.append(code);
1242
+ details.append(summary,argumentsBlock);
1243
+ entry.append(heading,userMessage,details);
1244
+ toolCalls.append(entry);
1245
+ scrollTranscriptToBottom();
1246
+ return entry;
1247
+ }
1248
+
1249
+ function setTranscriptMessageContent(item,content,timestamp){
1250
+ if(typeof content!=='string'){
1251
+ throw new TypeError('Chat session message content must be a string.');
1252
+ }
1253
+ item.querySelector('.thinking')?.remove();
1254
+ const markdown=item.querySelector('.markdown');
1255
+ markdown.raw=content;
1256
+ markdown.innerHTML=new MD(content).rendered;
1257
+ if(timestamp!==undefined){
1258
+ item.querySelector('.message_timestamp')?.replaceWith(
1259
+ transcriptTime(timestamp)
1260
+ );
1261
+ }
1262
+ scrollTranscriptToBottom();
1263
+ return item;
1264
+ }
1265
+
1266
+ function setTranscriptMessageTimestamp(item,timestamp){
1267
+ const current=item?.querySelector?.('.message_timestamp');
1268
+ if(!current||timestamp===undefined||timestamp===null){
1269
+ return false;
1270
+ }
1271
+ current.replaceWith(transcriptTime(timestamp));
1272
+ scrollTranscriptToBottom();
1273
+ return true;
1274
+ }
1275
+
1276
+ function latestCommittedTranscriptTurn(transcript,requestMessage){
1277
+ if(!Array.isArray(transcript)) return null;
1278
+ const requestRole=requestMessage?.role??'user';
1279
+ const requestContent=requestMessage?.content;
1280
+ for(let index=transcript.length-1;index>0;index--){
1281
+ const response=transcript[index];
1282
+ const request=transcript[index-1];
1283
+ if(
1284
+ response?.role==='assistant'
1285
+ &&request?.role===requestRole
1286
+ &&request.content===requestContent
1287
+ &&(
1288
+ requestRole!=='tool'
1289
+ ||request.tool_call_id===requestMessage.tool_call_id
1290
+ )
1291
+ ){
1292
+ return {request,response};
1293
+ }
1294
+ }
1295
+ return null;
1296
+ }
1297
+
1298
+ function createTranscriptMessage(role,content,name,{timestamp,toolCalls}={}){
1299
+ if(typeof content!=='string'){
1300
+ throw new TypeError('Chat session message content must be a string.');
1301
+ }
1302
+ const item=chatOutput.ownerDocument.createElement('li');
1303
+ item.className=role==='user'?'user':'ai';
1304
+ item.dataset.role=role;
1305
+ const header=chatOutput.ownerDocument.createElement('header');
1306
+ header.className='message_header';
1307
+ const heading=chatOutput.ownerDocument.createElement('strong');
1308
+ heading.textContent=name;
1309
+ header.append(heading);
1310
+ const markdown=chatOutput.ownerDocument.createElement('div');
1311
+ markdown.className='markdown';
1312
+ markdown.raw=content;
1313
+ markdown.innerHTML=new MD(content).rendered;
1314
+ item.append(header,markdown,transcriptTime(timestamp));
1315
+ for(const call of normalizeVisibleToolCalls(toolCalls)){
1316
+ appendVisibleToolCall(item,call);
1317
+ }
1318
+ return item;
1319
+ }
1320
+
1321
+ function appendTranscriptMessage(role,content,name,{timestamp=Date.now(),toolCalls}={}){
1322
+ const item=createTranscriptMessage(role,content,name,{timestamp,toolCalls});
1323
+ chatOutput.append(item);
1324
+ scrollTranscriptToBottom();
1325
+ return item;
1326
+ }
1327
+
1328
+ function renderSessionHistory(history){
1329
+ if(!Array.isArray(history)){
1330
+ throw new TypeError('Chat session history must be an array.');
1331
+ }
1332
+ const fragment=chatOutput.ownerDocument.createDocumentFragment();
1333
+ const rendered=[];
1334
+ let restoredPendingToolCall=null;
1335
+ for(const message of history){
1336
+ if(!message||typeof message!=='object'||Array.isArray(message)){
1337
+ throw new TypeError('Chat session history contains an invalid message.');
1338
+ }
1339
+ if(message.role==='system'){
1340
+ if(restoredPendingToolCall){
1341
+ throw chatError(
1342
+ 'Saved chat contains a system record before its pending structural tool result.',
1343
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
1344
+ );
1345
+ }
1346
+ continue;
1347
+ }
1348
+ if(!['assistant','tool','user'].includes(message.role)){
1349
+ throw new TypeError(`Chat session history contains an unsupported role: ${message.role}.`);
1350
+ }
1351
+ if(typeof message.content!=='string'){
1352
+ throw new TypeError('Chat session history message content must be a string.');
1353
+ }
1354
+ if(message.role==='tool'&&!message.content.trim()){
1355
+ throw chatError(
1356
+ 'Saved chat contains a blank structural tool result.',
1357
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
1358
+ );
1359
+ }
1360
+ const calls=normalizeVisibleToolCalls(message.tool_calls);
1361
+ if(calls.length>1){
1362
+ throw chatError(
1363
+ 'Saved chat contains parallel structural tool calls.',
1364
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
1365
+ );
1366
+ }
1367
+ if(message.role!=='assistant'&&calls.length){
1368
+ throw chatError(
1369
+ 'Only assistant records may contain structural tool calls.',
1370
+ 'AI_CHAT_INVALID_TOOL_CALL'
1371
+ );
1372
+ }
1373
+ if(message.role==='tool'){
1374
+ if(
1375
+ !restoredPendingToolCall
1376
+ ||typeof message.tool_call_id!=='string'
1377
+ ||message.tool_call_id!==restoredPendingToolCall.id
1378
+ ){
1379
+ throw chatError(
1380
+ 'Saved chat contains a tool result that does not match its pending structural tool call.',
1381
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
1382
+ );
1383
+ }
1384
+ restoredPendingToolCall=null;
1385
+ }else{
1386
+ if(restoredPendingToolCall){
1387
+ throw chatError(
1388
+ 'Saved chat continues before its pending structural tool result.',
1389
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
1390
+ );
1391
+ }
1392
+ if(calls.length){
1393
+ restoredPendingToolCall=calls[0];
1394
+ }
1395
+ }
1396
+ const name=message.role==='user'
1397
+ ?host.name
1398
+ :message.role==='tool'
1399
+ ?'Tool'
1400
+ :host.aiName;
1401
+ const item=createTranscriptMessage(message.role,message.content,name,{
1402
+ timestamp:message.timestamp,
1403
+ toolCalls:calls,
1404
+ });
1405
+ fragment.append(item);
1406
+ rendered.push(item);
1407
+ }
1408
+ setPendingStructuralToolCall(restoredPendingToolCall);
1409
+ chatOutput.replaceChildren(fragment);
1410
+ scrollTranscriptToBottom();
1411
+ return rendered;
1412
+ }
1413
+
1414
+ async function bindSession(options={}){
1415
+ if(destroyed){
1416
+ throw chatError(
1417
+ 'The chat component has been destroyed and cannot bind a session.',
1418
+ chatErrorCodes.destroyed
1419
+ );
1420
+ }
1421
+ if(!isPlainRecord(options)){
1422
+ throw new TypeError('Chat session binding options must be a plain object.');
1423
+ }
1424
+ const unsupported=Object.keys(options).find(
1425
+ key=>!['ai','session','sessionOptions'].includes(key)
1426
+ );
1427
+ if(unsupported){
1428
+ throw new TypeError(`Unsupported chat session binding option: ${unsupported}.`);
1429
+ }
1430
+ if(sessionBindingPending||boundChatSession){
1431
+ throw chatError(
1432
+ 'This chat component already has a session binding.',
1433
+ chatErrorCodes.sessionAlreadyBound
1434
+ );
1435
+ }
1436
+ const ai=options.ai??null;
1437
+ const suppliedSession=options.session??null;
1438
+ const sessionOptions=options.sessionOptions??{};
1439
+ if((ai===null)===(suppliedSession===null)){
1440
+ throw new TypeError('Specify exactly one of ai or session when binding chat.');
1441
+ }
1442
+ if(!isPlainRecord(sessionOptions)){
1443
+ throw new TypeError('sessionOptions must be a plain object.');
1444
+ }
1445
+ if(suppliedSession!==null&&Object.keys(sessionOptions).length>0){
1446
+ throw new TypeError('sessionOptions cannot be used with an existing session.');
1447
+ }
1448
+ if(suppliedSession!==null&&!compatibleChatSession(suppliedSession)){
1449
+ throw new TypeError('session must expose ready(), history(), and send().');
1450
+ }
1451
+
1452
+ const operationId=nextChatOperationId('session-binding');
1453
+ const generation=++sessionBindingGeneration;
1454
+ sessionBindingPending=true;
1455
+ setSessionStatus('binding','Opening chat session…');
1456
+ try{
1457
+ const session=suppliedSession??await createPersistentAIChatSession({
1458
+ ...sessionOptions,
1459
+ ai
1460
+ });
1461
+ await session.ready();
1462
+ const history=typeof session.transcript==='function'
1463
+ ?await session.transcript()
1464
+ :await session.history();
1465
+ if(destroyed||generation!==sessionBindingGeneration){
1466
+ throw chatError(
1467
+ 'The chat component was destroyed before session binding completed.',
1468
+ chatErrorCodes.destroyed
1469
+ );
1470
+ }
1471
+ renderSessionHistory(history);
1472
+ boundChatSession=session;
1473
+ boundChatAI=ai??session.ai??null;
1474
+ host.session=session;
1475
+ setSessionStatus(
1476
+ pendingStructuralToolMessage?'tool':'ready',
1477
+ pendingStructuralToolMessage||'Chat ready.'
1478
+ );
1479
+ applyAIAvailability(host.aiAvailability);
1480
+ sessionBindingPending=false;
1481
+ const detail={
1482
+ chat:host,
1483
+ session,
1484
+ ai:boundChatAI,
1485
+ history,
1486
+ pendingTool:pendingStructuralToolSummary()
1487
+ };
1488
+ dispatchChatEvent(
1489
+ 'chat-session-bound',
1490
+ detail,
1491
+ {
1492
+ bubbles:true,
1493
+ composed:true,
1494
+ operationId,
1495
+ publicDetail:{
1496
+ ...detail,
1497
+ reason:chatReasons.sessionBindingCompleted
1498
+ }
1499
+ }
1500
+ );
1501
+ return session;
1502
+ }catch(error){
1503
+ if(!destroyed&&generation===sessionBindingGeneration){
1504
+ const internalProtocolFailure=internalStructuralToolFailure(error);
1505
+ console.error('Arcane saved chat could not be opened.',error);
1506
+ const message=internalProtocolFailure
1507
+ ?'This saved chat could not be opened.'
1508
+ :visibleErrorMessage(error,'The chat session could not be opened.');
1509
+ setSessionStatus('error',message);
1510
+ dispatchChatEvent(
1511
+ 'chat-session-error',
1512
+ {phase:'binding',error,options},
1513
+ {
1514
+ bubbles:true,
1515
+ composed:true,
1516
+ operationId,
1517
+ publicDetail:{
1518
+ phase:'binding',
1519
+ options,
1520
+ reason:chatReasons.sessionBindingRejected,
1521
+ ...publicErrorFields(
1522
+ error,
1523
+ chatErrorCodes.sessionBindingRejected
1524
+ )
1525
+ }
1526
+ }
1527
+ );
1528
+ }
1529
+ throw error;
1530
+ }finally{
1531
+ if(generation===sessionBindingGeneration){
1532
+ sessionBindingPending=false;
1533
+ }
1534
+ }
1535
+ }
1536
+
838
1537
  function isAbortSignal(value){
839
1538
  return value!==null
840
1539
  &&typeof value==='object'
@@ -879,11 +1578,11 @@
879
1578
  return true;
880
1579
  }
881
1580
 
882
- const ownership=Object.freeze({
1581
+ const ownership={
883
1582
  signal:controller.signal,
884
1583
  abort:abortChatSubmission,
885
1584
  release:releaseChatSubmissionOwnership
886
- });
1585
+ };
887
1586
  activeSubmissionOwnerships.add(ownership);
888
1587
 
889
1588
  function followAbortSignal(signal,fallbackReason){
@@ -940,6 +1639,7 @@
940
1639
  panel:aiActivationPanel,
941
1640
  title:aiActivationTitle,
942
1641
  status:aiActivationStatus,
1642
+ progress:aiActivationProgress,
943
1643
  button:aiActivationButton
944
1644
  });
945
1645
 
@@ -949,8 +1649,18 @@
949
1649
  host.setInitialSpeechMuted=setInitialSpeechMuted;
950
1650
  host.setConversationComplete=setConversationComplete;
951
1651
  host.bindConversationTimebox=bindConversationTimebox;
1652
+ host.bindSession=bindSession;
952
1653
  host.submitMessage=submitMessage;
1654
+ host.submitToolResult=submitToolResult;
1655
+ Object.defineProperty(host,'pendingTool',{
1656
+ configurable:true,
1657
+ enumerable:true,
1658
+ get:pendingStructuralToolSummary
1659
+ });
953
1660
  host.destroy=destroy;
1661
+ host.session=null;
1662
+ host.sessionStatus={state:'idle',message:'Chat session is not connected.'};
1663
+ host.modelName=typeof host.modelName==='string'?host.modelName:'';
954
1664
  host.aiAvailability={llm:false,stt:false,tts:false};
955
1665
  host.conversationComplete=false;
956
1666
  applyAIAvailability(host.aiAvailability);
@@ -965,6 +1675,17 @@
965
1675
  destroy,
966
1676
  {once:true,signal:aiRuntimeStateAbortController.signal}
967
1677
  );
1678
+ window.addEventListener(
1679
+ AI_TTS_FAILURE_EVENT,
1680
+ function reportActiveRuntimeTTSFailure(event){
1681
+ const aiRuntime=boundChatAI??globalThis.ai;
1682
+ if(!aiRuntime||event.detail?.ai!==aiRuntime){
1683
+ return;
1684
+ }
1685
+ reportTTSError(event.detail?.error,event.detail?.boundary);
1686
+ },
1687
+ {signal:aiRuntimeStateAbortController.signal}
1688
+ );
968
1689
 
969
1690
  function setConversationComplete(complete=true){
970
1691
  if(typeof complete!=='boolean'){
@@ -975,11 +1696,14 @@
975
1696
  chatArea.dataset.conversationComplete=String(complete);
976
1697
  textArea.disabled=complete;
977
1698
  uploadBtn.disabled=complete;
978
- send.disabled=complete||!host.aiAvailability.llm;
1699
+ send.disabled=complete
1700
+ ||!host.aiAvailability.llm
1701
+ ||sessionMessagePending
1702
+ ||Boolean(pendingStructuralToolMessage);
979
1703
  if(complete){
980
1704
  textArea.blur?.();
981
1705
  chatOutput.focus?.({preventScroll:true});
982
- chatOutput.scrollTop=chatOutput.scrollHeight;
1706
+ scrollTranscriptToBottom();
983
1707
  }
984
1708
  return complete;
985
1709
  }
@@ -1020,7 +1744,7 @@
1020
1744
  status=''
1021
1745
  }={}
1022
1746
  ){
1023
- const messageId=String(id).trim();
1747
+ const messageId=String(id);
1024
1748
  const normalizedTotal=Number.isFinite(Number(total))
1025
1749
  ?Math.max(0,Math.floor(Number(total)))
1026
1750
  :0;
@@ -1075,19 +1799,21 @@
1075
1799
  const warningLabel=normalizedFailed>0
1076
1800
  ?` · ${normalizedFailed} warning${normalizedFailed===1?'':'s'}`
1077
1801
  :'';
1802
+ const visibleLabel=visibleProgressText(label,'Working','label');
1803
+ const visibleStatus=visibleProgressText(status,'Streaming','status');
1078
1804
  const progressStatus=isIndeterminate
1079
- ?`${String(status||'Streaming')}${warningLabel}`
1805
+ ?`${visibleStatus}${warningLabel}`
1080
1806
  :`${normalizedCurrent} of ${normalizedTotal}${warningLabel}`;
1081
1807
  const percent=normalizedTotal>0
1082
1808
  ?normalizedCurrent/normalizedTotal*100
1083
1809
  :0;
1084
1810
 
1085
- labelElement.textContent=String(label||'Working');
1811
+ labelElement.textContent=visibleLabel;
1086
1812
  countElement.textContent=progressStatus;
1087
1813
  progress.classList.toggle('indeterminate',isIndeterminate);
1088
1814
  fill.style.width=isIndeterminate?'32%':`${percent}%`;
1089
1815
  progress.setAttribute('role','progressbar');
1090
- progress.setAttribute('aria-label',String(label||'Working'));
1816
+ progress.setAttribute('aria-label',visibleLabel);
1091
1817
  if(isIndeterminate){
1092
1818
  progress.removeAttribute('aria-valuemin');
1093
1819
  progress.removeAttribute('aria-valuemax');
@@ -1099,9 +1825,9 @@
1099
1825
  }
1100
1826
  progress.setAttribute(
1101
1827
  'aria-valuetext',
1102
- `${String(label||'Working')} ${progressStatus}`
1828
+ `${visibleLabel} ${progressStatus}`
1103
1829
  );
1104
- chatOutput.scrollTop=chatOutput.scrollHeight;
1830
+ scrollTranscriptToBottom();
1105
1831
  return true;
1106
1832
  }
1107
1833
 
@@ -1134,8 +1860,13 @@
1134
1860
 
1135
1861
  function applyAIAvailability(availability){
1136
1862
  host.aiAvailability={...availability};
1137
- send.disabled=host.conversationComplete||!availability.llm;
1138
- send.title=availability.llm
1863
+ send.disabled=host.conversationComplete
1864
+ ||!availability.llm
1865
+ ||sessionMessagePending
1866
+ ||Boolean(pendingStructuralToolMessage);
1867
+ send.title=pendingStructuralToolMessage
1868
+ ?pendingStructuralToolMessage
1869
+ :availability.llm
1139
1870
  ?'Send message'
1140
1871
  :'The selected language model service is unavailable.';
1141
1872
  send.setAttribute('aria-label',send.title);
@@ -1146,6 +1877,7 @@
1146
1877
  panel,
1147
1878
  title,
1148
1879
  status,
1880
+ progress,
1149
1881
  button,
1150
1882
  publish=dispatchChatEvent,
1151
1883
  createOperationId=nextChatOperationId,
@@ -1159,13 +1891,6 @@
1159
1891
  let requestGeneration=0;
1160
1892
  let destroyed=false;
1161
1893
 
1162
- function visibleError(error,fallback){
1163
- const message=typeof error?.message==='string'
1164
- ?error.message.trim()
1165
- :'';
1166
- return (message||fallback).slice(0,240);
1167
- }
1168
-
1169
1894
  function cancellation(error){
1170
1895
  return error?.name==='AbortError'
1171
1896
  ||['ARCANE_AI_REQUEST_ABORTED','AI_CHAT_ABORTED'].includes(error?.code);
@@ -1178,15 +1903,76 @@
1178
1903
  &&role.modelId.length>0;
1179
1904
  }
1180
1905
 
1906
+ function modelDisplayName(){
1907
+ const configured=typeof host.modelName==='string'
1908
+ ?host.modelName.trim()
1909
+ :'';
1910
+ return configured||role?.modelId||'the selected language model';
1911
+ }
1912
+
1913
+ function byteProgressUnit(value){
1914
+ const unit=String(value??'')
1915
+ .trim()
1916
+ .toLowerCase()
1917
+ .replace(/[\s_-]+/g,'');
1918
+ return unit.includes('byte')
1919
+ ||unit.includes('octet')
1920
+ ||/^(?:[kmgtpe]?i?b)(?:(?:\/|per)?(?:s|sec|second))?$/u.test(unit);
1921
+ }
1922
+
1923
+ function determinateProgress(){
1924
+ const total=Number(role?.progress?.total);
1925
+ const completed=Number(role?.progress?.completed);
1926
+ const unit=typeof role?.progress?.unit==='string'&&role.progress.unit
1927
+ ?role.progress.unit
1928
+ :'items';
1929
+ if(
1930
+ byteProgressUnit(unit)
1931
+ ||!Number.isFinite(total)
1932
+ ||total<=0
1933
+ ||!Number.isFinite(completed)
1934
+ ){
1935
+ return null;
1936
+ }
1937
+ return {
1938
+ completed:Math.max(0,completed),
1939
+ total,
1940
+ unit
1941
+ };
1942
+ }
1943
+
1181
1944
  function progressMessage(){
1182
- if(!role?.progress){
1183
- return 'The selected language model is loading.';
1945
+ const measured=determinateProgress();
1946
+ const phase=typeof role?.progress?.phase==='string'&&role.progress.phase
1947
+ ?role.progress.phase
1948
+ :'loading';
1949
+ const parts=[
1950
+ `Loading ${modelDisplayName()} through the Arcane SDK`,
1951
+ phase
1952
+ ];
1953
+ if(measured){
1954
+ parts.push(`${measured.completed} of ${measured.total} ${measured.unit}`);
1955
+ }
1956
+ parts.push('The first activation can take several minutes; keep this tab open');
1957
+ return parts.join(' · ');
1958
+ }
1959
+
1960
+ function renderProgress(){
1961
+ const visible=requestPending||role?.state==='loading';
1962
+ progress.hidden=!visible;
1963
+ if(!visible){
1964
+ progress.removeAttribute('value');
1965
+ progress.removeAttribute('max');
1966
+ return;
1967
+ }
1968
+ const measured=determinateProgress();
1969
+ if(!measured){
1970
+ progress.removeAttribute('value');
1971
+ progress.removeAttribute('max');
1972
+ return;
1184
1973
  }
1185
- const progress=role.progress;
1186
- const amount=progress.total===null
1187
- ?`${progress.completed} ${progress.unit}`
1188
- :`${progress.completed} of ${progress.total} ${progress.unit}`;
1189
- return `${progress.phase}, ${amount}${progress.heartbeat?', active heartbeat':''}.`;
1974
+ progress.max=measured.total;
1975
+ progress.value=Math.min(measured.completed,measured.total);
1190
1976
  }
1191
1977
 
1192
1978
  function render(){
@@ -1200,6 +1986,7 @@
1200
1986
  'aria-busy',
1201
1987
  String(requestPending||['loading','unloading'].includes(role?.state))
1202
1988
  );
1989
+ renderProgress();
1203
1990
  if(!visible){
1204
1991
  return;
1205
1992
  }
@@ -1211,9 +1998,9 @@
1211
1998
  return;
1212
1999
  }
1213
2000
  if(role.state==='loading'){
1214
- title.textContent='Starting language model';
2001
+ title.textContent=`Loading ${modelDisplayName()}`;
1215
2002
  status.textContent=progressMessage();
1216
- button.textContent='Cancel loading';
2003
+ button.textContent='Cancel activation';
1217
2004
  button.disabled=false;
1218
2005
  return;
1219
2006
  }
@@ -1226,7 +2013,7 @@
1226
2013
  }
1227
2014
  if(role.state==='error'||requestError){
1228
2015
  title.textContent='Language model activation failed';
1229
- status.textContent=requestError||visibleError(
2016
+ status.textContent=requestError||visibleErrorMessage(
1230
2017
  role.error,
1231
2018
  'The selected language model could not start.'
1232
2019
  );
@@ -1258,8 +2045,8 @@
1258
2045
  return false;
1259
2046
  }
1260
2047
  requestError='';
1261
- const intent=Object.freeze({role:'llm',action,reason:'user'});
1262
- const request=Object.freeze({intent,state:role});
2048
+ const intent={role:'llm',action,reason:'user'};
2049
+ const request={intent,state:role};
1263
2050
  const operationId=typeof role?.operationId==='string'&&role.operationId
1264
2051
  ?role.operationId
1265
2052
  :createOperationId('llm-activation');
@@ -1272,12 +2059,7 @@
1272
2059
  composed:true,
1273
2060
  cancelable:true,
1274
2061
  operationId,
1275
- publicDetail:{
1276
- role:'llm',
1277
- action,
1278
- reason:reasons.languageModelActivationRequested,
1279
- state:role.state
1280
- }
2062
+ publicDetail:request
1281
2063
  }
1282
2064
  );
1283
2065
  if(!accepted||destroyed||generation!==requestGeneration){
@@ -1301,22 +2083,20 @@
1301
2083
  &&['unloaded','unloading'].includes(role?.state)){
1302
2084
  return false;
1303
2085
  }
1304
- const message=visibleError(
2086
+ const message=visibleErrorMessage(
1305
2087
  error,
1306
2088
  `The language model ${action} request failed.`
1307
2089
  );
1308
2090
  requestError=message;
2091
+ console.error('Arcane language model activation request failed.',error);
1309
2092
  publish(
1310
2093
  'chat-ai-activation-error',
1311
- Object.freeze({request,error,message}),
2094
+ {request,error,message},
1312
2095
  {
1313
2096
  bubbles:true,
1314
2097
  composed:true,
1315
2098
  operationId,
1316
- publicDetail:{
1317
- role:'llm',
1318
- action,
1319
- reason:reasons.languageModelActivationRejected,
2099
+ publicDetail:{request,error,message,
1320
2100
  ...readErrorFields(
1321
2101
  error,
1322
2102
  errorCodes.languageModelActivationRejected
@@ -1342,6 +2122,13 @@
1342
2122
  if(destroyed){
1343
2123
  return;
1344
2124
  }
2125
+ if(
2126
+ nextRole?.state==='error'
2127
+ &&nextRole.error
2128
+ &&nextRole.error!==role?.error
2129
+ ){
2130
+ console.error('Arcane language model runtime failed.',nextRole.error);
2131
+ }
1345
2132
  if(role?.state!==nextRole.state
1346
2133
  ||role?.providerId!==nextRole.providerId
1347
2134
  ||role?.modelId!==nextRole.modelId
@@ -1366,7 +2153,7 @@
1366
2153
  }
1367
2154
 
1368
2155
  button.addEventListener('click',activateSelectedAI);
1369
- return Object.freeze({request,synchronize,destroy});
2156
+ return {request,synchronize,destroy};
1370
2157
  }
1371
2158
 
1372
2159
  function synchronizeAIRuntimeState(snapshot){
@@ -1421,6 +2208,7 @@
1421
2208
  try{
1422
2209
  controller.start();
1423
2210
  }catch(error){
2211
+ console.error('Arcane conversation timebox could not start.',error);
1424
2212
  conversationTimeboxStatus.textContent='The elapsed timer could not start.';
1425
2213
  conversationTimeboxPanel.dataset.error='true';
1426
2214
  conversationTimeboxUnsubscribe?.();
@@ -1503,7 +2291,7 @@
1503
2291
  const operationId=typeof context?.operationId==='string'&&context.operationId
1504
2292
  ?context.operationId
1505
2293
  :nextChatOperationId('conversation-timebox');
1506
- const deliveryContext=Object.freeze({...context,operationId});
2294
+ const deliveryContext={...context,operationId};
1507
2295
  const prior=programmaticSubmissionQueue.catch(
1508
2296
  function ignorePriorProgrammaticSubmissionFailure(){
1509
2297
  return false;
@@ -1613,7 +2401,7 @@
1613
2401
  }
1614
2402
  }
1615
2403
  );
1616
- console.error('The conversation timebox message could not be sent.');
2404
+ console.error('The conversation timebox message could not be sent.',error);
1617
2405
  return false;
1618
2406
  }
1619
2407
  );
@@ -1663,19 +2451,36 @@
1663
2451
  );
1664
2452
 
1665
2453
  async function streamMessage(text='', id='', isThinking){
2454
+ if(typeof text!=='string'){
2455
+ if(text instanceof Error){
2456
+ console.error('Arcane chat stream delivered an Error as content.',text);
2457
+ throw text;
2458
+ }
2459
+ const error=new TypeError(
2460
+ 'Arcane chat stream content must be text.',
2461
+ {cause:text}
2462
+ );
2463
+ error.code='ARCANE_CHAT_STREAM_CONTENT_INVALID';
2464
+ console.error('Arcane chat stream delivered invalid content.',error);
2465
+ throw error;
2466
+ }
1666
2467
  if(!id){
1667
2468
  console.warn('Streaming messages require ids, none specified.')
1668
2469
  return false;
1669
2470
  }
1670
2471
 
1671
- let message=chatOutput.querySelector(`#message-${id}`);
2472
+ let message=[...chatOutput.children].find(
2473
+ candidate=>candidate.id===`message-${id}`
2474
+ )||null;
1672
2475
 
1673
2476
  if(!message){
1674
2477
  await receivedMessage(host.aiName,isThinking?'':text,id);
1675
2478
  if(!isThinking){
1676
2479
  return false;
1677
2480
  }
1678
- message=chatOutput.querySelector(`#message-${id}`);
2481
+ message=[...chatOutput.children].find(
2482
+ candidate=>candidate.id===`message-${id}`
2483
+ )||null;
1679
2484
  if(!message){
1680
2485
  return false;
1681
2486
  }
@@ -1688,7 +2493,10 @@
1688
2493
  if(isThinking && !thinkingHTML){
1689
2494
  thinkingHTML=message.ownerDocument.createElement('span');
1690
2495
  thinkingHTML.className='thinking';
1691
- message.append(thinkingHTML);
2496
+ message.insertBefore(
2497
+ thinkingHTML,
2498
+ message.querySelector('.message_timestamp')
2499
+ );
1692
2500
  }
1693
2501
 
1694
2502
  if(isThinking){
@@ -1699,7 +2507,7 @@
1699
2507
  thinkingHTML.append(
1700
2508
  message.ownerDocument.createTextNode(String(text))
1701
2509
  );
1702
- chatOutput.scrollTop = chatOutput.scrollHeight;
2510
+ scrollTranscriptToBottom();
1703
2511
  return true;
1704
2512
  }
1705
2513
 
@@ -1712,7 +2520,7 @@
1712
2520
  &&host.aiAvailability.tts
1713
2521
  &&speech.muted===false
1714
2522
  ){
1715
- const aiRuntime=globalThis.ai;
2523
+ const aiRuntime=boundChatAI??globalThis.ai;
1716
2524
  if(typeof aiRuntime?.streamTTS==='function'){
1717
2525
  void Promise.resolve(aiRuntime.streamTTS(text)).catch(
1718
2526
  reportTTSError
@@ -1732,7 +2540,7 @@
1732
2540
 
1733
2541
  target.innerHTML=new MD(target.raw).rendered;
1734
2542
 
1735
- chatOutput.scrollTop = chatOutput.scrollHeight;
2543
+ scrollTranscriptToBottom();
1736
2544
  }
1737
2545
 
1738
2546
  textArea.addEventListener(
@@ -1780,7 +2588,7 @@
1780
2588
  cancelable:true,
1781
2589
  operationId,
1782
2590
  publicDetail:{
1783
- size:Number.isFinite(file.size)?file.size:null,
2591
+ file,
1784
2592
  type:String(file.type||''),
1785
2593
  reason:chatReasons.fileStorageCompleted
1786
2594
  }
@@ -1796,7 +2604,7 @@
1796
2604
  composed:true,
1797
2605
  operationId,
1798
2606
  publicDetail:{
1799
- size:Number.isFinite(file.size)?file.size:null,
2607
+ file,
1800
2608
  type:String(file.type||''),
1801
2609
  reason:chatReasons.fileStorageRejected,
1802
2610
  ...publicErrorFields(
@@ -1879,13 +2687,6 @@
1879
2687
  {signal:aiRuntimeStateAbortController.signal}
1880
2688
  );
1881
2689
 
1882
- function getMilTime() {
1883
- const now = new Date();
1884
- const hours = String(now.getHours()).padStart(2, '0');
1885
- const minutes = String(now.getMinutes()).padStart(2, '0');
1886
- return `${hours}:${minutes}`;
1887
- }
1888
-
1889
2690
  async function resizeTextArea(e={}) {
1890
2691
  if(e.key && e.key == 'Enter' && !e.shiftKey && !send.disabled){
1891
2692
  e.preventDefault();
@@ -1894,11 +2695,423 @@
1894
2695
 
1895
2696
  // Reset height to shrink to content size
1896
2697
  textArea.style.height = 'auto';
1897
- let scrollHeight = textArea.scrollHeight;
1898
- let computedStyle = window.getComputedStyle(textArea);
1899
- let lineHeight = parseInt(computedStyle.lineHeight, 10) || parseInt(computedStyle.fontSize, 10) * 1.2;
1900
- let maxHeight = lineHeight * 10;
1901
- textArea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
2698
+ textArea.style.height = `${textArea.scrollHeight}px`;
2699
+ }
2700
+
2701
+ function renderSessionMessageFailure(messageId,error){
2702
+ const message=[...chatOutput.children].find(
2703
+ candidate=>candidate.id===`message-${messageId}`
2704
+ )||null;
2705
+ if(!message){
2706
+ return false;
2707
+ }
2708
+ message.classList.add('session_error');
2709
+ message.querySelector('.thinking')?.remove();
2710
+ const markdown=message.querySelector('.markdown');
2711
+ const text=visibleErrorMessage(error,'The AI request failed.');
2712
+ markdown.raw=text;
2713
+ markdown.innerHTML=new MD(text).rendered;
2714
+ scrollTranscriptToBottom();
2715
+ return true;
2716
+ }
2717
+
2718
+ function internalStructuralToolFailure(error){
2719
+ return [
2720
+ 'ARCANE_AI_TOOL_CALL_INVALID',
2721
+ 'ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED',
2722
+ 'ARCANE_AI_TOOL_MESSAGE_REQUIRED',
2723
+ 'AI_CHAT_INVALID_TOOL_CALL',
2724
+ 'AI_CHAT_INVALID_TOOL_MESSAGE',
2725
+ 'AI_CHAT_INCOHERENT_PERSISTENCE',
2726
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED',
2727
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
2728
+ 'AI_CHAT_TOOL_MESSAGE_REQUIRED',
2729
+ 'AI_CHAT_TOOL_RESULT_NOT_PENDING',
2730
+ 'AI_CHAT_TOOL_RESULT_REQUIRED',
2731
+ 'AI_CHAT_TRANSACTION_SETTLED'
2732
+ ].includes(error?.code);
2733
+ }
2734
+
2735
+ function restoreRejectedStructuralDraft(messageId,operationId,text){
2736
+ const response=[...chatOutput.children].find(
2737
+ candidate=>candidate.id===`message-${messageId}`
2738
+ )||null;
2739
+ const request=[...chatOutput.children].find(
2740
+ candidate=>candidate.dataset.operationId===operationId
2741
+ )||null;
2742
+ response?.remove();
2743
+ request?.remove();
2744
+ const existing=textArea.value;
2745
+ if(existing!==text){
2746
+ textArea.value=existing
2747
+ ?`${text}\n${existing}`
2748
+ :text;
2749
+ }
2750
+ textArea.style.height='auto';
2751
+ textArea.style.height=`${textArea.scrollHeight}px`;
2752
+ scrollTranscriptToBottom();
2753
+ return Boolean(response||request);
2754
+ }
2755
+
2756
+ async function sendMessageThroughBoundSession(
2757
+ text,
2758
+ context,
2759
+ sessionRequestMessage={content:text,role:'user'},
2760
+ perTurnRequest=null
2761
+ ){
2762
+ const session=boundChatSession;
2763
+ const bindingGeneration=sessionBindingGeneration;
2764
+ if(!session){
2765
+ throw chatError(
2766
+ 'The chat session is not bound.',
2767
+ chatErrorCodes.sessionBindingRejected
2768
+ );
2769
+ }
2770
+ if(sessionMessagePending){
2771
+ throw chatError(
2772
+ 'A chat session message is already active.',
2773
+ 'AI_CHAT_BUSY'
2774
+ );
2775
+ }
2776
+
2777
+ const request={
2778
+ message:sessionRequestMessage,
2779
+ signal:context.signal,
2780
+ ...(perTurnRequest===null?{}:{request:perTurnRequest})
2781
+ };
2782
+ const previousPendingToolCall=pendingStructuralToolCall;
2783
+ let streamedStructuralToolCall=null;
2784
+ const messageId=`session-${++sessionMessageSequence}`;
2785
+ const sessionMessageToken=Symbol(messageId);
2786
+ activeSessionMessageToken=sessionMessageToken;
2787
+ sessionMessagePending=true;
2788
+ applyAIAvailability(host.aiAvailability);
2789
+ setSessionStatus('sending','AI is responding…');
2790
+ try{
2791
+ await receivedMessage(host.aiName,'',messageId);
2792
+ await streamMessage('',messageId,true);
2793
+ setMessageProgress(
2794
+ messageId,
2795
+ {
2796
+ label:'AI response',
2797
+ indeterminate:true,
2798
+ status:'Working'
2799
+ }
2800
+ );
2801
+ const result=typeof session.stream==='function'
2802
+ ?await session.stream(
2803
+ request,
2804
+ {
2805
+ onChunk:async function renderSessionStreamChunk(chunk,requestId,isThinking){
2806
+ if(
2807
+ destroyed
2808
+ ||bindingGeneration!==sessionBindingGeneration
2809
+ ||session!==boundChatSession
2810
+ ) return false;
2811
+ return streamMessage(chunk,messageId,isThinking);
2812
+ },
2813
+ onToolCall:async function renderSessionToolCall(call){
2814
+ if(
2815
+ destroyed
2816
+ ||bindingGeneration!==sessionBindingGeneration
2817
+ ||session!==boundChatSession
2818
+ ) return false;
2819
+ const message=[...chatOutput.children].find(
2820
+ candidate=>candidate.id===`message-${messageId}`
2821
+ )||null;
2822
+ if(!message) return false;
2823
+ const normalized=normalizeVisibleToolCalls([call])[0];
2824
+ if(
2825
+ streamedStructuralToolCall
2826
+ &&!sameStructuralToolCall(streamedStructuralToolCall,normalized)
2827
+ ){
2828
+ throw chatError(
2829
+ 'The streamed structural tool call changed before completion.',
2830
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
2831
+ );
2832
+ }
2833
+ if(streamedStructuralToolCall){
2834
+ return true;
2835
+ }
2836
+ streamedStructuralToolCall=normalized;
2837
+ setPendingStructuralToolCall(normalized);
2838
+ appendVisibleToolCall(message,normalized);
2839
+ setSessionStatus('tool',pendingStructuralToolMessage);
2840
+ return true;
2841
+ }
2842
+ }
2843
+ )
2844
+ :await session.send(request);
2845
+ if(
2846
+ destroyed
2847
+ ||bindingGeneration!==sessionBindingGeneration
2848
+ ||session!==boundChatSession
2849
+ ){
2850
+ return false;
2851
+ }
2852
+ if(!result||typeof result!=='object'||Array.isArray(result)){
2853
+ throw new TypeError('The chat session returned an invalid result.');
2854
+ }
2855
+ if(!result.message||typeof result.message.content!=='string'){
2856
+ throw new TypeError('The chat session result must contain assistant message content.');
2857
+ }
2858
+ const committedTurn=typeof session.transcript==='function'
2859
+ ?latestCommittedTranscriptTurn(
2860
+ await session.transcript(),
2861
+ sessionRequestMessage
2862
+ )
2863
+ :null;
2864
+ const message=[...chatOutput.children].find(
2865
+ candidate=>candidate.id===`message-${messageId}`
2866
+ )||null;
2867
+ if(!message){
2868
+ throw new Error('The active chat response card is unavailable.');
2869
+ }
2870
+ setTranscriptMessageContent(
2871
+ message,
2872
+ result.message.content,
2873
+ committedTurn?.response?.timestamp??result.message.timestamp
2874
+ );
2875
+ const requestMessage=[...chatOutput.children].find(
2876
+ candidate=>candidate.dataset.operationId===context.operationId
2877
+ )||null;
2878
+ setTranscriptMessageTimestamp(
2879
+ requestMessage,
2880
+ committedTurn?.request?.timestamp
2881
+ );
2882
+ const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
2883
+ if(terminalToolCalls.length>1){
2884
+ throw chatError(
2885
+ 'The chat session returned parallel structural tool calls.',
2886
+ 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
2887
+ );
2888
+ }
2889
+ if(
2890
+ streamedStructuralToolCall
2891
+ &&(
2892
+ terminalToolCalls.length!==1
2893
+ ||!sameStructuralToolCall(
2894
+ streamedStructuralToolCall,
2895
+ terminalToolCalls[0]
2896
+ )
2897
+ )
2898
+ ){
2899
+ throw chatError(
2900
+ 'The terminal structural tool call does not match the streamed call.',
2901
+ 'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
2902
+ );
2903
+ }
2904
+ if(!streamedStructuralToolCall&&terminalToolCalls.length){
2905
+ appendVisibleToolCall(message,terminalToolCalls[0]);
2906
+ }
2907
+ setPendingStructuralToolCall(
2908
+ terminalToolCalls.length?terminalToolCalls.at(-1):null
2909
+ );
2910
+ setSessionStatus(
2911
+ pendingStructuralToolMessage?'tool':'ready',
2912
+ pendingStructuralToolMessage||'Chat ready.'
2913
+ );
2914
+ const detail={
2915
+ session,
2916
+ request,
2917
+ result,
2918
+ message:result.message,
2919
+ context
2920
+ };
2921
+ if(activeSessionMessageToken===sessionMessageToken){
2922
+ activeSessionMessageToken=null;
2923
+ sessionMessagePending=false;
2924
+ applyAIAvailability(host.aiAvailability);
2925
+ }
2926
+ dispatchChatEvent(
2927
+ 'chat-session-message',
2928
+ detail,
2929
+ {
2930
+ bubbles:true,
2931
+ composed:true,
2932
+ operationId:context.operationId||null,
2933
+ publicDetail:{
2934
+ ...detail,
2935
+ reason:chatReasons.sessionMessageCompleted
2936
+ }
2937
+ }
2938
+ );
2939
+ return result;
2940
+ }catch(error){
2941
+ if(!destroyed&&bindingGeneration===sessionBindingGeneration){
2942
+ setPendingStructuralToolCall(previousPendingToolCall);
2943
+ }
2944
+ if(
2945
+ !destroyed
2946
+ &&bindingGeneration===sessionBindingGeneration
2947
+ &&internalStructuralToolFailure(error)
2948
+ ){
2949
+ if(sessionRequestMessage.role==='tool'){
2950
+ const response=[...chatOutput.children].find(
2951
+ candidate=>candidate.id===`message-${messageId}`
2952
+ )||null;
2953
+ const requestMessage=[...chatOutput.children].find(
2954
+ candidate=>candidate.dataset.operationId===context.operationId
2955
+ )||null;
2956
+ response?.remove();
2957
+ requestMessage?.remove();
2958
+ scrollTranscriptToBottom();
2959
+ }else{
2960
+ restoreRejectedStructuralDraft(messageId,context.operationId,text);
2961
+ }
2962
+ console.error('Arcane structural tool protocol failure.',error);
2963
+ setSessionStatus(
2964
+ pendingStructuralToolCall?'tool':'ready',
2965
+ pendingStructuralToolMessage||'Chat ready.'
2966
+ );
2967
+ }else if(!destroyed&&bindingGeneration===sessionBindingGeneration){
2968
+ console.error('Arcane chat request failed.',error);
2969
+ if(sessionRequestMessage.role==='tool'){
2970
+ const requestMessage=[...chatOutput.children].find(
2971
+ candidate=>candidate.dataset.operationId===context.operationId
2972
+ )||null;
2973
+ requestMessage?.remove();
2974
+ }
2975
+ renderSessionMessageFailure(messageId,error);
2976
+ setSessionStatus(
2977
+ 'error',
2978
+ visibleErrorMessage(error,'The AI request failed.')
2979
+ );
2980
+ dispatchChatEvent(
2981
+ 'chat-session-error',
2982
+ {phase:'message',session,request,context,error},
2983
+ {
2984
+ bubbles:true,
2985
+ composed:true,
2986
+ operationId:context.operationId||null,
2987
+ publicDetail:{
2988
+ phase:'message',
2989
+ session,
2990
+ request,
2991
+ context,
2992
+ reason:chatReasons.sessionMessageRejected,
2993
+ ...publicErrorFields(
2994
+ error,
2995
+ chatErrorCodes.sessionMessageRejected
2996
+ )
2997
+ }
2998
+ }
2999
+ );
3000
+ }
3001
+ throw error;
3002
+ }finally{
3003
+ if(activeSessionMessageToken===sessionMessageToken){
3004
+ activeSessionMessageToken=null;
3005
+ sessionMessagePending=false;
3006
+ }
3007
+ if(!destroyed&&activeSessionMessageToken===null){
3008
+ applyAIAvailability(host.aiAvailability);
3009
+ }
3010
+ }
3011
+ }
3012
+
3013
+ async function submitToolResult(options={},context={}){
3014
+ if(destroyed||host.conversationComplete){
3015
+ return false;
3016
+ }
3017
+ if(!isPlainRecord(options)||!isPlainRecord(context)){
3018
+ throw new TypeError('Tool-result options and context must be plain objects.');
3019
+ }
3020
+ const unsupportedOption=Object.keys(options).find(
3021
+ key=>!['disposition','message','persist','request','toolCallId'].includes(key)
3022
+ );
3023
+ if(unsupportedOption){
3024
+ throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
3025
+ }
3026
+ const unsupportedContext=Object.keys(context).find(
3027
+ key=>!['operationId','signal'].includes(key)
3028
+ );
3029
+ if(unsupportedContext){
3030
+ throw new TypeError(`Unsupported tool-result context: ${unsupportedContext}.`);
3031
+ }
3032
+ if(!boundChatSession||sessionBindingPending||sessionMessagePending){
3033
+ return false;
3034
+ }
3035
+ if(!host.aiAvailability.llm){
3036
+ return false;
3037
+ }
3038
+ if(!pendingStructuralToolCall){
3039
+ const error=new TypeError('There is no pending structural tool call to settle.');
3040
+ error.code='AI_CHAT_TOOL_RESULT_NOT_PENDING';
3041
+ throw error;
3042
+ }
3043
+ const disposition=options.disposition;
3044
+ const dispositions=new Map([
3045
+ ['executed','Executed'],
3046
+ ['declined','Declined'],
3047
+ ['cancelled','Cancelled'],
3048
+ ['not-executed','Not executed']
3049
+ ]);
3050
+ if(!dispositions.has(disposition)){
3051
+ throw new TypeError(
3052
+ 'Tool-result disposition must be executed, declined, cancelled, or not-executed.'
3053
+ );
3054
+ }
3055
+ if(typeof options.message!=='string'||!options.message.trim()){
3056
+ throw new TypeError('Tool-result message must contain user-facing text.');
3057
+ }
3058
+ const perTurnRequest=options.request??null;
3059
+ if(perTurnRequest!==null&&!isPlainRecord(perTurnRequest)){
3060
+ throw new TypeError('Tool-result request options must be a plain object.');
3061
+ }
3062
+ const managedRequestField=perTurnRequest&&Object.keys(perTurnRequest).find(
3063
+ key=>['messages','onChunk','onResponse','onToolCall','signal','stream'].includes(key)
3064
+ );
3065
+ if(managedRequestField){
3066
+ throw new TypeError(
3067
+ `Tool-result request.${managedRequestField} is managed by the chat session.`
3068
+ );
3069
+ }
3070
+ const toolCallId=options.toolCallId??pendingStructuralToolCall.id;
3071
+ if(typeof toolCallId!=='string'||toolCallId!==pendingStructuralToolCall.id){
3072
+ const error=new TypeError('Tool-result ID does not match the pending structural tool call.');
3073
+ error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
3074
+ throw error;
3075
+ }
3076
+ const persist=options.persist??true;
3077
+ if(typeof persist!=='boolean'){
3078
+ throw new TypeError('Tool-result persist must be boolean.');
3079
+ }
3080
+
3081
+ const text=`${dispositions.get(disposition)} — ${options.message}`;
3082
+ const operationId=typeof context.operationId==='string'&&context.operationId
3083
+ ?context.operationId
3084
+ :nextChatOperationId('tool-result');
3085
+ const ownership=createChatSubmissionOwnership(context.signal??null);
3086
+ const eventContext={
3087
+ source:'tool',
3088
+ synthetic:true,
3089
+ operationId,
3090
+ signal:ownership.signal
3091
+ };
3092
+ if(ownership.signal.aborted){
3093
+ ownership.release();
3094
+ return false;
3095
+ }
3096
+ try{
3097
+ const visibleMessage=appendTranscriptMessage('tool',text,'Tool');
3098
+ visibleMessage.dataset.operationId=operationId;
3099
+ const result=sendMessageThroughBoundSession(
3100
+ text,
3101
+ eventContext,
3102
+ {
3103
+ content:text,
3104
+ persist,
3105
+ role:'tool',
3106
+ tool_call_id:toolCallId
3107
+ },
3108
+ perTurnRequest
3109
+ );
3110
+ return observeHostSubmission(result,eventContext,ownership);
3111
+ }catch(error){
3112
+ ownership.release();
3113
+ throw error;
3114
+ }
1902
3115
  }
1903
3116
 
1904
3117
  function observeHostSubmission(result,context,ownership){
@@ -1913,6 +3126,9 @@
1913
3126
  if(destroyed||context.signal.aborted){
1914
3127
  return false;
1915
3128
  }
3129
+ if(internalStructuralToolFailure(error)){
3130
+ return false;
3131
+ }
1916
3132
  dispatchChatEvent(
1917
3133
  'chat-send-error',
1918
3134
  {error,context:{...context}},
@@ -1930,7 +3146,7 @@
1930
3146
  }
1931
3147
  }
1932
3148
  );
1933
- console.error('The chat message could not be submitted.');
3149
+ console.error('The chat message could not be submitted.',error);
1934
3150
  return false;
1935
3151
  }
1936
3152
  ).finally(
@@ -1957,12 +3173,15 @@
1957
3173
  if(!host.aiAvailability.llm){
1958
3174
  return false;
1959
3175
  }
1960
- const submissionContext=Object.freeze({
3176
+ if(sessionBindingPending||sessionMessagePending||pendingStructuralToolMessage){
3177
+ return false;
3178
+ }
3179
+ const submissionContext={
1961
3180
  source:'user',
1962
3181
  preserveDraft:false,
1963
3182
  synthetic:false,
1964
3183
  ...context
1965
- });
3184
+ };
1966
3185
  const text=textOverride.length>0?textOverride:textArea.value;
1967
3186
  if(!text.trim()){
1968
3187
  return false;
@@ -1972,13 +3191,11 @@
1972
3191
  ?submissionContext.operationId
1973
3192
  :nextChatOperationId('message');
1974
3193
  const ownership=createChatSubmissionOwnership(submissionContext.signal??null);
1975
- const eventContext=Object.freeze(
1976
- {
1977
- ...submissionContext,
1978
- operationId,
1979
- signal:ownership.signal
1980
- }
1981
- );
3194
+ const eventContext={
3195
+ ...submissionContext,
3196
+ operationId,
3197
+ signal:ownership.signal
3198
+ };
1982
3199
  if(ownership.signal.aborted){
1983
3200
  ownership.release();
1984
3201
  return false;
@@ -1997,7 +3214,8 @@
1997
3214
  cancelable:true,
1998
3215
  operationId,
1999
3216
  publicDetail:{
2000
- messageLength:text.length,
3217
+ message:text,
3218
+ context:eventContext,
2001
3219
  source:eventContext.source,
2002
3220
  synthetic:eventContext.synthetic===true,
2003
3221
  reason:chatReasons.messageSubmissionRequested
@@ -2022,7 +3240,6 @@
2022
3240
  return false;
2023
3241
  }
2024
3242
  try{
2025
- const messageTime = getMilTime();
2026
3243
  if(!eventContext.preserveDraft){
2027
3244
  textArea.value='';
2028
3245
  }
@@ -2031,32 +3248,28 @@
2031
3248
  // console.log(forceText,text,1);
2032
3249
  if(!eventContext.synthetic&&!eventContext.reuseVisibleMessage){
2033
3250
  hostSubmissionGeneration++;
2034
- chatOutput.innerHTML += `
2035
- <li class='user'>
2036
- <strong>${host.name} ${messageTime}:</strong>
2037
- <br>
2038
- <br>
2039
- <div class='markdown'>
2040
- ${new MD(text).rendered}
2041
- </div>
2042
- </li>
2043
- `;
2044
- }
2045
- chatOutput.scrollTop = chatOutput.scrollHeight;
3251
+ const visibleMessage=appendTranscriptMessage('user',text,host.name);
3252
+ visibleMessage.dataset.operationId=operationId;
3253
+ }
3254
+ scrollTranscriptToBottom();
2046
3255
 
2047
3256
  if(host.aiAvailability.tts&&speech.muted===false){
2048
- const aiRuntime=globalThis.ai;
3257
+ const aiRuntime=boundChatAI??globalThis.ai;
2049
3258
  aiRuntime?.stopAudio?.();
2050
3259
  if(typeof aiRuntime?.resumeAudio==='function'){
2051
3260
  void Promise.resolve(aiRuntime.resumeAudio()).catch(
2052
- reportTTSError
3261
+ function reportPlaybackResumeFailure(error){
3262
+ reportTTSError(error,'playback-resume');
3263
+ }
2053
3264
  );
2054
3265
  }
2055
3266
  }
2056
3267
 
2057
3268
  let result;
2058
3269
  try{
2059
- result=host.sendMessage(text,eventContext);
3270
+ result=boundChatSession
3271
+ ?sendMessageThroughBoundSession(text,eventContext)
3272
+ :host.sendMessage(text,eventContext);
2060
3273
  }catch(error){
2061
3274
  result=Promise.reject(error);
2062
3275
  }
@@ -2068,45 +3281,46 @@
2068
3281
  }
2069
3282
 
2070
3283
  async function receivedMessage(name = 'Assistant', text = '', id=''){
2071
- const messageTime = getMilTime();
2072
- //console.log(text)
2073
- chatOutput.innerHTML += `
2074
- <li class='ai' ${id? `id="message-${id}"`:''}>
2075
- <strong>${name} ${messageTime}:</strong>
2076
- <br>
2077
- <br>
2078
- <div class='markdown'>
2079
- ${new MD(text).rendered}
2080
- </div>
2081
- </li>
2082
- `;
2083
- chatOutput.scrollTop = chatOutput.scrollHeight;
2084
- }
2085
-
2086
- function reportTTSError(error){
3284
+ const message=appendTranscriptMessage('assistant',text,name);
3285
+ if(id){
3286
+ message.id=`message-${id}`;
3287
+ }
3288
+ scrollTranscriptToBottom();
3289
+ return message;
3290
+ }
3291
+
3292
+ function reportTTSError(error,boundary='synthesis'){
2087
3293
  if(destroyed){
2088
3294
  return false;
2089
3295
  }
2090
3296
  if(typeof speech.reportTTSError==='function'){
2091
3297
  try{
2092
- speech.reportTTSError(error);
3298
+ speech.reportTTSError(error,boundary);
2093
3299
  return true;
2094
3300
  }catch(reportingError){
2095
3301
  error=reportingError;
2096
3302
  }
2097
3303
  }
2098
3304
 
2099
- const message=typeof error?.message==='string'&&error.message.trim()
2100
- ?error.message.trim().slice(0,240)
2101
- :'Speech synthesis failed.';
3305
+ const message=visibleErrorMessage(
3306
+ error,
3307
+ boundary==='decode'
3308
+ ?'Speech audio could not be decoded.'
3309
+ :boundary==='playback-start'
3310
+ ?'Speech playback could not start.'
3311
+ :boundary==='playback-resume'
3312
+ ?'Speech playback could not resume.'
3313
+ :'Speech synthesis failed.'
3314
+ );
2102
3315
  dispatchChatEvent(
2103
3316
  'chat-speech-synthesis-error',
2104
- {error,message},
3317
+ {boundary,error,message},
2105
3318
  {
2106
3319
  bubbles:true,
2107
3320
  composed:true,
2108
3321
  operationId:nextChatOperationId('speech-synthesis'),
2109
3322
  publicDetail:{
3323
+ boundary,
2110
3324
  reason:chatReasons.speechSynthesisRejected,
2111
3325
  ...publicErrorFields(
2112
3326
  error,
@@ -2115,14 +3329,16 @@
2115
3329
  }
2116
3330
  }
2117
3331
  );
2118
- console.error('Speech synthesis failed.');
3332
+ console.error('Speech synthesis failed.',error);
2119
3333
  return false;
2120
3334
  }
2121
3335
 
2122
3336
  function destroy(){
2123
3337
  if(destroyed){
2124
- return;
3338
+ return false;
2125
3339
  }
3340
+ sessionBindingGeneration+=1;
3341
+ setSessionStatus('destroyed','Chat closed.');
2126
3342
  destroyed=true;
2127
3343
  const destroyReason=createChatSubmissionAbort(
2128
3344
  chatReasons.componentDestroyed,
@@ -2138,9 +3354,17 @@
2138
3354
  conversationTimeboxUnsubscribe=null;
2139
3355
  conversationTimeboxController=null;
2140
3356
  initialSpeechMuteListenerInstalled=false;
3357
+ sessionBindingPending=false;
3358
+ sessionMessagePending=false;
3359
+ activeSessionMessageToken=null;
3360
+ setPendingStructuralToolCall();
3361
+ boundChatSession=null;
3362
+ boundChatAI=null;
3363
+ host.session=null;
2141
3364
  speech.destroy?.();
2142
3365
  events.dispose();
2143
3366
  host.ready=false;
3367
+ return true;
2144
3368
  }
2145
3369
 
2146
3370
  // Initialize and adjust on input