@aparte/core 0.10.0 → 0.12.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 (85) hide show
  1. package/dist/client/aparte-client.d.ts +54 -33
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/stream-adapter.d.ts +6 -1
  4. package/dist/client/stream-adapter.d.ts.map +1 -1
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts +25 -4
  6. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  7. package/dist/components/chat/aparte-chat.d.ts +8 -4
  8. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-action.d.ts +15 -6
  10. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts +5 -4
  12. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer-attachments.d.ts +5 -3
  14. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  15. package/dist/components/composer/aparte-composer-cancel.d.ts +2 -2
  16. package/dist/components/composer/aparte-composer-input.d.ts +8 -5
  17. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer-send.d.ts +2 -2
  19. package/dist/components/composer/aparte-composer-toolbar.d.ts +3 -3
  20. package/dist/components/composer/aparte-composer.d.ts +81 -10
  21. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  22. package/dist/components/conversation-list/aparte-conversation-list.d.ts +19 -12
  23. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  24. package/dist/components/elicitation/aparte-elicitation.d.ts +14 -2
  25. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  26. package/dist/components/status/aparte-chat-status.d.ts +9 -0
  27. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  28. package/dist/components/viewport/aparte-chat-viewport.d.ts +15 -4
  29. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  30. package/dist/config/aparte-config.d.ts +72 -15
  31. package/dist/config/aparte-config.d.ts.map +1 -1
  32. package/dist/config/locale.d.ts +43 -0
  33. package/dist/config/locale.d.ts.map +1 -1
  34. package/dist/custom-elements.json +2824 -1801
  35. package/dist/elicitation/approval-panel.d.ts +42 -0
  36. package/dist/elicitation/approval-panel.d.ts.map +1 -0
  37. package/dist/elicitation/index.d.ts +4 -1
  38. package/dist/elicitation/index.d.ts.map +1 -1
  39. package/dist/elicitation/panel.d.ts +1 -1
  40. package/dist/elicitation/panel.d.ts.map +1 -1
  41. package/dist/elicitation/types.d.ts +116 -9
  42. package/dist/elicitation/types.d.ts.map +1 -1
  43. package/dist/generated/element-attributes.d.ts +177 -0
  44. package/dist/generated/element-attributes.d.ts.map +1 -0
  45. package/dist/index.css +621 -92
  46. package/dist/index.d.ts +4 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +368 -255
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.node.d.ts +4 -3
  51. package/dist/index.node.d.ts.map +1 -1
  52. package/dist/index.node.js +46 -44
  53. package/dist/index.node.js.map +1 -1
  54. package/dist/interop/element-props.d.ts +40 -2
  55. package/dist/interop/element-props.d.ts.map +1 -1
  56. package/dist/{index-BnKBciDG.js → is-awaiting-reply-CxWRISBC.js} +949 -957
  57. package/dist/is-awaiting-reply-CxWRISBC.js.map +1 -0
  58. package/dist/primitives/select/aparte-optgroup.d.ts +3 -0
  59. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  60. package/dist/primitives/select/aparte-option.d.ts +1 -0
  61. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  62. package/dist/primitives/select/aparte-select.d.ts +22 -18
  63. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  64. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  65. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  66. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
  67. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  68. package/dist/types/element-attributes.d.ts +42 -0
  69. package/dist/types/element-attributes.d.ts.map +1 -0
  70. package/dist/types/event-map.d.ts +1 -2
  71. package/dist/types/event-map.d.ts.map +1 -1
  72. package/dist/types/index.d.ts +3 -2
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/dist/types/segments.d.ts +1 -8
  75. package/dist/types/segments.d.ts.map +1 -1
  76. package/dist/types/tools.d.ts +26 -23
  77. package/dist/types/tools.d.ts.map +1 -1
  78. package/dist/utils/chat-boundary.d.ts +22 -0
  79. package/dist/utils/chat-boundary.d.ts.map +1 -0
  80. package/dist/utils/segments.d.ts +8 -0
  81. package/dist/utils/segments.d.ts.map +1 -1
  82. package/package.json +3 -2
  83. package/dist/index-BnKBciDG.js.map +0 -1
  84. package/dist/renderers/segments/progress.d.ts +0 -3
  85. package/dist/renderers/segments/progress.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { I as escapeAttr, D as cssEscape, $ as runWithConfig, Z as resolveConfig, a5 as mergeSegmentUpdate, J as escapeHtml, M as getSegmentRenderer, N as installDefaultRenderersOnce, r as AparteMessageRepository, a6 as stampSegmentActivity, a7 as stampSegmentOnInsert, a8 as stampSegmentOnUpdate, a9 as renumberSegments, aa as openSegmentIds, ab as isTerminalStatus, ac as adoptMessageSegments, O as isAwaitingReply, U as populateBubbleFromMessage, a4 as uuid, _ as revokeAttachmentUrls, a2 as subscribeConfigChange, w as buildElicitationPanel } from "./index-BnKBciDG.js";
