@aparte/core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +20 -6
  2. package/dist/client/aparte-client.d.ts +113 -47
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/client/lifecycle-events.d.ts +55 -0
  5. package/dist/client/lifecycle-events.d.ts.map +1 -0
  6. package/dist/client/stream-adapter.d.ts +11 -5
  7. package/dist/client/stream-adapter.d.ts.map +1 -1
  8. package/dist/client/xml-artifact-feed.d.ts +76 -0
  9. package/dist/client/xml-artifact-feed.d.ts.map +1 -0
  10. package/dist/components/bubble/aparte-chat-bubble.d.ts +35 -8
  11. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  12. package/dist/components/chat/aparte-chat.d.ts +0 -6
  13. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  14. package/dist/components/composer/aparte-composer-action.d.ts +18 -3
  15. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  16. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  17. package/dist/components/composer/aparte-composer-attachments.d.ts +1 -1
  18. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  19. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  20. package/dist/components/composer/aparte-composer-input.d.ts +0 -2
  21. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  22. package/dist/components/composer/aparte-composer-send.d.ts +5 -0
  23. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  24. package/dist/components/composer/aparte-composer-toolbar.d.ts +4 -0
  25. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  26. package/dist/components/composer/aparte-composer.d.ts +64 -6
  27. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  28. package/dist/components/composer/index.d.ts +1 -1
  29. package/dist/components/composer/index.d.ts.map +1 -1
  30. package/dist/components/conversation-list/aparte-conversation-list.d.ts +2 -5
  31. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  32. package/dist/components/conversation-list/index.d.ts +1 -1
  33. package/dist/components/conversation-list/index.d.ts.map +1 -1
  34. package/dist/components/elicitation/aparte-elicitation.d.ts +63 -3
  35. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  36. package/dist/components/index.d.ts +2 -2
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/status/aparte-chat-status.d.ts +8 -6
  39. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  40. package/dist/components/viewport/aparte-chat-viewport.d.ts +35 -9
  41. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  42. package/dist/config/action-provider.d.ts +4 -4
  43. package/dist/config/aparte-config.d.ts +124 -41
  44. package/dist/config/aparte-config.d.ts.map +1 -1
  45. package/dist/config/attachment-renderer.d.ts +1 -1
  46. package/dist/config/avatar-provider.d.ts +1 -1
  47. package/dist/config/config-context.d.ts +66 -11
  48. package/dist/config/config-context.d.ts.map +1 -1
  49. package/dist/config/elicitation-field-renderer.d.ts +73 -0
  50. package/dist/config/elicitation-field-renderer.d.ts.map +1 -0
  51. package/dist/config/error-renderer.d.ts +1 -1
  52. package/dist/config/icon-provider.d.ts +2 -2
  53. package/dist/config/icon-provider.d.ts.map +1 -1
  54. package/dist/config/index.d.ts +7 -5
  55. package/dist/config/index.d.ts.map +1 -1
  56. package/dist/config/locale.d.ts +43 -2
  57. package/dist/config/locale.d.ts.map +1 -1
  58. package/dist/config/sanitize.d.ts +1 -7
  59. package/dist/config/sanitize.d.ts.map +1 -1
  60. package/dist/config/sibling-nav-renderer.d.ts +1 -1
  61. package/dist/config/skeleton-provider.d.ts +2 -2
  62. package/dist/config/skeleton-provider.d.ts.map +1 -1
  63. package/dist/config/status-renderer.d.ts +1 -1
  64. package/dist/conversations/conversation-controller.d.ts +19 -5
  65. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  66. package/dist/conversations/conversation-manager.d.ts +2 -2
  67. package/dist/conversations/conversation-manager.d.ts.map +1 -1
  68. package/dist/conversations/index.d.ts +1 -1
  69. package/dist/conversations/index.d.ts.map +1 -1
  70. package/dist/conversations/types.d.ts +1 -1
  71. package/dist/custom-elements.json +7962 -6175
  72. package/dist/elicitation/index.d.ts +1 -1
  73. package/dist/elicitation/panel.d.ts +32 -0
  74. package/dist/elicitation/panel.d.ts.map +1 -1
  75. package/dist/elicitation/types.d.ts +29 -2
  76. package/dist/elicitation/types.d.ts.map +1 -1
  77. package/dist/host/aparte-chat-host.d.ts +3 -3
  78. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  79. package/dist/{index-Dn7TH14G.js → index-BieFbKeW.js} +1795 -1238
  80. package/dist/index-BieFbKeW.js.map +1 -0
  81. package/dist/index.css +311 -21
  82. package/dist/index.d.ts +21 -13
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +447 -200
  85. package/dist/index.js.map +1 -1
  86. package/dist/index.node.d.ts +22 -13
  87. package/dist/index.node.d.ts.map +1 -1
  88. package/dist/index.node.js +54 -46
  89. package/dist/index.node.js.map +1 -1
  90. package/dist/interop/element-props.d.ts +1 -1
  91. package/dist/interop/element-props.d.ts.map +1 -1
  92. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  93. package/dist/primitives/index.d.ts +1 -1
  94. package/dist/primitives/index.d.ts.map +1 -1
  95. package/dist/primitives/select/aparte-optgroup.d.ts +15 -4
  96. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  97. package/dist/primitives/select/aparte-option.d.ts +0 -5
  98. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  99. package/dist/primitives/select/aparte-select.d.ts +17 -5
  100. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  101. package/dist/primitives/select/index.d.ts +1 -1
  102. package/dist/primitives/select/index.d.ts.map +1 -1
  103. package/dist/renderers/markdown-stream.d.ts +42 -0
  104. package/dist/renderers/markdown-stream.d.ts.map +1 -0
  105. package/dist/renderers/segment-renderers.d.ts +20 -14
  106. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  107. package/dist/renderers/segments/artifact/binary-file.d.ts +45 -0
  108. package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -0
  109. package/dist/renderers/segments/artifact/card.d.ts +3 -0
  110. package/dist/renderers/segments/artifact/card.d.ts.map +1 -0
  111. package/dist/renderers/segments/artifact/preview-document.d.ts +27 -0
  112. package/dist/renderers/segments/artifact/preview-document.d.ts.map +1 -0
  113. package/dist/renderers/segments/artifact/shared.d.ts +12 -0
  114. package/dist/renderers/segments/artifact/shared.d.ts.map +1 -0
  115. package/dist/renderers/segments/code.d.ts +3 -0
  116. package/dist/renderers/segments/code.d.ts.map +1 -0
  117. package/dist/renderers/segments/error.d.ts +3 -0
  118. package/dist/renderers/segments/error.d.ts.map +1 -0
  119. package/dist/renderers/segments/file-tree.d.ts +3 -0
  120. package/dist/renderers/segments/file-tree.d.ts.map +1 -0
  121. package/dist/renderers/segments/pipeline-waiting.d.ts +3 -0
  122. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -0
  123. package/dist/renderers/segments/progress.d.ts +3 -0
  124. package/dist/renderers/segments/progress.d.ts.map +1 -0
  125. package/dist/renderers/segments/terminal.d.ts +3 -0
  126. package/dist/renderers/segments/terminal.d.ts.map +1 -0
  127. package/dist/renderers/segments/text.d.ts +10 -0
  128. package/dist/renderers/segments/text.d.ts.map +1 -0
  129. package/dist/renderers/segments/thinking.d.ts +3 -0
  130. package/dist/renderers/segments/thinking.d.ts.map +1 -0
  131. package/dist/renderers/segments/tool-call.d.ts +3 -0
  132. package/dist/renderers/segments/tool-call.d.ts.map +1 -0
  133. package/dist/runtime/message-repository.d.ts +3 -3
  134. package/dist/runtime/message-repository.d.ts.map +1 -1
  135. package/dist/transport/backend-handler.d.ts +19 -7
  136. package/dist/transport/backend-handler.d.ts.map +1 -1
  137. package/dist/transport/backend-transport.d.ts +1 -1
  138. package/dist/transport/backend-transport.d.ts.map +1 -1
  139. package/dist/transport/direct-transport.d.ts +4 -4
  140. package/dist/transport/direct-transport.d.ts.map +1 -1
  141. package/dist/transport/index.d.ts +2 -2
  142. package/dist/transport/index.d.ts.map +1 -1
  143. package/dist/transport/types.d.ts +3 -3
  144. package/dist/types/element-map.d.ts +69 -0
  145. package/dist/types/element-map.d.ts.map +1 -0
  146. package/dist/types/event-map.d.ts +84 -23
  147. package/dist/types/event-map.d.ts.map +1 -1
  148. package/dist/types/events.d.ts +206 -64
  149. package/dist/types/events.d.ts.map +1 -1
  150. package/dist/types/index.d.ts +4 -5
  151. package/dist/types/index.d.ts.map +1 -1
  152. package/dist/types/model-provider.d.ts +100 -54
  153. package/dist/types/model-provider.d.ts.map +1 -1
  154. package/dist/types/models.d.ts +29 -7
  155. package/dist/types/models.d.ts.map +1 -1
  156. package/dist/types/segments.d.ts +1 -8
  157. package/dist/types/segments.d.ts.map +1 -1
  158. package/dist/types/theming.d.ts +25 -37
  159. package/dist/types/theming.d.ts.map +1 -1
  160. package/dist/types/tools.d.ts +60 -15
  161. package/dist/types/tools.d.ts.map +1 -1
  162. package/dist/utils/escape.d.ts +46 -0
  163. package/dist/utils/escape.d.ts.map +1 -0
  164. package/dist/utils/files-to-attachments.d.ts +18 -1
  165. package/dist/utils/files-to-attachments.d.ts.map +1 -1
  166. package/dist/utils/uuid.d.ts +26 -0
  167. package/dist/utils/uuid.d.ts.map +1 -0
  168. package/package.json +3 -5
  169. package/dist/index-Dn7TH14G.js.map +0 -1
  170. package/dist/types/providers.d.ts +0 -93
  171. package/dist/types/providers.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { R as resolveConfig, S as runWithConfig, F as getSegmentRenderer, U as installDefaultRenderersOnce, M as MessageRepository, G as isAwaitingReply, L as populateBubbleFromMessage, d as AparteConfig, r as buildElicitationPanel } from "./index-Dn7TH14G.js";