2
- import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, s, t, u, v, x, y, z, B, C, E, F, G, H, K, L, P, Q, R, S, T, V, W, X, Y, a0, a1, a3 } from "./index-BnKBciDG.js";
1
+ import { a4 as subscribeConfigChange, $ as resolveConfig, a1 as runWithConfig, p as AparteElicitationAbortError, x as buildApprovalPanel, y as buildElicitationPanel, K as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, L as escapeHtml, O as getSegmentRenderer, P as installDefaultRenderersOnce, s as AparteMessageRepository, a8 as stampSegmentActivity, a9 as stampSegmentOnInsert, aa as stampSegmentOnUpdate, ab as renumberSegments, ac as openSegmentIds, ad as isTerminalStatus, ae as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls } from "./is-awaiting-reply-CxWRISBC.js";
2
+ import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, q, r, t, u, v, w, z, B, C, D, E, G, H, I, J, M, N, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./is-awaiting-reply-CxWRISBC.js";
3
3
  class AparteOption extends HTMLElement {
4
4
  static get observedAttributes() {
5
5
  return ["value", "disabled", "selected", "data-status"];
@@ -631,6 +631,221 @@ class AparteProgressSpinner extends HTMLElement {
631
631
  if (!customElements.get("aparte-progress-spinner")) {
632
632
  customElements.define("aparte-progress-spinner", AparteProgressSpinner);
633
633
  }
634
+ class AparteElicitation extends HTMLElement {
635
+ _pending = null;
636
+ /**
637
+ * A turn ended — cancel the open question only if it was OUR turn.
638
+ *
639
+ * These two listeners sit on `window` and had no instance filter at all, so a
640
+ * Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
641
+ * waiting on — and that chat's model was told the user had refused a question
642
+ * the user was still looking at. Same defect as the `compact()` handler that
643
+ * emptied both chats, which its four sibling handlers in `AparteClient` already
644
+ * guarded against.
645
+ *
646
+ * The leniency rule is the composer's, deliberately: an event with no
647
+ * `targetId` is for everyone (a single-chat app never sets one), and a chat we
648
+ * cannot identify accepts everything rather than becoming deaf. Only a
649
+ * MISMATCH is ignored.
650
+ */
651
+ _onTurnEnd = (e2) => {
652
+ const evtTargetId = e2.detail?.targetId;
653
+ const own = this._pendingTargetId();
654
+ if (evtTargetId && own && evtTargetId !== own) return;
655
+ this._cancelPending();
656
+ };
657
+ _unsubscribeConfig = null;
658
+ connectedCallback() {
659
+ this.style.display = "none";
660
+ this._unsubscribeConfig = subscribeConfigChange(this, () => this._relabelPending());
661
+ resolveConfig(this).setElicitationPresenter(this._present, this);
662
+ window.addEventListener("aparte-message-aborted", this._onTurnEnd);
663
+ window.addEventListener("aparte-message-error", this._onTurnEnd);
664
+ }
665
+ /**
666
+ * The boundary above us appeared, changed, or went away — move the
667
+ * registration with it.
668
+ *
669
+ * `connectedCallback` alone is not enough and cannot be: registering is a
670
+ * WRITE, and under all four wrappers it happens before `attachConfig` runs, so
671
+ * it lands on the global singleton. `requestUserInput()` then resolves the
672
+ * instance config, finds nothing, and rejects the request — the model
673
+ * hears the user refuse a question the user never saw.
674
+ *
675
+ * See {@link AparteConfigAware}.
676
+ */
677
+ aparteConfigChanged(next, previous) {
678
+ previous.removeElicitationPresenter(this._present);
679
+ next.setElicitationPresenter(this._present, this);
680
+ }
681
+ disconnectedCallback() {
682
+ resolveConfig(this).removeElicitationPresenter(this._present);
683
+ window.removeEventListener("aparte-message-aborted", this._onTurnEnd);
684
+ window.removeEventListener("aparte-message-error", this._onTurnEnd);
685
+ this._unsubscribeConfig?.();
686
+ this._unsubscribeConfig = null;
687
+ this._cancelPending();
688
+ }
689
+ /**
690
+ * Re-apply the open question's strings, in place.
691
+ *
692
+ * Two owners, and both have to move or the panel goes bilingual: the panel's own
693
+ * defaults (`relabel`), and the Skip button, which this file builds and this file
694
+ * therefore has to re-text. The composer's one button is a third, and it already
695
+ * follows — `aparte-composer-send` remembers the panel state it was given.
696
+ */
697
+ _relabelPending() {
698
+ if (!this._pending) return;
699
+ const cfg = resolveConfig(this);
700
+ runWithConfig(cfg, () => this._pending.relabel());
701
+ }
702
+ _present = (request) => {
703
+ const composer = this._getComposer();
704
+ if (!composer) return Promise.reject(new AparteElicitationAbortError("no-presenter"));
705
+ return new Promise((resolve, reject) => {
706
+ let done = false;
707
+ const slot = {};
708
+ const close = () => {
709
+ if (done) return false;
710
+ done = true;
711
+ this._pending = null;
712
+ composer.hidePanel(slot.token);
713
+ return true;
714
+ };
715
+ const settle = (result) => {
716
+ if (close()) resolve(result);
717
+ };
718
+ const fail = (reason = "aborted") => {
719
+ if (close()) reject(new AparteElicitationAbortError(reason));
720
+ };
721
+ if (request.signal) {
722
+ if (request.signal.aborted) {
723
+ fail();
724
+ return;
725
+ }
726
+ request.signal.addEventListener("abort", () => fail(), { once: true });
727
+ }
728
+ const cfg = resolveConfig(this);
729
+ if (request.kind === "approval") {
730
+ const panel2 = runWithConfig(cfg, () => buildApprovalPanel(request.message, request.options ?? [], () => {
731
+ composer.setPanelSubmitEnabled(panel2.isComplete(), "submit");
732
+ }));
733
+ panel2.onSettle((answer) => settle({ action: "accept", content: answer }));
734
+ this._pending = { abort: () => fail(), composer, relabel: () => panel2.relabel() };
735
+ slot.token = composer.showPanel(panel2.el, {
736
+ submitEnabled: panel2.isComplete(),
737
+ mode: "submit",
738
+ // The composer's button carries the INSTRUCTION, which is written
739
+ // text — exactly the act that button already means. The options
740
+ // never route through it: a decision is its own click.
741
+ onSubmit: () => {
742
+ if (panel2.isComplete()) settle({ action: "accept", content: panel2.getContent() });
743
+ },
744
+ onEvict: () => fail()
745
+ });
746
+ panel2.focus();
747
+ return;
748
+ }
749
+ const schema = request.schema ?? { type: "string" };
750
+ const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, schema, () => {
751
+ composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
752
+ }));
753
+ const skip = document.createElement("button");
754
+ skip.type = "button";
755
+ skip.className = "aparte-elic-skip";
756
+ skip.textContent = cfg.t("elicitationSkip");
757
+ skip.addEventListener("click", () => settle({ action: "decline" }));
758
+ panel.dismiss.appendChild(skip);
759
+ this._pending = {
760
+ abort: () => fail(),
761
+ composer,
762
+ relabel: () => {
763
+ panel.relabel();
764
+ skip.textContent = resolveConfig(this).t("elicitationSkip");
765
+ }
766
+ };
767
+ slot.token = composer.showPanel(panel.el, {
768
+ submitEnabled: panel.canProceed(),
769
+ mode: panel.mode(),
770
+ /*
771
+ * Something else took the slot — another request, a conversation switch,
772
+ * or a turn ending. The composer tears the panel down either way; only
773
+ * this callback can settle the promise, and without it the request hung
774
+ * AND `_pending` stayed set, so every later question was short-circuited
775
+ * for the life of the page. `cancel`, not `decline`: nobody declined
776
+ * anything, the question was taken away.
777
+ */
778
+ onEvict: () => fail(),
779
+ onSubmit: () => {
780
+ if (panel.mode() === "advance") {
781
+ panel.proceed();
782
+ composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
783
+ return;
784
+ }
785
+ if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
786
+ }
787
+ });
788
+ panel.focus();
789
+ });
790
+ };
791
+ _cancelPending() {
792
+ this._pending?.abort();
793
+ }
794
+ /**
795
+ * The host id of the chat whose composer holds the open panel.
796
+ *
797
+ * Walks up from the composer rather than from `this`, because the panel lives in
798
+ * the composer and that is what the turn belongs to. Matches the hosts
799
+ * `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
800
+ * Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
801
+ * wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
802
+ * resolves `undefined` on three wrappers out of four.
803
+ */
804
+ _pendingTargetId() {
805
+ let el = this._pending?.composer ?? null;
806
+ while (el) {
807
+ const tag = el.tagName?.toLowerCase();
808
+ const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
809
+ if (isHost && el.id) return el.id;
810
+ el = el.parentElement;
811
+ }
812
+ return void 0;
813
+ }
814
+ /**
815
+ * The composer to present in: the nearest one in an ancestor subtree, and
816
+ * nothing else.
817
+ *
818
+ * There used to be a `document.querySelector('aparte-composer')` fallback, which
819
+ * is the "first chat on the page" bug this repo has now fixed in four other
820
+ * places: on a page with two chats, an elicitation that could not find its own
821
+ * composer mounted its panel in the OTHER chat's — so one conversation's question
822
+ * appeared under the other conversation, and answering it resolved a tool call
823
+ * belonging to a chat the user was not looking at.
824
+ *
825
+ * Returning `null` instead REJECTS the request, which is honest: nothing was
826
+ * shown, so nothing was answered. The warning names the fix, because this is a
827
+ * setup mistake and only the developer can correct it — the guide's own example
828
+ * puts `<aparte-elicitation>` inside `<aparte-chat>`.
829
+ */
830
+ _getComposer() {
831
+ let node = this.parentElement;
832
+ while (node) {
833
+ const composer = node.querySelector("aparte-composer");
834
+ if (composer && typeof composer.showPanel === "function") return composer;
835
+ const tag = node.tagName?.toLowerCase();
836
+ const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
837
+ if (isChatBoundary) break;
838
+ node = node.parentElement;
839
+ }
840
+ console.warn(
841
+ "[aparte-elicitation] No <aparte-composer> in this element's subtree, so the request could not be shown, so it REJECTED and the turn halted. Nothing was told to the model — there is nothing true to tell it. 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."
842
+ );
843
+ return null;
844
+ }
845
+ }
846
+ if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
847
+ customElements.define("aparte-elicitation", AparteElicitation);
848
+ }
634
849
  class AparteChat extends HTMLElement {
635
850
  static get observedAttributes() {
636
851
  return ["placeholder", "disabled", "center-empty", "attachments"];
@@ -683,6 +898,7 @@ class AparteChat extends HTMLElement {
683
898
  const attachments = this.hasAttribute("attachments");
684
899
  this.innerHTML = `
685
900
  <aparte-chat-viewport></aparte-chat-viewport>
901
+ <aparte-elicitation></aparte-elicitation>
686
902
  <aparte-composer${composerAttrs}>
687
903
  <div class="aparte-composer-shell">
688
904
  ${attachments ? "<aparte-composer-attachments></aparte-composer-attachments>" : ""}
@@ -935,7 +1151,7 @@ class AparteChatBubble extends HTMLElement {
935
1151
  }
936
1152
  /** Update a specific segment */
937
1153
  updateSegment(segmentId, updates) {
938
- const index = this._segments.findIndex((s2) => s2.id === segmentId);
1154
+ const index = this._segments.findIndex((s) => s.id === segmentId);
939
1155
  if (index !== -1) {
940
1156
  const updated = mergeSegmentUpdate(this._segments[index], updates);
941
1157
  this._segments[index] = updated;
@@ -944,7 +1160,7 @@ class AparteChatBubble extends HTMLElement {
944
1160
  }
945
1161
  /** Append content to a segment */
946
1162
  appendToSegment(segmentId, content) {
947
- const segment = this._segments.find((s2) => s2.id === segmentId);
1163
+ const segment = this._segments.find((s) => s.id === segmentId);
948
1164
  if (segment && "content" in segment) {
949
1165
  segment.content += content;
950
1166
  this._applySegmentUpdate(segmentId, segment, { content: segment.content });
@@ -970,7 +1186,7 @@ class AparteChatBubble extends HTMLElement {
970
1186
  * the human-in-the-loop control.
971
1187
  */
972
1188
  removeSegment(segmentId) {
973
- const index = this._segments.findIndex((s2) => s2.id === segmentId);
1189
+ const index = this._segments.findIndex((s) => s.id === segmentId);
974
1190
  if (index !== -1) {
975
1191
  this._segments.splice(index, 1);
976
1192
  }
@@ -1053,7 +1269,7 @@ class AparteChatBubble extends HTMLElement {
1053
1269
  } else {
1054
1270
  warnMissingRenderer(segment.type);
1055
1271
  const fallback = document.createElement("div");
1056
- fallback.className = "segment segment-unknown";
1272
+ fallback.className = "aparte-segment aparte-segment-unknown";
1057
1273
  fallback.textContent = `[Unknown segment type: ${segment.type}]`;
1058
1274
  this._segmentsEl.appendChild(fallback);
1059
1275
  }
@@ -1282,7 +1498,7 @@ class AparteChatBubble extends HTMLElement {
1282
1498
  } else {
1283
1499
  warnMissingRenderer(segment.type);
1284
1500
  const fallback = document.createElement("div");
1285
- fallback.className = "segment segment-unknown";
1501
+ fallback.className = "aparte-segment aparte-segment-unknown";
1286
1502
  fallback.textContent = `[Unknown segment type: ${segment.type}]`;
1287
1503
  this._segmentsEl.appendChild(fallback);
1288
1504
  }
@@ -1302,7 +1518,7 @@ class AparteChatBubble extends HTMLElement {
1302
1518
  _reflectError() {
1303
1519
  const message = this.querySelector(".aparte-message");
1304
1520
  if (!message) return;
1305
- const hasError = this._segments.some((s2) => s2.type === "error");
1521
+ const hasError = this._segments.some((s) => s.type === "error");
1306
1522
  if (hasError) message.setAttribute("data-error", "");
1307
1523
  else message.removeAttribute("data-error");
1308
1524
  }
@@ -1337,18 +1553,18 @@ class AparteChatBubble extends HTMLElement {
1337
1553
  const customAttachment = this._cfg.getAttachmentRenderer?.();
1338
1554
  if (customAttachment) {
1339
1555
  this._attachmentsEl.replaceChildren();
1340
- for (const a2 of this._attachments) {
1341
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a2)));
1556
+ for (const a4 of this._attachments) {
1557
+ const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a4)));
1342
1558
  if (el) this._attachmentsEl.appendChild(el);
1343
1559
  }
1344
1560
  return;
1345
1561
  }
1346
- this._attachmentsEl.innerHTML = this._attachments.map((a2) => {
1347
- const name = escapeHtml(a2.name);
1348
- if (a2.type.startsWith("image/")) {
1349
- 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>`;
1562
+ this._attachmentsEl.innerHTML = this._attachments.map((a4) => {
1563
+ const name = escapeHtml(a4.name);
1564
+ if (a4.type.startsWith("image/")) {
1565
+ return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeHtml(a4.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
1350
1566
  }
1351
- 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>`;
1567
+ return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._fileExt(a4.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
1352
1568
  }).join("");
1353
1569
  if (!this._cfg.getHostHandlers().attachmentPreview) return;
1354
1570
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
@@ -1453,14 +1669,14 @@ class AparteChatBubble extends HTMLElement {
1453
1669
  const buttons = [];
1454
1670
  if (this._role === "user") {
1455
1671
  if (config.user) {
1456
- for (const a2 of config.user) buttons.push(this._actionButtonHtml(a2, icons, locale));
1672
+ for (const a4 of config.user) buttons.push(this._actionButtonHtml(a4, icons, locale));
1457
1673
  } else {
1458
1674
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
1459
1675
  if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
1460
1676
  }
1461
1677
  } else if (this._role === "assistant") {
1462
1678
  if (config.assistant) {
1463
- for (const a2 of config.assistant) buttons.push(this._actionButtonHtml(a2, icons, locale));
1679
+ for (const a4 of config.assistant) buttons.push(this._actionButtonHtml(a4, icons, locale));
1464
1680
  } else {
1465
1681
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
1466
1682
  if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
@@ -1495,16 +1711,16 @@ class AparteChatBubble extends HTMLElement {
1495
1711
  /** Append the registered custom action buttons for this bubble's role. */
1496
1712
  _appendCustomActions(icons) {
1497
1713
  if (!this._actionBarEl) return;
1498
- for (const a2 of this._cfg.getActions("bubble")) {
1499
- const roles = a2.bubble?.roles ?? ["user", "assistant"];
1714
+ for (const a4 of this._cfg.getActions("bubble")) {
1715
+ const roles = a4.bubble?.roles ?? ["user", "assistant"];
1500
1716
  if (!roles.includes(this._role)) continue;
1501
1717
  const btn = document.createElement("button");
1502
1718
  btn.className = "aparte-action-btn aparte-action-custom";
1503
- btn.dataset["action"] = `custom:${a2.id}`;
1504
- btn.setAttribute("aria-label", a2.label);
1505
- btn.setAttribute("title", a2.label);
1506
- const fromProvider = icons[a2.icon];
1507
- btn.innerHTML = a2.icon.startsWith("<") ? a2.icon : typeof fromProvider === "function" ? fromProvider() : a2.iconFallback ?? "";
1719
+ btn.dataset["action"] = `custom:${a4.id}`;
1720
+ btn.setAttribute("aria-label", a4.label);
1721
+ btn.setAttribute("title", a4.label);
1722
+ const fromProvider = icons[a4.icon];
1723
+ btn.innerHTML = a4.icon.startsWith("<") ? a4.icon : typeof fromProvider === "function" ? fromProvider() : a4.iconFallback ?? "";
1508
1724
  this._actionBarEl.appendChild(btn);
1509
1725
  }
1510
1726
  }
@@ -1570,12 +1786,12 @@ class AparteChatBubble extends HTMLElement {
1570
1786
  composed: true,
1571
1787
  detail
1572
1788
  }));
1573
- this._cfg.getActions("bubble").find((x2) => x2.id === actionId)?.onClick?.(e2);
1789
+ this._cfg.getActions("bubble").find((x) => x.id === actionId)?.onClick?.(e2);
1574
1790
  return;
1575
1791
  }
1576
1792
  switch (action) {
1577
1793
  case "copy": {
1578
- const text = this._content || this._segments.map((s2) => s2.content ?? "").join("\n");
1794
+ const text = this._content || this._segments.map((s) => s.content ?? "").join("\n");
1579
1795
  const icons = this._cfg.getIconProvider();
1580
1796
  const locale = this._cfg.getLocale();
1581
1797
  navigator.clipboard.writeText(text).then(() => {
@@ -2013,7 +2229,7 @@ class AparteChatViewport extends HTMLElement {
2013
2229
  if (!message.segments) {
2014
2230
  message.segments = [];
2015
2231
  }
2016
- const index = message.segments.findIndex((s2) => s2.id === segmentId);
2232
+ const index = message.segments.findIndex((s) => s.id === segmentId);
2017
2233
  const segment = index === -1 ? void 0 : message.segments[index];
2018
2234
  if (segment && "content" in segment) {
2019
2235
  message.segments[index] = {
@@ -2075,15 +2291,15 @@ class AparteChatViewport extends HTMLElement {
2075
2291
  this._notifyBubble(messageId, "addSegment", stamped);
2076
2292
  this._autoScroll();
2077
2293
  }
2078
- updateSegment(a2, b2, c2) {
2294
+ updateSegment(a4, b2, c2) {
2079
2295
  const clientForm = c2 === void 0 && typeof b2 === "object";
2080
- const messageId = clientForm ? this._activeMessageId() : a2;
2081
- const segmentId = clientForm ? a2 : b2;
2296
+ const messageId = clientForm ? this._activeMessageId() : a4;
2297
+ const segmentId = clientForm ? a4 : b2;
2082
2298
  const updates = clientForm ? b2 : c2;
2083
2299
  if (!messageId) return;
2084
2300
  const message = this._repo.getMessageById(messageId);
2085
2301
  if (!message?.segments) return;
2086
- const segmentIndex = message.segments.findIndex((s2) => s2.id === segmentId);
2302
+ const segmentIndex = message.segments.findIndex((s) => s.id === segmentId);
2087
2303
  if (segmentIndex !== -1) {
2088
2304
  const current = message.segments[segmentIndex];
2089
2305
  const stamped = stampSegmentOnUpdate(current, updates);
@@ -2091,14 +2307,14 @@ class AparteChatViewport extends HTMLElement {
2091
2307
  this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
2092
2308
  }
2093
2309
  }
2094
- removeSegment(a2, b2) {
2310
+ removeSegment(a4, b2) {
2095
2311
  const clientForm = b2 === void 0;
2096
- const messageId = clientForm ? this._activeMessageId() : a2;
2097
- const segmentId = clientForm ? a2 : b2;
2312
+ const messageId = clientForm ? this._activeMessageId() : a4;
2313
+ const segmentId = clientForm ? a4 : b2;
2098
2314
  if (!messageId || !segmentId) return;
2099
2315
  const message = this._repo.getMessageById(messageId);
2100
2316
  if (message?.segments) {
2101
- const idx = message.segments.findIndex((s2) => s2.id === segmentId);
2317
+ const idx = message.segments.findIndex((s) => s.id === segmentId);
2102
2318
  if (idx !== -1) {
2103
2319
  message.segments.splice(idx, 1);
2104
2320
  renumberSegments(message.segments);
@@ -2930,6 +3146,20 @@ class AparteComposer extends HTMLElement {
2930
3146
  _panelMode = "submit";
2931
3147
  _panelSubmitEnabled = false;
2932
3148
  _panelOnSubmit = null;
3149
+ /**
3150
+ * Who owns the one panel slot, and how to tell them they lost it.
3151
+ *
3152
+ * There is exactly one slot, `showPanel` empties it unconditionally, and nothing
3153
+ * used to say whose it was. Three paths therefore closed a panel whose owner was
3154
+ * still awaiting an answer, and none of them told the owner: a second `showPanel`,
3155
+ * the owner-of-record's own `hidePanel`, and — the one that actually bit —
3156
+ * `_handleMessageDone`, which fires on EVERY turn end. A question still open when a
3157
+ * turn finished lost its panel while `<aparte-elicitation>` kept `_pending` set,
3158
+ * so the request never settled AND every later request was short-circuited for the
3159
+ * life of the page.
3160
+ */
3161
+ _panelToken = null;
3162
+ _panelOnEvict = null;
2933
3163
  // Internal bus events that represent an observable STATE change — these are
2934
3164
  // mirrored to the public `aparte-composer-change` DOM event. `submit`/`cancel`
2935
3165
  // are actions, not state, and are covered by `aparte-send`/`aparte-cancel`.
@@ -3097,9 +3327,14 @@ class AparteComposer extends HTMLElement {
3097
3327
  * used to set on a child: an attribute is themeable, is visible to a consumer's
3098
3328
  * own rules, and does not clobber a `display` the consumer had set (the restore
3099
3329
  * wrote `''`, not the previous value).
3330
+ *
3331
+ * Returns the TOKEN for this panel. Pass it back to `hidePanel` so a presenter
3332
+ * that has already lost the slot cannot close the panel that replaced it, and
3333
+ * supply `onEvict` to be told when that happens — an owner that is not told is an
3334
+ * owner whose promise nobody can settle.
3100
3335
  */
3101
3336
  showPanel(panel, options) {
3102
- this.hidePanel();
3337
+ this._evictPanel();
3103
3338
  const inputEl = this.querySelector("aparte-composer-input");
3104
3339
  this.setAttribute("data-panel-active", "");
3105
3340
  panel.dataset["apartePanel"] = "true";
@@ -3112,10 +3347,58 @@ class AparteComposer extends HTMLElement {
3112
3347
  this._panelSubmitEnabled = options?.submitEnabled ?? false;
3113
3348
  this._panelMode = options?.mode ?? "submit";
3114
3349
  this._panelOnSubmit = options?.onSubmit ?? null;
3350
+ this._panelOnEvict = options?.onEvict ?? null;
3351
+ const token = Symbol("aparte-composer-panel");
3352
+ this._panelToken = token;
3115
3353
  this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled, mode: this._panelMode });
3354
+ return token;
3116
3355
  }
3117
- /** Remove the panel and restore the composer's own controls. */
3118
- hidePanel() {
3356
+ /**
3357
+ * Remove the panel, tell its owner, and restore the composer's own controls.
3358
+ *
3359
+ * With a `token`, this closes the panel only if that token still owns the slot —
3360
+ * so a presenter settling late cannot tear down the panel that replaced it. With
3361
+ * no token it closes whatever is there, which is what a consumer driving the
3362
+ * composer directly means.
3363
+ *
3364
+ * BOTH forms notify. The no-token form used to call `_teardownPanel` directly,
3365
+ * which nulls `_panelOnEvict` without ever calling it — so the documented public
3366
+ * `hidePanel()` closed an open approval panel and left its request pending
3367
+ * forever. The turn hung on "waiting for you", and because
3368
+ * `AparteConfig.requestUserInput` chains each request on the previous one, NO
3369
+ * further question or approval on that config was ever presented again, for the
3370
+ * life of the page.
3371
+ *
3372
+ * The old JSDoc justified the silent branch as "what `reset()` needs". It was not:
3373
+ * `reset()` calls `_evictPanel()`, which notifies. The branch had no consumer and
3374
+ * one failure mode.
3375
+ *
3376
+ * The two forms differ on purpose, and the difference is who already knows:
3377
+ *
3378
+ * - **With a matching token** the OWNER is closing its own panel, which is what
3379
+ * `<aparte-elicitation>`'s `close()` does right after it resolves. It must NOT be
3380
+ * notified — telling it "you were evicted" for a request it just settled would
3381
+ * fire `onEvict` against a finished promise.
3382
+ * - **With no token** somebody else is closing a panel they do not own, so the owner
3383
+ * cannot know and has to be told. That includes the presenter's own defensive
3384
+ * `hidePanel(undefined)` when it settles before `showPanel` ran: whatever is open
3385
+ * then belongs to another request, and that request must not orphan.
3386
+ */
3387
+ hidePanel(token) {
3388
+ if (token !== void 0) {
3389
+ if (token !== this._panelToken) return;
3390
+ this._teardownPanel();
3391
+ return;
3392
+ }
3393
+ this._evictPanel();
3394
+ }
3395
+ /** Close the panel AND tell its owner, so a pending request never orphans. */
3396
+ _evictPanel() {
3397
+ const onEvict = this._panelOnEvict;
3398
+ this._teardownPanel();
3399
+ onEvict?.();
3400
+ }
3401
+ _teardownPanel() {
3119
3402
  const existing = this.querySelector("[data-aparte-panel]");
3120
3403
  if (existing) existing.remove();
3121
3404
  this.removeAttribute("data-panel-active");
@@ -3123,6 +3406,8 @@ class AparteComposer extends HTMLElement {
3123
3406
  this._panelSubmitEnabled = false;
3124
3407
  this._panelMode = "submit";
3125
3408
  this._panelOnSubmit = null;
3409
+ this._panelOnEvict = null;
3410
+ this._panelToken = null;
3126
3411
  this._emit("panel-change", { active: false, submitEnabled: false, mode: "submit" });
3127
3412
  this.focus();
3128
3413
  }
@@ -3188,7 +3473,7 @@ class AparteComposer extends HTMLElement {
3188
3473
  cancel() {
3189
3474
  this._emit("cancel", {});
3190
3475
  this.dispatchEvent(new CustomEvent("aparte-cancel", { bubbles: true, composed: true }));
3191
- const abortDetail = { targetId: this.targetId ?? void 0 };
3476
+ const abortDetail = { targetId: this._ownTargetId() };
3192
3477
  window.dispatchEvent(new CustomEvent("aparte-abort", { bubbles: false, detail: abortDetail }));
3193
3478
  window.dispatchEvent(new CustomEvent("aparte-message-aborted", { bubbles: false, detail: abortDetail }));
3194
3479
  }
@@ -3200,7 +3485,7 @@ class AparteComposer extends HTMLElement {
3200
3485
  reset() {
3201
3486
  this.setValue("");
3202
3487
  this.clearAttachments();
3203
- if (this._panelActive) this.hidePanel();
3488
+ if (this._panelActive) this._evictPanel();
3204
3489
  }
3205
3490
  /** Focus the input primitive inside this composer. */
3206
3491
  focus() {
@@ -3269,7 +3554,7 @@ class AparteComposer extends HTMLElement {
3269
3554
  if (!this._isForThisComposer(e2)) return;
3270
3555
  this._streaming = false;
3271
3556
  this._emit("streaming-change", { streaming: false });
3272
- if (this._panelActive) this.hidePanel();
3557
+ if (this._panelActive) this._evictPanel();
3273
3558
  }
3274
3559
  }
3275
3560
  if (!customElements.get("aparte-composer")) {
@@ -3431,10 +3716,10 @@ class AparteComposerInput extends HTMLElement {
3431
3716
  }
3432
3717
  }
3433
3718
  _handleFocus() {
3434
- this.classList.add("is-focused");
3719
+ this.classList.add("aparte-is-focused");
3435
3720
  }
3436
3721
  _handleBlur() {
3437
- this.classList.remove("is-focused");
3722
+ this.classList.remove("aparte-is-focused");
3438
3723
  }
3439
3724
  _handlePaste(e2) {
3440
3725
  e2.preventDefault();
@@ -3477,9 +3762,9 @@ class AparteComposerInput extends HTMLElement {
3477
3762
  }
3478
3763
  _updatePlaceholder() {
3479
3764
  if (this._editor) {
3480
- const p2 = this._getPlaceholder();
3481
- this._editor.setAttribute("data-placeholder", p2);
3482
- this._editor.setAttribute("aria-label", p2);
3765
+ const p = this._getPlaceholder();
3766
+ this._editor.setAttribute("data-placeholder", p);
3767
+ this._editor.setAttribute("aria-label", p);
3483
3768
  }
3484
3769
  }
3485
3770
  _updatePlaceholderVisibility() {
@@ -3605,7 +3890,7 @@ class AparteComposerSend extends HTMLElement {
3605
3890
  const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
3606
3891
  this._button.setAttribute("aria-label", label);
3607
3892
  this._button.setAttribute("title", label);
3608
- this._button.classList.remove("is-streaming");
3893
+ this._button.classList.remove("aparte-is-streaming");
3609
3894
  }
3610
3895
  _handleClick(e2) {
3611
3896
  e2.preventDefault();
@@ -3621,7 +3906,7 @@ class AparteComposerSend extends HTMLElement {
3621
3906
  const label = resolveConfig(this).t("sendButton") || "Send";
3622
3907
  this._button.setAttribute("aria-label", label);
3623
3908
  this._button.setAttribute("title", label);
3624
- this._button.classList.remove("is-streaming");
3909
+ this._button.classList.remove("aparte-is-streaming");
3625
3910
  }
3626
3911
  _syncStreamingState(streaming) {
3627
3912
  if (!this._button) return;
@@ -3631,7 +3916,7 @@ class AparteComposerSend extends HTMLElement {
3631
3916
  const label = resolveConfig(this).t("stopButton") || "Stop";
3632
3917
  this._button.setAttribute("aria-label", label);
3633
3918
  this._button.setAttribute("title", label);
3634
- this._button.classList.add("is-streaming");
3919
+ this._button.classList.add("aparte-is-streaming");
3635
3920
  } else {
3636
3921
  this._syncState();
3637
3922
  }
@@ -3906,15 +4191,15 @@ class AparteComposerAddAttachment extends HTMLElement {
3906
4191
  const onDragOver = (e2) => {
3907
4192
  if (root.disabled) return;
3908
4193
  prevent(e2);
3909
- root.classList.add("is-dragover");
4194
+ root.classList.add("aparte-is-dragover");
3910
4195
  };
3911
4196
  const onDragLeave = (e2) => {
3912
4197
  prevent(e2);
3913
- root.classList.remove("is-dragover");
4198
+ root.classList.remove("aparte-is-dragover");
3914
4199
  };
3915
4200
  const onDrop = (e2) => {
3916
4201
  prevent(e2);
3917
- root.classList.remove("is-dragover");
4202
+ root.classList.remove("aparte-is-dragover");
3918
4203
  if (root.disabled) return;
3919
4204
  const files = e2.dataTransfer?.files;
3920
4205
  if (files?.length) this._getRoot()?.addAttachments(files);
@@ -4207,180 +4492,6 @@ class AparteConversationList extends HTMLElement {
4207
4492
  }
4208
4493
  }
4209
4494
  if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
4210
- class AparteElicitation extends HTMLElement {
4211
- _pending = null;
4212
- /**
4213
- * A turn ended — cancel the open question only if it was OUR turn.
4214
- *
4215
- * These two listeners sit on `window` and had no instance filter at all, so a
4216
- * Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
4217
- * waiting on — and that chat's model was told the user had refused a question
4218
- * the user was still looking at. Same defect as the `compact()` handler that
4219
- * emptied both chats, which its four sibling handlers in `AparteClient` already
4220
- * guarded against.
4221
- *
4222
- * The leniency rule is the composer's, deliberately: an event with no
4223
- * `targetId` is for everyone (a single-chat app never sets one), and a chat we
4224
- * cannot identify accepts everything rather than becoming deaf. Only a
4225
- * MISMATCH is ignored.
4226
- */
4227
- _onTurnEnd = (e2) => {
4228
- const evtTargetId = e2.detail?.targetId;
4229
- const own = this._pendingTargetId();
4230
- if (evtTargetId && own && evtTargetId !== own) return;
4231
- this._cancelPending();
4232
- };
4233
- _unsubscribeConfig = null;
4234
- connectedCallback() {
4235
- this.style.display = "none";
4236
- this._unsubscribeConfig = subscribeConfigChange(this, () => this._relabelPending());
4237
- resolveConfig(this).setElicitationPresenter(this._present);
4238
- window.addEventListener("aparte-message-aborted", this._onTurnEnd);
4239
- window.addEventListener("aparte-message-error", this._onTurnEnd);
4240
- }
4241
- /**
4242
- * The boundary above us appeared, changed, or went away — move the
4243
- * registration with it.
4244
- *
4245
- * `connectedCallback` alone is not enough and cannot be: registering is a
4246
- * WRITE, and under all four wrappers it happens before `attachConfig` runs, so
4247
- * it lands on the global singleton. `requestUserInput()` then resolves the
4248
- * instance config, finds nothing, and answers the model `cancel` — the model
4249
- * hears the user refuse a question the user never saw.
4250
- *
4251
- * See {@link AparteConfigAware}.
4252
- */
4253
- aparteConfigChanged(next, previous) {
4254
- if (previous.getElicitationPresenter() === this._present) previous.setElicitationPresenter(null);
4255
- next.setElicitationPresenter(this._present);
4256
- }
4257
- disconnectedCallback() {
4258
- const cfg = resolveConfig(this);
4259
- if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
4260
- window.removeEventListener("aparte-message-aborted", this._onTurnEnd);
4261
- window.removeEventListener("aparte-message-error", this._onTurnEnd);
4262
- this._unsubscribeConfig?.();
4263
- this._unsubscribeConfig = null;
4264
- this._cancelPending();
4265
- }
4266
- /**
4267
- * Re-apply the open question's strings, in place.
4268
- *
4269
- * Two owners, and both have to move or the panel goes bilingual: the panel's own
4270
- * defaults (`relabel`), and the Skip button, which this file builds and this file
4271
- * therefore has to re-text. The composer's one button is a third, and it already
4272
- * follows — `aparte-composer-send` remembers the panel state it was given.
4273
- */
4274
- _relabelPending() {
4275
- if (!this._pending) return;
4276
- const cfg = resolveConfig(this);
4277
- runWithConfig(cfg, () => this._pending.panel.relabel());
4278
- this._pending.skip.textContent = cfg.t("elicitationSkip");
4279
- }
4280
- _present = (request) => {
4281
- if (this._pending) return Promise.resolve({ action: "cancel" });
4282
- const composer = this._getComposer();
4283
- if (!composer) return Promise.resolve({ action: "cancel" });
4284
- return new Promise((resolve) => {
4285
- let done = false;
4286
- const settle = (result) => {
4287
- if (done) return;
4288
- done = true;
4289
- this._pending = null;
4290
- composer.hidePanel();
4291
- resolve(result);
4292
- };
4293
- if (request.signal) {
4294
- if (request.signal.aborted) {
4295
- settle({ action: "cancel" });
4296
- return;
4297
- }
4298
- request.signal.addEventListener("abort", () => settle({ action: "cancel" }), { once: true });
4299
- }
4300
- const cfg = resolveConfig(this);
4301
- const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, request.schema, () => {
4302
- composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
4303
- }));
4304
- const skip = document.createElement("button");
4305
- skip.type = "button";
4306
- skip.className = "aparte-elic-skip";
4307
- skip.textContent = cfg.t("elicitationSkip");
4308
- skip.addEventListener("click", () => settle({ action: "decline" }));
4309
- panel.dismiss.appendChild(skip);
4310
- this._pending = { settle, composer, panel, skip };
4311
- composer.showPanel(panel.el, {
4312
- submitEnabled: panel.canProceed(),
4313
- mode: panel.mode(),
4314
- onSubmit: () => {
4315
- if (panel.mode() === "advance") {
4316
- panel.proceed();
4317
- composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
4318
- return;
4319
- }
4320
- if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
4321
- }
4322
- });
4323
- panel.focus();
4324
- });
4325
- };
4326
- _cancelPending() {
4327
- this._pending?.settle({ action: "cancel" });
4328
- }
4329
- /**
4330
- * The host id of the chat whose composer holds the open panel.
4331
- *
4332
- * Walks up from the composer rather than from `this`, because the panel lives in
4333
- * the composer and that is what the turn belongs to. Matches the hosts
4334
- * `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
4335
- * Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
4336
- * wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
4337
- * resolves `undefined` on three wrappers out of four.
4338
- */
4339
- _pendingTargetId() {
4340
- let el = this._pending?.composer ?? null;
4341
- while (el) {
4342
- const tag = el.tagName?.toLowerCase();
4343
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
4344
- if (isHost && el.id) return el.id;
4345
- el = el.parentElement;
4346
- }
4347
- return void 0;
4348
- }
4349
- /**
4350
- * The composer to present in: the nearest one in an ancestor subtree, and
4351
- * nothing else.
4352
- *
4353
- * There used to be a `document.querySelector('aparte-composer')` fallback, which
4354
- * is the "first chat on the page" bug this repo has now fixed in four other
4355
- * places: on a page with two chats, an elicitation that could not find its own
4356
- * composer mounted its panel in the OTHER chat's — so one conversation's question
4357
- * appeared under the other conversation, and answering it resolved a tool call
4358
- * belonging to a chat the user was not looking at.
4359
- *
4360
- * Returning `null` instead resolves `cancel`, which is honest: nothing was
4361
- * shown, so nothing was answered. The warning names the fix, because this is a
4362
- * setup mistake and only the developer can correct it — the guide's own example
4363
- * puts `<aparte-elicitation>` inside `<aparte-chat>`.
4364
- */
4365
- _getComposer() {
4366
- let node = this.parentElement;
4367
- while (node) {
4368
- const composer = node.querySelector("aparte-composer");
4369
- if (composer && typeof composer.showPanel === "function") return composer;
4370
- const tag = node.tagName?.toLowerCase();
4371
- const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
4372
- if (isChatBoundary) break;
4373
- node = node.parentElement;
4374
- }
4375
- console.warn(
4376
- "[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."
4377
- );
4378
- return null;
4379
- }
4380
- }
4381
- if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
4382
- customElements.define("aparte-elicitation", AparteElicitation);
4383
- }
4384
4495
  function registerAllComponents() {
4385
4496
  const _chat = customElements.get("aparte-chat");
4386
4497
  const _viewport = customElements.get("aparte-chat-viewport");
@@ -4421,53 +4532,55 @@ export {
4421
4532
  n as AparteConversationManager,
4422
4533
  o as AparteDirectTransport,
4423
4534
  AparteElicitation,
4424
- p as AparteError,
4425
- q as AparteErrorCode,
4535
+ AparteElicitationAbortError,
4536
+ q as AparteError,
4537
+ r as AparteErrorCode,
4426
4538
  AparteMessageRepository,
4427
4539
  AparteOptgroup,
4428
4540
  AparteOption,
4429
4541
  AparteProgressSpinner,
4430
4542
  AparteSelect,
4431
- s as AparteStreamParser,
4432
- t as aparteGlobalConfig,
4433
- u as applyElementProps,
4434
- v as attachConfig,
4543
+ t as AparteStreamParser,
4544
+ u as aparteGlobalConfig,
4545
+ v as applyElementProps,
4546
+ w as attachConfig,
4547
+ buildApprovalPanel,
4435
4548
  buildElicitationPanel,
4436
- x as collectRendererStyles,
4437
- y as contentToText,
4438
- z as contextConfig,
4439
- B as createAparteChatHandler,
4440
- C as createStreamAdapter,
4549
+ z as collectRendererStyles,
4550
+ B as contentToText,
4551
+ C as contextConfig,
4552
+ D as createAparteChatHandler,
4553
+ E as createStreamAdapter,
4441
4554
  cssEscape,
4442
- E as declineDefaultRenderers,
4443
- F as defaultSanitizer,
4444
- G as deriveArtifactKind,
4445
- H as detachConfig,
4555
+ G as declineDefaultRenderers,
4556
+ H as defaultSanitizer,
4557
+ I as deriveArtifactKind,
4558
+ J as detachConfig,
4446
4559
  escapeAttr,
4447
4560
  escapeHtml,
4448
- K as filesToAttachments,
4449
- L as getAllRenderers,
4561
+ M as filesToAttachments,
4562
+ N as getAllRenderers,
4450
4563
  getSegmentRenderer,
4451
4564
  installDefaultRenderersOnce,
4452
4565
  isAwaitingReply,
4453
- P as isFormatAdapter,
4454
- Q as isSafeUrl,
4455
- R as isSegmentSettled,
4456
- S as parseAparteEventStream,
4457
- T as parseMarkdownToSegments,
4566
+ R as isFormatAdapter,
4567
+ S as isSafeUrl,
4568
+ T as isSegmentSettled,
4569
+ U as parseAparteEventStream,
4570
+ V as parseMarkdownToSegments,
4458
4571
  populateBubbleFromMessage,
4459
- V as readableToAsyncIterable,
4572
+ X as readableToAsyncIterable,
4460
4573
  registerAllComponents,
4461
- W as registerDefaultRenderers,
4462
- X as registerSegmentRenderer,
4463
- Y as requestUserInput,
4574
+ Y as registerDefaultRenderers,
4575
+ Z as registerSegmentRenderer,
4576
+ _ as requestUserInput,
4464
4577
  resolveConfig,
4465
4578
  revokeAttachmentUrls,
4466
4579
  runWithConfig,
4467
- a0 as segmentDuration,
4468
- a1 as segmentTiming,
4580
+ a2 as segmentDuration,
4581
+ a3 as segmentTiming,
4469
4582
  subscribeConfigChange,
4470
- a3 as unregisterSegmentRenderer,
4583
+ a5 as unregisterSegmentRenderer,
4471
4584
  uuid
4472
4585
  };
4473
4586
  //# sourceMappingURL=index.js.map