2
- import { A, a, b, c, e, f, g, h, i, B, C, D, j, k, l, m, n, o, p, q, s, t, u, v, w, x, y, z, E, H, I, J, K, N, O, P, Q, T } from "./index-Dn7TH14G.js";
1
+ import { H as escapeAttr, X as resolveConfig, C as cssEscape, Z as runWithConfig, I as escapeHtml, L as getSegmentRenderer, M as installDefaultRenderersOnce, q as AparteMessageRepository, N as isAwaitingReply, S as populateBubbleFromMessage, $ as uuid, Y as revokeAttachmentUrls, v as buildElicitationPanel } from "./index-BieFbKeW.js";
2
+ import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, r, s, t, u, w, x, y, z, B, D, E, F, G, J, K, O, P, Q, R, T, U, V, W, _ } from "./index-BieFbKeW.js";
3
3
  class AparteOption extends HTMLElement {
4
4
  static get observedAttributes() {
5
5
  return ["value", "disabled", "selected", "data-status"];
@@ -192,6 +192,22 @@ class AparteSelect extends HTMLElement {
192
192
  _searchInput = null;
193
193
  _observer = null;
194
194
  // Bound handlers for cleanup
195
+ /**
196
+ * Bound, like its two neighbours below — because an inline arrow on `this` can
197
+ * never be removed.
198
+ *
199
+ * `_setupEventListeners()` runs on EVERY `connectedCallback`, and `_render()`'s
200
+ * idempotency guard means the host element survives a re-connect. So each move
201
+ * of the element (a portal, a Vue teleport, any framework re-parent) added
202
+ * another option-click listener: measured, one click fired the change handler
203
+ * FIVE times after five re-connects. `disconnectedCallback` removed the two
204
+ * document-level handlers and could not touch this one.
205
+ *
206
+ * This is verbatim the bug class `aparte-chat-viewport` documents having fixed
207
+ * for its own listeners; a repo sweep found this as the last inline-arrow
208
+ * listener on a persistent node.
209
+ */
210
+ _boundHandleOptionClick = this._handleOptionClick.bind(this);
195
211
  _boundHandleDocumentClick = this._handleDocumentClick.bind(this);
196
212
  _boundHandleKeydown = this._handleKeydown.bind(this);
197
213
  static get observedAttributes() {
@@ -205,6 +221,7 @@ class AparteSelect extends HTMLElement {
205
221
  this._setupMutationObserver();
206
222
  }
207
223
  disconnectedCallback() {
224
+ this.removeEventListener("click", this._boundHandleOptionClick);
208
225
  document.removeEventListener("click", this._boundHandleDocumentClick);
209
226
  document.removeEventListener("keydown", this._boundHandleKeydown);
210
227
  this._observer?.disconnect();
@@ -368,12 +385,8 @@ class AparteSelect extends HTMLElement {
368
385
  this._openDropdown();
369
386
  }
370
387
  });
371
- this.addEventListener("click", (e2) => {
372
- const option = e2.target.closest("aparte-option");
373
- if (option && !option.hasAttribute("disabled")) {
374
- this._selectOption(option);
375
- }
376
- });
388
+ this.removeEventListener("click", this._boundHandleOptionClick);
389
+ this.addEventListener("click", this._boundHandleOptionClick);
377
390
  this._searchInput?.addEventListener("input", (e2) => {
378
391
  const query = e2.target.value.toLowerCase();
379
392
  this._filterOptions(query);
@@ -381,6 +394,12 @@ class AparteSelect extends HTMLElement {
381
394
  document.addEventListener("click", this._boundHandleDocumentClick);
382
395
  document.addEventListener("keydown", this._boundHandleKeydown);
383
396
  }
397
+ _handleOptionClick(e2) {
398
+ const option = e2.target.closest("aparte-option");
399
+ if (option && !option.hasAttribute("disabled")) {
400
+ this._selectOption(option);
401
+ }
402
+ }
384
403
  _handleDocumentClick(e2) {
385
404
  if (!this.contains(e2.target)) {
386
405
  this._closeDropdown();
@@ -660,7 +679,7 @@ class AparteChat extends HTMLElement {
660
679
  if (this.hasAttribute("framework-managed")) return;
661
680
  if (this.querySelector("aparte-chat-viewport")) return;
662
681
  const placeholder = this.getAttribute("placeholder");
663
- const composerAttrs = (placeholder !== null ? ` placeholder="${this._escapeAttr(placeholder)}"` : "") + (this.hasAttribute("disabled") ? " disabled" : "");
682
+ const composerAttrs = (placeholder !== null ? ` placeholder="${escapeAttr(placeholder)}"` : "") + (this.hasAttribute("disabled") ? " disabled" : "");
664
683
  const attachments = this.hasAttribute("attachments");
665
684
  this.innerHTML = `
666
685
  <aparte-chat-viewport></aparte-chat-viewport>
@@ -723,27 +742,21 @@ class AparteChat extends HTMLElement {
723
742
  const empty = !viewport || !viewport.querySelector("aparte-chat-bubble");
724
743
  this.toggleAttribute("data-empty", empty);
725
744
  }
726
- _escapeAttr(value) {
727
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
728
- }
729
745
  }
730
746
  if (!customElements.get("aparte-chat")) {
731
747
  customElements.define("aparte-chat", AparteChat);
732
748
  }
733
- function cssEscape(value) {
734
- return value.replace(/["\\]/g, "\\$&");
735
- }
736
749
  let _warnedNoRenderer = false;
737
750
  function warnMissingRenderer(type) {
738
751
  if (_warnedNoRenderer) return;
739
752
  _warnedNoRenderer = true;
740
753
  console.warn(`[aparte] No renderer for segment "${type}". Register one with registerSegmentRenderer({ type: '${type}', render }) from @aparte/core — see https://apartejs.dev/guides/customization/#custom-segment-types`);
741
754
  }
742
- function resolveSegmentRenderer(type) {
743
- const renderer = getSegmentRenderer(type);
755
+ function resolveSegmentRenderer(type, config) {
756
+ const renderer = getSegmentRenderer(type, config);
744
757
  if (renderer) return renderer;
745
- installDefaultRenderersOnce();
746
- return getSegmentRenderer(type);
758
+ installDefaultRenderersOnce(config);
759
+ return getSegmentRenderer(type, config);
747
760
  }
748
761
  function segmentRenderResultToElement(result) {
749
762
  if (result instanceof HTMLElement) return result;
@@ -820,9 +833,11 @@ class AparteChatBubble extends HTMLElement {
820
833
  this._updateContent();
821
834
  this._updateTimestamp(this.getAttribute("timestamp"));
822
835
  window.addEventListener("aparte-config-change", this._onConfigChange);
836
+ this.addEventListener("click", this._onBranchPickerClick);
823
837
  }
824
838
  disconnectedCallback() {
825
839
  window.removeEventListener("aparte-config-change", this._onConfigChange);
840
+ this.removeEventListener("click", this._onBranchPickerClick);
826
841
  if (this._avatarCleanup) {
827
842
  try {
828
843
  this._avatarCleanup();
@@ -909,12 +924,26 @@ class AparteChatBubble extends HTMLElement {
909
924
  return [...this._segments];
910
925
  }
911
926
  /** Remove a segment by id (e.g. to discard a transient waiting indicator) */
927
+ /**
928
+ * Scoped to DIRECT children on purpose.
929
+ *
930
+ * Segments are appended as direct children of the container, but a descendant
931
+ * query returns the first match in document order — and sanitized model
932
+ * markdown renders inside that same container, with `data-*` attributes
933
+ * deliberately preserved (they are inert). So a decoy `data-segment-id` planted
934
+ * in an earlier segment's prose used to win over the real segment element.
935
+ *
936
+ * Parser ids are unguessable UUIDs, but a tool segment is `tool-${toolCallId}`
937
+ * and the MODEL chooses that id — so this was reachable, and pointing an update
938
+ * at a decoy left a rejected tool rendering as still-running: a spoof against
939
+ * the human-in-the-loop control.
940
+ */
912
941
  removeSegment(segmentId) {
913
942
  const index = this._segments.findIndex((s2) => s2.id === segmentId);
914
943
  if (index !== -1) {
915
944
  this._segments.splice(index, 1);
916
945
  }
917
- const el = this._segmentsEl?.querySelector(`[data-segment-id="${cssEscape(segmentId)}"]`);
946
+ const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
918
947
  el?.remove();
919
948
  this._updateWaiting();
920
949
  }
@@ -928,7 +957,7 @@ class AparteChatBubble extends HTMLElement {
928
957
  *
929
958
  * This is the *precondition* for the info ("i") action, not the trigger: the
930
959
  * button appears only if the app also declared it wants it —
931
- * `AparteConfig.setBubbleActions({ info: true })` — because the stats popover it
960
+ * `aparteGlobalConfig.setBubbleActions({ info: true })` — because the stats popover it
932
961
  * opens (`aparte-message-info`) is the app's, and core has none. Without usage
933
962
  * there is nothing to show, so the button never renders either way.
934
963
  */
@@ -983,7 +1012,7 @@ class AparteChatBubble extends HTMLElement {
983
1012
  console.warn(`[AparteChatBubble] _appendSegmentEl ABORT: _segmentsEl is null`);
984
1013
  return;
985
1014
  }
986
- const renderer = resolveSegmentRenderer(segment.type);
1015
+ const renderer = resolveSegmentRenderer(segment.type, this._cfg);
987
1016
  if (renderer) {
988
1017
  const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
989
1018
  if (el) {
@@ -1001,12 +1030,12 @@ class AparteChatBubble extends HTMLElement {
1001
1030
  this._reflectError();
1002
1031
  }
1003
1032
  _applySegmentUpdate(segmentId, segment, updates) {
1004
- const el = this._segmentsEl?.querySelector(`[data-segment-id="${cssEscape(segmentId)}"]`);
1033
+ const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1005
1034
  if (!el) {
1006
1035
  this._renderSegments();
1007
1036
  return;
1008
1037
  }
1009
- const renderer = resolveSegmentRenderer(segment.type);
1038
+ const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1010
1039
  if (!renderer) return;
1011
1040
  if (renderer.update) {
1012
1041
  runWithConfig(this._cfg, () => renderer.update(el, segment));
@@ -1056,11 +1085,11 @@ class AparteChatBubble extends HTMLElement {
1056
1085
  else this.innerHTML = out;
1057
1086
  } else {
1058
1087
  this.innerHTML = `
1059
- <div class="aparte-message" data-role="${role}" role="article" aria-label="${this._getAriaLabel()}">
1060
- <div class="aparte-avatar" data-role="${role}"></div>
1088
+ <div class="aparte-message" data-role="${escapeAttr(role)}" role="article" aria-label="${escapeAttr(this._getAriaLabel())}">
1089
+ <div class="aparte-avatar" data-role="${escapeAttr(role)}"></div>
1061
1090
  <div class="aparte-body">
1062
1091
  <div class="aparte-header">
1063
- <span class="aparte-name">${this._escapeHtml(displayName)}</span>
1092
+ <span class="aparte-name">${escapeHtml(displayName)}</span>
1064
1093
  <span class="aparte-timestamp"></span>
1065
1094
  </div>
1066
1095
  <div class="aparte-attachments" hidden></div>
@@ -1074,11 +1103,11 @@ class AparteChatBubble extends HTMLElement {
1074
1103
  </div>
1075
1104
  <div class="aparte-footer">
1076
1105
  <div class="aparte-branch-picker" hidden>
1077
- <button class="aparte-branch-prev" aria-label="${this._cfg.getLocale().previousResponse ?? "Previous response"}">&#8249;</button>
1106
+ <button class="aparte-branch-prev" aria-label="${escapeAttr(this._cfg.getLocale().previousResponse ?? "Previous response")}">&#8249;</button>
1078
1107
  <span class="aparte-branch-label">1 / 1</span>
1079
- <button class="aparte-branch-next" aria-label="${this._cfg.getLocale().nextResponse ?? "Next response"}">&#8250;</button>
1108
+ <button class="aparte-branch-next" aria-label="${escapeAttr(this._cfg.getLocale().nextResponse ?? "Next response")}">&#8250;</button>
1080
1109
  </div>
1081
- <div class="aparte-action-bar" role="toolbar" aria-label="${this._cfg.getLocale().messageActions ?? "Message actions"}"></div>
1110
+ <div class="aparte-action-bar" role="toolbar" aria-label="${escapeAttr(this._cfg.getLocale().messageActions ?? "Message actions")}"></div>
1082
1111
  </div>
1083
1112
  </div>
1084
1113
  </div>
@@ -1090,7 +1119,6 @@ class AparteChatBubble extends HTMLElement {
1090
1119
  this._actionBarEl = this.querySelector(".aparte-action-bar");
1091
1120
  this._branchPickerEl = this.querySelector(".aparte-branch-picker");
1092
1121
  this._footerEl = this.querySelector(".aparte-footer");
1093
- this._setupBranchPickerListeners();
1094
1122
  this._updateActionBar();
1095
1123
  this._renderAvatar();
1096
1124
  if (this._streaming) this._updateStreaming(true);
@@ -1204,7 +1232,7 @@ class AparteChatBubble extends HTMLElement {
1204
1232
  if (!this._segmentsEl) return;
1205
1233
  this._segmentsEl.innerHTML = "";
1206
1234
  for (const segment of this._segments) {
1207
- const renderer = resolveSegmentRenderer(segment.type);
1235
+ const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1208
1236
  if (renderer) {
1209
1237
  const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1210
1238
  if (el) {
@@ -1276,11 +1304,11 @@ class AparteChatBubble extends HTMLElement {
1276
1304
  return;
1277
1305
  }
1278
1306
  this._attachmentsEl.innerHTML = this._attachments.map((a2) => {
1279
- const name = this._escapeHtml(a2.name);
1307
+ const name = escapeHtml(a2.name);
1280
1308
  if (a2.type.startsWith("image/")) {
1281
- return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${this._escapeHtml(a2.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
1309
+ return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeHtml(a2.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
1282
1310
  }
1283
- return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${this._escapeHtml(this._fileExt(a2.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
1311
+ return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._fileExt(a2.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
1284
1312
  }).join("");
1285
1313
  if (!this._cfg.getHostHandlers().attachmentPreview) return;
1286
1314
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
@@ -1312,22 +1340,39 @@ class AparteChatBubble extends HTMLElement {
1312
1340
  // ─────────────────────────────────────────────────────────────────────────
1313
1341
  // Branch Picker
1314
1342
  // ─────────────────────────────────────────────────────────────────────────
1315
- _setupBranchPickerListeners() {
1316
- const prevBtn = this._branchPickerEl?.querySelector(".aparte-branch-prev");
1317
- const nextBtn = this._branchPickerEl?.querySelector(".aparte-branch-next");
1318
- const dispatchNav = (direction) => {
1319
- const messageId = this.getAttribute("message-id");
1320
- if (!messageId) return;
1321
- const detail = { messageId, direction };
1322
- this.dispatchEvent(new CustomEvent("aparte-branch-navigate", {
1323
- bubbles: true,
1324
- composed: true,
1325
- detail
1326
- }));
1327
- };
1328
- prevBtn?.addEventListener("click", () => dispatchNav("prev"));
1329
- nextBtn?.addEventListener("click", () => dispatchNav("next"));
1330
- }
1343
+ /**
1344
+ * The branch arrows are handled by DELEGATION, on this element, bound once.
1345
+ *
1346
+ * They used to get a fresh listener each, attached by `_render()` to the buttons
1347
+ * `_render()` had just created. So a click that landed while a re-render was
1348
+ * swapping those nodes hit an element about to be discarded, and did nothing at
1349
+ * all not late, nothing. Invisible on a fast machine; reproducible on
1350
+ * WebKit-Linux in CI, where `‹` left the picker on "2 / 2" and a 20-second
1351
+ * assertion watched it stay there.
1352
+ *
1353
+ * Delegation makes `_render()` irrelevant to it: the listener lives on the host,
1354
+ * which is never replaced, and `closest()` finds whichever button exists at the
1355
+ * moment of the click. It is also less work — one listener per bubble instead of
1356
+ * two per bubble per render.
1357
+ *
1358
+ * Bound in `connectedCallback` and removed in `disconnectedCallback` as a stable
1359
+ * field, because an inline arrow re-added on every re-connect is how this repo has
1360
+ * stacked listeners twice before (the viewport, and `aparte-select`).
1361
+ */
1362
+ _onBranchPickerClick = (event) => {
1363
+ const target = event.target;
1364
+ const button = target?.closest?.(".aparte-branch-prev, .aparte-branch-next");
1365
+ if (!button || !this.contains(button)) return;
1366
+ const direction = button.classList.contains("aparte-branch-prev") ? "prev" : "next";
1367
+ const messageId = this.getAttribute("message-id");
1368
+ if (!messageId) return;
1369
+ const detail = { messageId, direction };
1370
+ this.dispatchEvent(new CustomEvent("aparte-branch-navigate", {
1371
+ bubbles: true,
1372
+ composed: true,
1373
+ detail
1374
+ }));
1375
+ };
1331
1376
  _updateBranchPicker() {
1332
1377
  if (!this._branchPickerEl) return;
1333
1378
  if (this._siblingCount <= 1 || this._role !== "assistant") {
@@ -1428,28 +1473,28 @@ class AparteChatBubble extends HTMLElement {
1428
1473
  switch (action) {
1429
1474
  case "copy": {
1430
1475
  const l2 = locale.copy ?? "Copy";
1431
- return `<button class="aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${l2}" title="${l2}">${icons.copy()}</button>`;
1476
+ return `<button class="aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.copy()}</button>`;
1432
1477
  }
1433
1478
  case "edit": {
1434
1479
  const l2 = locale.edit ?? "Edit message";
1435
- return `<button class="aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${l2}" title="${l2}">${icons.edit()}</button>`;
1480
+ return `<button class="aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.edit()}</button>`;
1436
1481
  }
1437
1482
  case "retry": {
1438
1483
  const l2 = locale.retry ?? "Retry";
1439
- return `<button class="aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${l2}" title="${l2}">${icons.retry()}</button>`;
1484
+ return `<button class="aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.retry()}</button>`;
1440
1485
  }
1441
1486
  case "thumbUp": {
1442
1487
  const l2 = locale.feedbackPositive ?? "Good response";
1443
- return `<button class="aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${l2}" title="${l2}">${icons.thumbUp()}</button>`;
1488
+ return `<button class="aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbUp()}</button>`;
1444
1489
  }
1445
1490
  case "thumbDown": {
1446
1491
  const l2 = locale.feedbackNegative ?? "Bad response";
1447
- return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${l2}" title="${l2}">${icons.thumbDown()}</button>`;
1492
+ return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbDown()}</button>`;
1448
1493
  }
1449
1494
  case "info": {
1450
1495
  if (!this._usage) return "";
1451
1496
  const l2 = locale.messageInfo ?? "Details";
1452
- return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${l2}" title="${l2}">${INFO_ICON_SVG}</button>`;
1497
+ return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${INFO_ICON_SVG}</button>`;
1453
1498
  }
1454
1499
  default:
1455
1500
  return "";
@@ -1461,7 +1506,7 @@ class AparteChatBubble extends HTMLElement {
1461
1506
  const locale = this._cfg.getLocale();
1462
1507
  const saveLabel = locale.editConfirm ?? "Save";
1463
1508
  const cancelLabel = locale.editCancel ?? "Cancel";
1464
- this._actionBarEl.innerHTML = `<button class="aparte-action-btn aparte-action-edit-save" data-action="edit-save" aria-label="${saveLabel}" title="${saveLabel}">${this._cfg.getIcon("check")}</button><button class="aparte-action-btn aparte-action-edit-cancel" data-action="edit-cancel" aria-label="${cancelLabel}" title="${cancelLabel}">${this._cfg.getIcon("close")}</button>`;
1509
+ this._actionBarEl.innerHTML = `<button class="aparte-action-btn aparte-action-edit-save" data-action="edit-save" aria-label="${escapeAttr(saveLabel)}" title="${escapeAttr(saveLabel)}">${this._cfg.getIcon("check")}</button><button class="aparte-action-btn aparte-action-edit-cancel" data-action="edit-cancel" aria-label="${escapeAttr(cancelLabel)}" title="${escapeAttr(cancelLabel)}">${this._cfg.getIcon("close")}</button>`;
1465
1510
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1466
1511
  btn.addEventListener("click", (e2) => this._handleActionClick(e2));
1467
1512
  });
@@ -1636,9 +1681,6 @@ class AparteChatBubble extends HTMLElement {
1636
1681
  // ─────────────────────────────────────────────────────────────────────────
1637
1682
  // Utilities
1638
1683
  // ─────────────────────────────────────────────────────────────────────────
1639
- _escapeHtml(str) {
1640
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1641
- }
1642
1684
  _updateStreaming(streaming) {
1643
1685
  const wasStreaming = this._streaming;
1644
1686
  this._streaming = streaming;
@@ -1664,12 +1706,20 @@ class AparteChatStatus extends HTMLElement {
1664
1706
  static get observedAttributes() {
1665
1707
  return ["visible", "text"];
1666
1708
  }
1667
- _cfg;
1709
+ /**
1710
+ * Resolved LIVE, not cached. Caching it at connect made this element
1711
+ * permanently deaf to its own instance: `_onConfigChange` filters on
1712
+ * `detail.config !== this._cfg`, so once `_cfg` had latched the global config no
1713
+ * change for the real instance ever matched, and the filter meant to isolate
1714
+ * chats became the thing that silenced one.
1715
+ */
1716
+ get _cfg() {
1717
+ return resolveConfig(this);
1718
+ }
1668
1719
  constructor() {
1669
1720
  super();
1670
1721
  }
1671
1722
  connectedCallback() {
1672
- this._cfg = resolveConfig(this);
1673
1723
  this._render();
1674
1724
  window.addEventListener("aparte-config-change", this._onConfigChange);
1675
1725
  }
@@ -1792,7 +1842,7 @@ class AparteChatViewport extends HTMLElement {
1792
1842
  _spacerRafId = null;
1793
1843
  _spacerFrozenUntil = 0;
1794
1844
  _layoutTransitionMs = 0;
1795
- _repo = new MessageRepository();
1845
+ _repo = new AparteMessageRepository();
1796
1846
  _isAutoScrollEnabled = true;
1797
1847
  _scrollThreshold = 50;
1798
1848
  /** When true, the next _autoScroll() call uses smooth instead of instant, then resets. */
@@ -1947,7 +1997,17 @@ class AparteChatViewport extends HTMLElement {
1947
1997
  * framework wrapper's host element.
1948
1998
  */
1949
1999
  _activeMessageId() {
1950
- return this._repo.headId;
2000
+ const head = this._repo.headId;
2001
+ const streaming = this._streamingMessageId();
2002
+ if (streaming !== null && streaming !== head) return null;
2003
+ return head;
2004
+ }
2005
+ /** The id of the message currently streaming, if any. */
2006
+ _streamingMessageId() {
2007
+ for (const message of this._repo.getMessages()) {
2008
+ if (message.isStreaming) return message.id;
2009
+ }
2010
+ return null;
1951
2011
  }
1952
2012
  addSegment(messageIdOrSegment, maybeSegment) {
1953
2013
  const messageId = typeof messageIdOrSegment === "string" ? messageIdOrSegment : this._activeMessageId();
@@ -2007,7 +2067,7 @@ class AparteChatViewport extends HTMLElement {
2007
2067
  /**
2008
2068
  * Persist token usage on a message and propagate to the live bubble, which is
2009
2069
  * what allows the info ("i") action to render — provided the app declared it
2010
- * with `AparteConfig.setBubbleActions({ info: true })`; it is off by default,
2070
+ * with `aparteGlobalConfig.setBubbleActions({ info: true })`; it is off by default,
2011
2071
  * since the popover it opens belongs to the app.
2012
2072
  */
2013
2073
  setUsage(messageId, usage) {
@@ -2125,7 +2185,7 @@ class AparteChatViewport extends HTMLElement {
2125
2185
  const meta = this._repo.getMessage(messageId);
2126
2186
  if (!meta) return 0;
2127
2187
  const newMsg = {
2128
- id: crypto.randomUUID(),
2188
+ id: uuid(),
2129
2189
  role: "assistant",
2130
2190
  content: "",
2131
2191
  status: "pending",
@@ -2235,7 +2295,7 @@ class AparteChatViewport extends HTMLElement {
2235
2295
  * branch arrows on already-rendered bubbles.
2236
2296
  */
2237
2297
  importTree(tree) {
2238
- this.clearAll();
2298
+ this.clearAll({ revokeAttachments: false });
2239
2299
  this._repo.import(tree);
2240
2300
  this._reRenderActivePath();
2241
2301
  }
@@ -2248,7 +2308,12 @@ class AparteChatViewport extends HTMLElement {
2248
2308
  * so desynchronises the framework's view tree from the live DOM and the
2249
2309
  * next change-detection pass throws `NotFoundError` on insertBefore.
2250
2310
  */
2251
- clearAll() {
2311
+ clearAll(options) {
2312
+ if (options?.revokeAttachments !== false) {
2313
+ for (const message of this._repo.getMessages()) {
2314
+ revokeAttachmentUrls(message.attachments);
2315
+ }
2316
+ }
2252
2317
  this._repo.clear();
2253
2318
  if (!this._frameworkManagedDOM) {
2254
2319
  const wrapper = this.querySelector(".aparte-messages-wrapper");
@@ -2277,7 +2342,7 @@ class AparteChatViewport extends HTMLElement {
2277
2342
  * build chat history).
2278
2343
  */
2279
2344
  setMessages(messages) {
2280
- this.clearAll();
2345
+ this.clearAll({ revokeAttachments: false });
2281
2346
  for (const m2 of messages) {
2282
2347
  this.appendMessage(m2);
2283
2348
  }
@@ -2535,18 +2600,27 @@ class AparteChatViewport extends HTMLElement {
2535
2600
  this._bottomSpacer.style.height = `${px}px`;
2536
2601
  }
2537
2602
  }
2603
+ // Bound fields, not inline arrows: a custom element is re-connected every
2604
+ // time it is MOVED in the DOM (a portal, a dialog, a framework re-parenting),
2605
+ // so `_setupEventListeners` runs again each time. An inline arrow can never
2606
+ // be handed to `removeEventListener`, so it just accumulates — one branch
2607
+ // click then ran N handlers, N active-path re-renders and N storage writes
2608
+ // through the conversation controller. The window listeners next to these
2609
+ // were always removed properly; these two, attached to `this`, were not.
2610
+ _onScrollBtnClick = () => {
2611
+ this._isAutoScrollEnabled = true;
2612
+ this._smoothScrollToBottom();
2613
+ this._updateScrollButton();
2614
+ };
2615
+ _onBranchNavigate = (e2) => {
2616
+ const evt = e2;
2617
+ evt.stopPropagation();
2618
+ this.navigateBranch(evt.detail.messageId, evt.detail.direction);
2619
+ };
2538
2620
  _setupEventListeners() {
2539
2621
  this._container?.addEventListener("scroll", this._handleScroll, { passive: true });
2540
- this._scrollBtn?.addEventListener("click", () => {
2541
- this._isAutoScrollEnabled = true;
2542
- this._smoothScrollToBottom();
2543
- this._updateScrollButton();
2544
- });
2545
- this.addEventListener("aparte-branch-navigate", (e2) => {
2546
- const evt = e2;
2547
- evt.stopPropagation();
2548
- this.navigateBranch(evt.detail.messageId, evt.detail.direction);
2549
- });
2622
+ this._scrollBtn?.addEventListener("click", this._onScrollBtnClick);
2623
+ this.addEventListener("aparte-branch-navigate", this._onBranchNavigate);
2550
2624
  }
2551
2625
  _setupObservers() {
2552
2626
  this._resizeObserver = new ResizeObserver(() => {
@@ -2592,6 +2666,41 @@ class AparteChatViewport extends HTMLElement {
2592
2666
  _scrollToBottom() {
2593
2667
  if (!this._container) return;
2594
2668
  this._container.scrollTop = this._container.scrollHeight;
2669
+ this._settleAtBottom(4);
2670
+ }
2671
+ /**
2672
+ * Confirm over the next few frames that we actually reached the bottom.
2673
+ *
2674
+ * One assignment is not enough, and the reason is measured rather than guessed.
2675
+ * A timeline of a streamed turn on Safari (framework mode) recorded the content
2676
+ * settling in TWO layout passes — 1118 → 1121 → 1152 px. `scrollTop =
2677
+ * scrollHeight` ran against the middle one, clamped to that layout's max (603),
2678
+ * and nothing ran afterwards: the last 31px never closed. Auto-follow stayed
2679
+ * armed the whole time, so the component was not disarmed — it was SATISFIED.
2680
+ * `_isAtBottom()` answers "yes" for any gap under `_scrollThreshold` (50), which
2681
+ * is the right rule for keeping auto-follow armed and the wrong one as a
2682
+ * definition of "anchored".
2683
+ *
2684
+ * Ruled out on the way here, so nobody pays for it twice: not a WebKit
2685
+ * padding-accounting difference (a probe writing `scrollTop = 1e7` reached
2686
+ * exactly `scrollHeight - clientHeight`), not a missing `characterData`
2687
+ * mutation, and not a child resize a ResizeObserver could see.
2688
+ *
2689
+ * A BOUNDED retry, not one corrective frame: a single frame lands on the same
2690
+ * stale layout and was measured leaving a wider gap than doing nothing. Bounded
2691
+ * so it always terminates; re-reads `_isAutoScrollEnabled` every frame so a
2692
+ * reader who scrolls away mid-settle is left alone; stops as soon as the gap is
2693
+ * closed, so the common case costs one frame that does nothing.
2694
+ */
2695
+ _settleAtBottom(framesLeft) {
2696
+ if (framesLeft <= 0) return;
2697
+ requestAnimationFrame(() => {
2698
+ if (!this._container || !this._isAutoScrollEnabled) return;
2699
+ const max = this._container.scrollHeight - this._container.clientHeight;
2700
+ if (max - this._container.scrollTop <= 1) return;
2701
+ this._container.scrollTop = max;
2702
+ this._settleAtBottom(framesLeft - 1);
2703
+ });
2595
2704
  }
2596
2705
  _smoothScrollToBottom() {
2597
2706
  if (!this._container) return;
@@ -2684,7 +2793,7 @@ class AparteChatViewport extends HTMLElement {
2684
2793
  * Cap the number of rendered bubbles in the DOM (perf ceiling only).
2685
2794
  *
2686
2795
  * Drops the oldest `<aparte-chat-bubble>` elements beyond `_maxRenderedBubbles`
2687
- * from the DOM. It **never** touches the MessageRepository — the conversation
2796
+ * from the DOM. It **never** touches the AparteMessageRepository — the conversation
2688
2797
  * model and its persistence snapshot stay complete (retention/eviction is a
2689
2798
  * consumer/persistence concern, not the viewport's). No-op when a framework
2690
2799
  * owns the DOM.
@@ -2703,11 +2812,13 @@ class AparteChatViewport extends HTMLElement {
2703
2812
  if (this._warnedMaxMessagesDeprecation) return;
2704
2813
  this._warnedMaxMessagesDeprecation = true;
2705
2814
  console.warn(
2706
- "[Aparte] `maxMessages` / `max-messages` on aparte-chat-viewport is deprecated: it used to silently evict messages from the conversation model. It now only caps rendered bubbles in the DOM — use `maxRenderedBubbles` / `max-rendered-bubbles`. For actual history retention, configure it on your ConversationManager instead."
2815
+ "[Aparte] `maxMessages` / `max-messages` on aparte-chat-viewport is deprecated: it used to silently evict messages from the conversation model. It now only caps rendered bubbles in the DOM — use `maxRenderedBubbles` / `max-rendered-bubbles`. For actual history retention, configure it on your AparteConversationManager instead."
2707
2816
  );
2708
2817
  }
2709
2818
  _cleanup() {
2710
2819
  this._container?.removeEventListener("scroll", this._handleScroll);
2820
+ this._scrollBtn?.removeEventListener("click", this._onScrollBtnClick);
2821
+ this.removeEventListener("aparte-branch-navigate", this._onBranchNavigate);
2711
2822
  this._resizeObserver?.disconnect();
2712
2823
  this._mutationObserver?.disconnect();
2713
2824
  this._resizeObserver = null;
@@ -2727,6 +2838,8 @@ class AparteComposer extends HTMLElement {
2727
2838
  _attachments = [];
2728
2839
  _listeners = /* @__PURE__ */ new Map();
2729
2840
  _panelActive = false;
2841
+ /** What the send button means while a panel is up — see `showPanel`. */
2842
+ _panelMode = "submit";
2730
2843
  _panelSubmitEnabled = false;
2731
2844
  _panelOnSubmit = null;
2732
2845
  // Internal bus events that represent an observable STATE change — these are
@@ -2743,8 +2856,25 @@ class AparteComposer extends HTMLElement {
2743
2856
  _onMessageStart = this._handleMessageStart.bind(this);
2744
2857
  _onMessageDone = this._handleMessageDone.bind(this);
2745
2858
  /** Config governing THIS composer (nearest instance boundary, else global). */
2746
- _cfg = AparteConfig;
2747
- _configUnsub = null;
2859
+ /**
2860
+ * Resolved LIVE, not cached at connect.
2861
+ *
2862
+ * A wrapper runs `AparteChatHost.bind()` — which calls `attachConfig` — from its
2863
+ * POST-mount hook, so this element connects BEFORE the boundary exists. Caching
2864
+ * here latched the global config forever: an instance `config` carrying an RTL
2865
+ * locale flipped the transcript and not the composer, and the
2866
+ * `requireModelSelection` gate was read off the wrong object. `aparte-chat-bubble`
2867
+ * has always resolved live, and its JSDoc names this exact race.
2868
+ */
2869
+ get _cfg() {
2870
+ return resolveConfig(this);
2871
+ }
2872
+ /** Only OUR config: a change on another chat's instance must not touch us. */
2873
+ _onConfigChangeEvent = (e2) => {
2874
+ const detail = e2.detail;
2875
+ if (detail?.config && detail.config !== this._cfg) return;
2876
+ this._onConfigChange();
2877
+ };
2748
2878
  /** True while `requireModelSelection` is on AND no model is selected — blocks send. */
2749
2879
  _modelGated = false;
2750
2880
  _onConfigChange = () => {
@@ -2776,8 +2906,7 @@ class AparteComposer extends HTMLElement {
2776
2906
  window.addEventListener("aparte-message-done", this._onMessageDone);
2777
2907
  window.addEventListener("aparte-message-error", this._onMessageDone);
2778
2908
  window.addEventListener("aparte-message-aborted", this._onMessageDone);
2779
- this._cfg = resolveConfig(this);
2780
- this._configUnsub = this._cfg.subscribe(this._onConfigChange);
2909
+ window.addEventListener("aparte-config-change", this._onConfigChangeEvent);
2781
2910
  this._evaluateModelGate();
2782
2911
  this._applyDirection();
2783
2912
  }
@@ -2786,8 +2915,7 @@ class AparteComposer extends HTMLElement {
2786
2915
  window.removeEventListener("aparte-message-done", this._onMessageDone);
2787
2916
  window.removeEventListener("aparte-message-error", this._onMessageDone);
2788
2917
  window.removeEventListener("aparte-message-aborted", this._onMessageDone);
2789
- this._configUnsub?.();
2790
- this._configUnsub = null;
2918
+ window.removeEventListener("aparte-config-change", this._onConfigChangeEvent);
2791
2919
  this._listeners.clear();
2792
2920
  }
2793
2921
  attributeChangedCallback(name, _old, value) {
@@ -2863,11 +2991,29 @@ class AparteComposer extends HTMLElement {
2863
2991
  this._attachments = [];
2864
2992
  this._emit("attachments-change", { attachments: [] });
2865
2993
  }
2866
- /** Inject a panel into the composer, hiding the text input. The send button calls onSubmit when clicked. */
2994
+ /**
2995
+ * Inject a panel into the composer. The send button calls `onSubmit` when clicked.
2996
+ *
2997
+ * While a panel is up, the composer is answering a QUESTION, not composing a
2998
+ * message — so the affordances that lead nowhere go away with the text input:
2999
+ * the attachment picker above all, which stayed clickable while the user was
3000
+ * being asked something ("on voyait encore l'icône de upload", reported from a
3001
+ * real session). Ratified decision #8: an affordance nothing can honour is not
3002
+ * rendered.
3003
+ *
3004
+ * What STAYS, deliberately: the attachments strip, because pending attachments
3005
+ * are the user's state and not an action to offer — hiding them would look like
3006
+ * losing them; and the toolbar, because switching model still does something.
3007
+ *
3008
+ * Declared with an attribute + CSS rather than the inline `style.display` this
3009
+ * used to set on a child: an attribute is themeable, is visible to a consumer's
3010
+ * own rules, and does not clobber a `display` the consumer had set (the restore
3011
+ * wrote `''`, not the previous value).
3012
+ */
2867
3013
  showPanel(panel, options) {
2868
3014
  this.hidePanel();
2869
3015
  const inputEl = this.querySelector("aparte-composer-input");
2870
- if (inputEl) inputEl.style.display = "none";
3016
+ this.setAttribute("data-panel-active", "");
2871
3017
  panel.dataset["apartePanel"] = "true";
2872
3018
  if (inputEl) {
2873
3019
  inputEl.insertAdjacentElement("afterend", panel);
@@ -2876,26 +3022,35 @@ class AparteComposer extends HTMLElement {
2876
3022
  }
2877
3023
  this._panelActive = true;
2878
3024
  this._panelSubmitEnabled = options?.submitEnabled ?? false;
3025
+ this._panelMode = options?.mode ?? "submit";
2879
3026
  this._panelOnSubmit = options?.onSubmit ?? null;
2880
- this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled });
3027
+ this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled, mode: this._panelMode });
2881
3028
  }
2882
- /** Remove the panel and restore the text input. */
3029
+ /** Remove the panel and restore the composer's own controls. */
2883
3030
  hidePanel() {
2884
3031
  const existing = this.querySelector("[data-aparte-panel]");
2885
3032
  if (existing) existing.remove();
2886
- const inputEl = this.querySelector("aparte-composer-input");
2887
- if (inputEl) inputEl.style.display = "";
3033
+ this.removeAttribute("data-panel-active");
2888
3034
  this._panelActive = false;
2889
3035
  this._panelSubmitEnabled = false;
3036
+ this._panelMode = "submit";
2890
3037
  this._panelOnSubmit = null;
2891
- this._emit("panel-change", { active: false, submitEnabled: false });
3038
+ this._emit("panel-change", { active: false, submitEnabled: false, mode: "submit" });
2892
3039
  this.focus();
2893
3040
  }
2894
- /** Update the send button enabled state while a panel is active. */
2895
- setPanelSubmitEnabled(enabled) {
3041
+ /**
3042
+ * Update the send button's state while a panel is active.
3043
+ *
3044
+ * `mode` moves with it because both change on the same event — answering the
3045
+ * question you are on can enable the button AND turn it from "advance" into
3046
+ * "submit" (when it was the last one), and two separate calls would flash a
3047
+ * wrong icon between them.
3048
+ */
3049
+ setPanelSubmitEnabled(enabled, mode) {
2896
3050
  if (!this._panelActive) return;
2897
3051
  this._panelSubmitEnabled = enabled;
2898
- this._emit("panel-change", { active: true, submitEnabled: enabled });
3052
+ if (mode) this._panelMode = mode;
3053
+ this._emit("panel-change", { active: true, submitEnabled: enabled, mode: this._panelMode });
2899
3054
  }
2900
3055
  get panelActive() {
2901
3056
  return this._panelActive;
@@ -2986,7 +3141,36 @@ class AparteComposer extends HTMLElement {
2986
3141
  * Without this filter, streaming in one chat flips every composer's state. */
2987
3142
  _isForThisComposer(e2) {
2988
3143
  const evtTargetId = e2.detail?.targetId;
2989
- return !evtTargetId || !this.targetId || evtTargetId === this.targetId;
3144
+ return !evtTargetId || !this._ownTargetId() || evtTargetId === this._ownTargetId();
3145
+ }
3146
+ /**
3147
+ * Which chat this composer belongs to: its `target` attribute, or the id of the
3148
+ * chat host above it.
3149
+ *
3150
+ * All four wrappers set `target` themselves, so the attribute alone identified a
3151
+ * composer there. In RAW core — the documented quick start, where the markup is
3152
+ * hand-written — nothing sets it, so `!this.targetId` was true and this composer
3153
+ * accepted every chat's lifecycle events: on a two-chat page, one chat's Stop
3154
+ * tore down the other's open elicitation panel while its tool call kept waiting,
3155
+ * i.e. the question vanished and the turn hung.
3156
+ *
3157
+ * Found by a two-chat test written for the elicitation presenter, which is the
3158
+ * only reason it surfaced: raw core with two chats is a shape nothing exercised.
3159
+ * The hosts matched are the ones `aparte-chat-bubble._resolveTargetId()` matches,
3160
+ * for the reason written there — Angular's wrapper root IS `<aparte-chat>`, the
3161
+ * other three render a `[data-aparte-chat]` div.
3162
+ */
3163
+ _ownTargetId() {
3164
+ const attr = this.targetId;
3165
+ if (attr) return attr;
3166
+ let el = this.parentElement;
3167
+ while (el) {
3168
+ const tag = el.tagName?.toLowerCase();
3169
+ const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
3170
+ if (isHost && el.id) return el.id;
3171
+ el = el.parentElement;
3172
+ }
3173
+ return void 0;
2990
3174
  }
2991
3175
  _handleMessageStart(e2) {
2992
3176
  if (!this._isForThisComposer(e2)) return;
@@ -3093,7 +3277,7 @@ class AparteComposerInput extends HTMLElement {
3093
3277
  _render() {
3094
3278
  if (this.querySelector(".aparte-ci-editor")) return;
3095
3279
  const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
3096
- const placeholder = this._escapeAttr(this._getPlaceholder());
3280
+ const placeholder = escapeAttr(this._getPlaceholder());
3097
3281
  this.innerHTML = `<div
3098
3282
  class="aparte-ci-editor"
3099
3283
  contenteditable="${!disabled}"
@@ -3217,9 +3401,6 @@ class AparteComposerInput extends HTMLElement {
3217
3401
  this._editor.setAttribute("aria-disabled", String(disabled));
3218
3402
  }
3219
3403
  /** Escape a value before it lands in a double-quoted HTML attribute. */
3220
- _escapeAttr(str) {
3221
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3222
- }
3223
3404
  }
3224
3405
  if (!customElements.get("aparte-composer-input")) {
3225
3406
  customElements.define("aparte-composer-input", AparteComposerInput);
@@ -3250,8 +3431,8 @@ class AparteComposerSend extends HTMLElement {
3250
3431
  const disabled = !root || root.disabled || root.value.trim() === "";
3251
3432
  this.innerHTML = `<button
3252
3433
  class="aparte-cs-button aparte-send-button"
3253
- aria-label="${label}"
3254
- title="${label}"
3434
+ aria-label="${escapeAttr(label)}"
3435
+ title="${escapeAttr(label)}"
3255
3436
  ${disabled ? "disabled" : ""}
3256
3437
  >${icon}</button>`;
3257
3438
  this._button = this.querySelector(".aparte-cs-button");
@@ -3276,12 +3457,14 @@ class AparteComposerSend extends HTMLElement {
3276
3457
  root._on("attachments-change", () => this._syncState())
3277
3458
  );
3278
3459
  this._unsubscribes.push(
3279
- root._on("panel-change", ({ active, submitEnabled }) => {
3460
+ root._on("panel-change", ({ active, submitEnabled, mode }) => {
3280
3461
  if (!this._button) return;
3281
3462
  if (active) {
3282
3463
  this._button.disabled = !submitEnabled;
3283
- this._button.innerHTML = this._getSendIcon();
3284
- const label = resolveConfig(this).t("submitButton") || "Submit";
3464
+ const cfg = resolveConfig(this);
3465
+ const advancing = mode === "advance";
3466
+ this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
3467
+ const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
3285
3468
  this._button.setAttribute("aria-label", label);
3286
3469
  this._button.setAttribute("title", label);
3287
3470
  this._button.classList.remove("is-streaming");
@@ -3324,6 +3507,13 @@ class AparteComposerSend extends HTMLElement {
3324
3507
  this._syncState();
3325
3508
  }
3326
3509
  }
3510
+ /**
3511
+ * The icon for submitting an ANSWER, which is not the same act as sending a
3512
+ * message — one button, two meanings, and it has to say which one it is.
3513
+ */
3514
+ _getSubmitIcon() {
3515
+ return resolveConfig(this).getIcon("check");
3516
+ }
3327
3517
  _getSendIcon() {
3328
3518
  return resolveConfig(this).getIcon("send") || "Send";
3329
3519
  }
@@ -3358,8 +3548,8 @@ class AparteComposerCancel extends HTMLElement {
3358
3548
  const icon = this._getStopIcon();
3359
3549
  this.innerHTML = `<button
3360
3550
  class="aparte-cc-button"
3361
- aria-label="${label}"
3362
- title="${label}"
3551
+ aria-label="${escapeAttr(label)}"
3552
+ title="${escapeAttr(label)}"
3363
3553
  hidden
3364
3554
  >${icon}</button>`;
3365
3555
  this._button = this.querySelector(".aparte-cc-button");
@@ -3426,9 +3616,9 @@ class AparteComposerAttachments extends HTMLElement {
3426
3616
  if (file.type.startsWith("image/")) {
3427
3617
  const url = URL.createObjectURL(file);
3428
3618
  this._objectUrls.push(url);
3429
- return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${url}" alt="${name}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
3619
+ return `<div class="aparte-thumb aparte-thumb--image" title="${escapeAttr(name)}"><img class="aparte-thumb__img" src="${escapeAttr(url)}" alt="${escapeAttr(name)}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
3430
3620
  }
3431
- return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${this._escape(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
3621
+ return `<div class="aparte-thumb aparte-thumb--file" title="${escapeAttr(name)}"><span class="aparte-thumb__ext">${this._escape(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
3432
3622
  }).join("");
3433
3623
  this.querySelectorAll(".aparte-thumb__remove").forEach((btn, i2) => {
3434
3624
  btn.addEventListener("click", (e2) => {
@@ -3507,8 +3697,8 @@ class AparteComposerAddAttachment extends HTMLElement {
3507
3697
  const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
3508
3698
  this.innerHTML = `<button
3509
3699
  class="aparte-caa-button aparte-action-button"
3510
- aria-label="${label}"
3511
- title="${label}"
3700
+ aria-label="${escapeAttr(label)}"
3701
+ title="${escapeAttr(label)}"
3512
3702
  type="button"
3513
3703
  ${disabled ? "disabled" : ""}
3514
3704
  >${icon}</button>`;
@@ -3618,7 +3808,7 @@ class AparteComposerAction extends HTMLElement {
3618
3808
  }
3619
3809
  _render() {
3620
3810
  if (this.querySelector(".aparte-cact-button")) return;
3621
- const label = this._escapeAttr(this.getAttribute("label") ?? "");
3811
+ const label = escapeAttr(this.getAttribute("label") ?? "");
3622
3812
  const icon = this._resolveIcon(this.getAttribute("icon") ?? "");
3623
3813
  const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
3624
3814
  this.innerHTML = `<button
@@ -3658,9 +3848,6 @@ class AparteComposerAction extends HTMLElement {
3658
3848
  return resolveConfig(this).getIcon(icon) ?? icon;
3659
3849
  }
3660
3850
  /** Escape a value before it lands in a double-quoted HTML attribute. */
3661
- _escapeAttr(str) {
3662
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3663
- }
3664
3851
  }
3665
3852
  if (!customElements.get("aparte-composer-action")) {
3666
3853
  customElements.define("aparte-composer-action", AparteComposerAction);
@@ -3776,8 +3963,8 @@ class AparteConversationList extends HTMLElement {
3776
3963
  class="aparte-conv-item__archive"
3777
3964
  type="button"
3778
3965
  data-archive-id="${escapedId}"
3779
- data-archive-action="${archiveAction}"
3780
- aria-label="${archiveAriaLabel}"
3966
+ data-archive-action="${escapeAttr(archiveAction)}"
3967
+ aria-label="${escapeAttr(archiveAriaLabel)}"
3781
3968
  tabindex="0"
3782
3969
  >${archiveIcon}</button>
3783
3970
  <button
@@ -3856,14 +4043,49 @@ class AparteConversationList extends HTMLElement {
3856
4043
  if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
3857
4044
  class AparteElicitation extends HTMLElement {
3858
4045
  _pending = null;
3859
- _onTurnEnd = () => this._cancelPending();
4046
+ /**
4047
+ * A turn ended — cancel the open question only if it was OUR turn.
4048
+ *
4049
+ * These two listeners sit on `window` and had no instance filter at all, so a
4050
+ * Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
4051
+ * waiting on — and that chat's model was told the user had refused a question
4052
+ * the user was still looking at. Same defect as the `compact()` handler that
4053
+ * emptied both chats, which its four sibling handlers in `AparteClient` already
4054
+ * guarded against.
4055
+ *
4056
+ * The leniency rule is the composer's, deliberately: an event with no
4057
+ * `targetId` is for everyone (a single-chat app never sets one), and a chat we
4058
+ * cannot identify accepts everything rather than becoming deaf. Only a
4059
+ * MISMATCH is ignored.
4060
+ */
4061
+ _onTurnEnd = (e2) => {
4062
+ const evtTargetId = e2.detail?.targetId;
4063
+ const own = this._pendingTargetId();
4064
+ if (evtTargetId && own && evtTargetId !== own) return;
4065
+ this._cancelPending();
4066
+ };
3860
4067
  connectedCallback() {
3861
4068
  this.style.display = "none";
3862
- this._injectStyles();
3863
4069
  resolveConfig(this).setElicitationPresenter(this._present);
3864
4070
  window.addEventListener("aparte-message-aborted", this._onTurnEnd);
3865
4071
  window.addEventListener("aparte-message-error", this._onTurnEnd);
3866
4072
  }
4073
+ /**
4074
+ * The boundary above us appeared, changed, or went away — move the
4075
+ * registration with it.
4076
+ *
4077
+ * `connectedCallback` alone is not enough and cannot be: registering is a
4078
+ * WRITE, and under all four wrappers it happens before `attachConfig` runs, so
4079
+ * it lands on the global singleton. `requestUserInput()` then resolves the
4080
+ * instance config, finds nothing, and answers the model `cancel` — the model
4081
+ * hears the user refuse a question the user never saw.
4082
+ *
4083
+ * See {@link AparteConfigAware}.
4084
+ */
4085
+ aparteConfigChanged(next, previous) {
4086
+ if (previous.getElicitationPresenter() === this._present) previous.setElicitationPresenter(null);
4087
+ next.setElicitationPresenter(this._present);
4088
+ }
3867
4089
  disconnectedCallback() {
3868
4090
  const cfg = resolveConfig(this);
3869
4091
  if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
@@ -3891,22 +4113,26 @@ class AparteElicitation extends HTMLElement {
3891
4113
  }
3892
4114
  request.signal.addEventListener("abort", () => settle({ action: "cancel" }), { once: true });
3893
4115
  }
3894
- const panel = buildElicitationPanel(request.message, request.schema, () => {
3895
- composer.setPanelSubmitEnabled(panel.isComplete());
3896
- });
3897
- const footer = document.createElement("div");
3898
- footer.className = "aparte-elic-footer";
4116
+ const cfg = resolveConfig(this);
4117
+ const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, request.schema, () => {
4118
+ composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
4119
+ }));
3899
4120
  const skip = document.createElement("button");
3900
4121
  skip.type = "button";
3901
4122
  skip.className = "aparte-elic-skip";
3902
- skip.textContent = "Skip";
4123
+ skip.textContent = cfg.t("elicitationSkip");
3903
4124
  skip.addEventListener("click", () => settle({ action: "decline" }));
3904
- footer.appendChild(skip);
3905
- panel.el.appendChild(footer);
4125
+ panel.dismiss.appendChild(skip);
3906
4126
  this._pending = { settle, composer };
3907
4127
  composer.showPanel(panel.el, {
3908
- submitEnabled: panel.isComplete(),
4128
+ submitEnabled: panel.canProceed(),
4129
+ mode: panel.mode(),
3909
4130
  onSubmit: () => {
4131
+ if (panel.mode() === "advance") {
4132
+ panel.proceed();
4133
+ composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
4134
+ return;
4135
+ }
3910
4136
  if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
3911
4137
  }
3912
4138
  });
@@ -3916,43 +4142,56 @@ class AparteElicitation extends HTMLElement {
3916
4142
  _cancelPending() {
3917
4143
  this._pending?.settle({ action: "cancel" });
3918
4144
  }
4145
+ /**
4146
+ * The host id of the chat whose composer holds the open panel.
4147
+ *
4148
+ * Walks up from the composer rather than from `this`, because the panel lives in
4149
+ * the composer and that is what the turn belongs to. Matches the hosts
4150
+ * `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
4151
+ * Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
4152
+ * wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
4153
+ * resolves `undefined` on three wrappers out of four.
4154
+ */
4155
+ _pendingTargetId() {
4156
+ let el = this._pending?.composer ?? null;
4157
+ while (el) {
4158
+ const tag = el.tagName?.toLowerCase();
4159
+ const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
4160
+ if (isHost && el.id) return el.id;
4161
+ el = el.parentElement;
4162
+ }
4163
+ return void 0;
4164
+ }
4165
+ /**
4166
+ * The composer to present in: the nearest one in an ancestor subtree, and
4167
+ * nothing else.
4168
+ *
4169
+ * There used to be a `document.querySelector('aparte-composer')` fallback, which
4170
+ * is the "first chat on the page" bug this repo has now fixed in four other
4171
+ * places: on a page with two chats, an elicitation that could not find its own
4172
+ * composer mounted its panel in the OTHER chat's — so one conversation's question
4173
+ * appeared under the other conversation, and answering it resolved a tool call
4174
+ * belonging to a chat the user was not looking at.
4175
+ *
4176
+ * Returning `null` instead resolves `cancel`, which is honest: nothing was
4177
+ * shown, so nothing was answered. The warning names the fix, because this is a
4178
+ * setup mistake and only the developer can correct it — the guide's own example
4179
+ * puts `<aparte-elicitation>` inside `<aparte-chat>`.
4180
+ */
3919
4181
  _getComposer() {
3920
4182
  let node = this.parentElement;
3921
4183
  while (node) {
3922
4184
  const composer = node.querySelector("aparte-composer");
3923
4185
  if (composer && typeof composer.showPanel === "function") return composer;
4186
+ const tag = node.tagName?.toLowerCase();
4187
+ const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
4188
+ if (isChatBoundary) break;
3924
4189
  node = node.parentElement;
3925
4190
  }
3926
- const doc = document.querySelector("aparte-composer");
3927
- return doc && typeof doc.showPanel === "function" ? doc : null;
3928
- }
3929
- _injectStyles() {
3930
- const id = "aparte-elicitation-styles";
3931
- if (document.getElementById(id)) return;
3932
- const style = document.createElement("style");
3933
- style.id = id;
3934
- style.textContent = `
3935
- .aparte-elic-panel { display:flex; flex-direction:column; gap:6px; padding:6px 4px; width:100%; box-sizing:border-box; max-height:50vh; overflow-y:auto; }
3936
- .aparte-elic-message { margin:0 6px 6px; font-size:.82rem; font-weight:600; color:var(--aparte-text); line-height:1.4; }
3937
- .aparte-elic-field + .aparte-elic-field { margin-top:8px; padding-top:8px; border-top:1px solid var(--aparte-border, rgba(128,128,128,0.12)); }
3938
- .aparte-elic-title { margin:0 6px 4px; font-size:.8rem; font-weight:600; color:var(--aparte-text); }
3939
- .aparte-elic-desc { margin:0 6px 6px; font-size:.76rem; color:var(--aparte-text-muted, rgba(128,128,128,0.75)); }
3940
- .aparte-elic-options { display:flex; flex-direction:column; gap:1px; }
3941
- .aparte-elic-option { display:flex; align-items:flex-start; gap:10px; padding:7px 10px; border-radius:8px; cursor:pointer; border:1px solid transparent; transition:background .12s,border-color .12s; }
3942
- .aparte-elic-option:hover { background:var(--aparte-surface-2, rgba(128,128,128,0.08)); border-color:var(--aparte-border, rgba(128,128,128,0.15)); }
3943
- .aparte-elic-control { margin-top:3px; flex-shrink:0; accent-color:var(--aparte-primary, #6366f1); width:15px; height:15px; cursor:pointer; }
3944
- .aparte-elic-option-body { display:flex; flex-direction:column; gap:2px; flex:1; }
3945
- .aparte-elic-option-title { font-size:.875rem; font-weight:500; color:var(--aparte-text, inherit); line-height:1.4; }
3946
- .aparte-elic-option-desc { font-size:.78rem; color:var(--aparte-text-muted, rgba(128,128,128,0.75)); line-height:1.4; }
3947
- .aparte-elic-other-input, .aparte-elic-text { margin-top:4px; width:100%; padding:6px 8px; border:1px solid var(--aparte-border, rgba(128,128,128,0.2)); border-radius:6px; font-size:.875rem; background:var(--aparte-surface-2, rgba(128,128,128,0.06)); color:var(--aparte-text, inherit); outline:none; box-sizing:border-box; font-family:inherit; }
3948
- textarea.aparte-elic-text { min-height:64px; resize:vertical; }
3949
- .aparte-elic-other-input:focus, .aparte-elic-text:focus { border-color:var(--aparte-primary, #6366f1); }
3950
- .aparte-elic-option--recommended { border-color:var(--aparte-primary, #6366f1) !important; background:color-mix(in srgb, var(--aparte-primary, #6366f1) 6%, transparent); }
3951
- .aparte-elic-footer { display:flex; justify-content:flex-end; margin-top:6px; }
3952
- .aparte-elic-skip { border:none; background:transparent; color:var(--aparte-text-muted, rgba(128,128,128,0.75)); font-size:.8rem; cursor:pointer; padding:4px 8px; border-radius:6px; font-family:inherit; }
3953
- .aparte-elic-skip:hover { background:var(--aparte-surface-2, rgba(128,128,128,0.08)); color:var(--aparte-text); }
3954
- `;
3955
- document.head.appendChild(style);
4191
+ console.warn(
4192
+ "[aparte-elicitation] No <aparte-composer> in this element's subtree, so the request was cancelled — the model will read that as a refusal. Move <aparte-elicitation> inside the <aparte-chat> it belongs to. It is deliberately NOT borrowing another chat's composer: on a page with two chats that put the question under the wrong one."
4193
+ );
4194
+ return null;
3956
4195
  }
3957
4196
  }
3958
4197
  if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
@@ -3969,13 +4208,20 @@ function registerAllComponents() {
3969
4208
  }
3970
4209
  export {
3971
4210
  A as APARTE_CONVERSATION_SCHEMA_VERSION,
3972
- a as APARTE_HOST_ATTR,
4211
+ a as APARTE_DEFAULT_BUBBLE_ACTIONS,
4212
+ b as APARTE_DEFAULT_HOST_HANDLERS,
4213
+ c as APARTE_DEFAULT_ICON_FALLBACKS,
4214
+ d as APARTE_DEFAULT_LOCALE,
4215
+ e as APARTE_DEFAULT_SKELETON_FALLBACKS,
4216
+ f as APARTE_DEFAULT_UI_EVENTS,
4217
+ g as APARTE_HOST_ATTR,
4218
+ h as AparteBackendTransport,
3973
4219
  AparteChat,
3974
4220
  AparteChatBubble,
3975
- b as AparteChatHost,
4221
+ i as AparteChatHost,
3976
4222
  AparteChatStatus,
3977
4223
  AparteChatViewport,
3978
- c as AparteClient,
4224
+ j as AparteClient,
3979
4225
  AparteComposer,
3980
4226
  AparteComposerAction,
3981
4227
  AparteComposerAddAttachment,
@@ -3984,54 +4230,55 @@ export {
3984
4230
  AparteComposerInput,
3985
4231
  AparteComposerSend,
3986
4232
  AparteComposerToolbar,
3987
- AparteConfig,
3988
- e as AparteConfigClass,
3989
- f as AparteConversationController,
4233
+ k as AparteConfig,
4234
+ l as AparteConversationController,
3990
4235
  AparteConversationList,
4236
+ m as AparteConversationManager,
4237
+ n as AparteDirectTransport,
3991
4238
  AparteElicitation,
3992
- g as AparteError,
3993
- h as AparteErrorCode,
4239
+ o as AparteError,
4240
+ p as AparteErrorCode,
4241
+ AparteMessageRepository,
3994
4242
  AparteOptgroup,
3995
4243
  AparteOption,
3996
4244
  AparteProgressSpinner,
3997
4245
  AparteSelect,
3998
- i as AparteStreamParser,
3999
- B as BackendTransport,
4000
- C as ConversationManager,
4001
- D as DEFAULT_BUBBLE_ACTIONS,
4002
- j as DEFAULT_HOST_HANDLERS,
4003
- k as DEFAULT_ICON_FALLBACKS,
4004
- l as DEFAULT_LOCALE,
4005
- m as DEFAULT_SKELETON_FALLBACKS,
4006
- n as DEFAULT_UI_EVENTS,
4007
- o as DirectTransport,
4008
- MessageRepository,
4009
- p as applyElementProps,
4010
- q as attachConfig,
4246
+ r as AparteStreamParser,
4247
+ s as aparteGlobalConfig,
4248
+ t as applyElementProps,
4249
+ u as attachConfig,
4011
4250
  buildElicitationPanel,
4012
- s as collectRendererStyles,
4013
- t as contentToText,
4014
- u as contextConfig,
4015
- v as createAparteChatHandler,
4016
- w as createStreamAdapter,
4017
- x as defaultSanitizer,
4018
- y as deriveArtifactKind,
4019
- z as detachConfig,
4020
- E as filesToAttachments,
4251
+ w as collectRendererStyles,
4252
+ x as contentToText,
4253
+ y as contextConfig,
4254
+ z as createAparteChatHandler,
4255
+ B as createStreamAdapter,
4256
+ cssEscape,
4257
+ D as declineDefaultRenderers,
4258
+ E as defaultSanitizer,
4259
+ F as deriveArtifactKind,
4260
+ G as detachConfig,
4261
+ escapeAttr,
4262
+ escapeHtml,
4263
+ J as filesToAttachments,
4264
+ K as getAllRenderers,
4021
4265
  getSegmentRenderer,
4266
+ installDefaultRenderersOnce,
4022
4267
  isAwaitingReply,
4023
- H as isFormatAdapter,
4024
- I as isSafeUrl,
4025
- J as parseAparteEventStream,
4026
- K as parseMarkdownToSegments,
4268
+ O as isFormatAdapter,
4269
+ P as isSafeUrl,
4270
+ Q as parseAparteEventStream,
4271
+ R as parseMarkdownToSegments,
4027
4272
  populateBubbleFromMessage,
4028
- N as readableToAsyncIterable,
4273
+ T as readableToAsyncIterable,
4029
4274
  registerAllComponents,
4030
- O as registerDefaultRenderers,
4031
- P as registerSegmentRenderer,
4032
- Q as requestUserInput,
4275
+ U as registerDefaultRenderers,
4276
+ V as registerSegmentRenderer,
4277
+ W as requestUserInput,
4033
4278
  resolveConfig,
4279
+ revokeAttachmentUrls,
4034
4280
  runWithConfig,
4035
- T as unregisterSegmentRenderer
4281
+ _ as unregisterSegmentRenderer,
4282
+ uuid
4036
4283
  };
4037
4284
  //# sourceMappingURL=index.js.map