@aparte/core 0.8.0 → 0.10.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 (75) hide show
  1. package/dist/client/aparte-client.d.ts.map +1 -1
  2. package/dist/client/stream-adapter.d.ts.map +1 -1
  3. package/dist/client/xml-artifact-feed.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +24 -3
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
  7. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-add-attachment.d.ts +11 -0
  9. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-cancel.d.ts +8 -0
  11. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer-send.d.ts +32 -0
  14. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  15. package/dist/components/elicitation/aparte-elicitation.d.ts +10 -0
  16. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  17. package/dist/components/viewport/aparte-chat-viewport.d.ts +13 -1
  18. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  19. package/dist/config/aparte-config.d.ts +34 -1
  20. package/dist/config/aparte-config.d.ts.map +1 -1
  21. package/dist/config/config-subscribe.d.ts +4 -0
  22. package/dist/config/config-subscribe.d.ts.map +1 -0
  23. package/dist/config/index.d.ts +1 -0
  24. package/dist/config/index.d.ts.map +1 -1
  25. package/dist/config/locale.d.ts +98 -0
  26. package/dist/config/locale.d.ts.map +1 -1
  27. package/dist/config/sanitize.d.ts.map +1 -1
  28. package/dist/custom-elements.json +7590 -6959
  29. package/dist/elicitation/panel.d.ts +13 -0
  30. package/dist/elicitation/panel.d.ts.map +1 -1
  31. package/dist/host/aparte-chat-host.d.ts +3 -1
  32. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  33. package/dist/{index-BieFbKeW.js → index-BnKBciDG.js} +642 -248
  34. package/dist/index-BnKBciDG.js.map +1 -0
  35. package/dist/index.css +147 -310
  36. package/dist/index.d.ts +3 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +281 -92
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.node.d.ts +3 -1
  41. package/dist/index.node.d.ts.map +1 -1
  42. package/dist/index.node.js +60 -55
  43. package/dist/index.node.js.map +1 -1
  44. package/dist/parsers/aparte-stream-parser.d.ts +25 -0
  45. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  46. package/dist/renderers/highlight-stream.d.ts +17 -0
  47. package/dist/renderers/highlight-stream.d.ts.map +1 -0
  48. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  49. package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -1
  50. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  51. package/dist/renderers/segments/artifact/shared.d.ts +19 -2
  52. package/dist/renderers/segments/artifact/shared.d.ts.map +1 -1
  53. package/dist/renderers/segments/code.d.ts.map +1 -1
  54. package/dist/renderers/segments/error.d.ts.map +1 -1
  55. package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
  56. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  57. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  58. package/dist/types/event-map.d.ts +1 -2
  59. package/dist/types/event-map.d.ts.map +1 -1
  60. package/dist/types/events.d.ts +0 -16
  61. package/dist/types/events.d.ts.map +1 -1
  62. package/dist/types/index.d.ts +2 -2
  63. package/dist/types/index.d.ts.map +1 -1
  64. package/dist/types/models.d.ts +1 -6
  65. package/dist/types/models.d.ts.map +1 -1
  66. package/dist/types/segments.d.ts +103 -51
  67. package/dist/types/segments.d.ts.map +1 -1
  68. package/dist/utils/segments.d.ts +244 -0
  69. package/dist/utils/segments.d.ts.map +1 -0
  70. package/package.json +1 -1
  71. package/dist/index-BieFbKeW.js.map +0 -1
  72. package/dist/renderers/segments/file-tree.d.ts +0 -3
  73. package/dist/renderers/segments/file-tree.d.ts.map +0 -1
  74. package/dist/renderers/segments/terminal.d.ts +0 -3
  75. package/dist/renderers/segments/terminal.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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";
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";
3
3
  class AparteOption extends HTMLElement {
4
4
  static get observedAttributes() {
5
5
  return ["value", "disabled", "selected", "data-status"];
@@ -804,7 +804,38 @@ class AparteChatBubble extends HTMLElement {
804
804
  this._updateName();
805
805
  this._updateLocalizedLabels();
806
806
  this._updateWaiting();
807
+ this._renderAvatar();
808
+ this._relabelSegments();
809
+ this._updateTimestamp(this.getAttribute("timestamp"));
807
810
  };
811
+ /**
812
+ * Ask every rendered segment to re-read its config-derived text.
813
+ *
814
+ * Not `_renderSegments()`, which wipes the container and rebuilds: that destroys a
815
+ * mounted artifact preview, reverts a reasoning block the reader expanded by
816
+ * clicking `<summary>` (the DOM's real state is never written back to `collapsed`),
817
+ * resets scroll inside long terminal panes, drops focus from an Approve/Reject
818
+ * gate, and throws away the incremental Markdown parser's buffered lookahead
819
+ * mid-stream — for a change that added no content. It also fires container-wide
820
+ * childList mutations, which is what the viewport's observer reads as "scroll to
821
+ * the bottom".
822
+ *
823
+ * `relabel` is the narrow alternative, bound by the same no-child-node rule as
824
+ * `update()`. A renderer that has no config-derived text does not implement it,
825
+ * and this loop simply skips it.
826
+ */
827
+ _relabelSegments() {
828
+ if (!this._segmentsEl) return;
829
+ for (const segment of this._segments) {
830
+ const renderer = resolveSegmentRenderer(segment.type, this._cfg);
831
+ if (!renderer?.relabel) continue;
832
+ const el = this._segmentsEl.querySelector(
833
+ `:scope > [data-segment-id="${cssEscape(segment.id)}"]`
834
+ );
835
+ if (!el) continue;
836
+ runWithConfig(this._cfg, () => renderer.relabel(el, segment));
837
+ }
838
+ }
808
839
  /**
809
840
  * Re-apply the locale strings written straight into the markup by `_render()` —
810
841
  * the accessible names a screen reader reads, which nothing else refreshes.
@@ -892,7 +923,7 @@ class AparteChatBubble extends HTMLElement {
892
923
  }
893
924
  /** Set segments for rich content */
894
925
  setSegments(segments) {
895
- this._segments = segments;
926
+ this._segments = [...segments];
896
927
  this._renderSegments();
897
928
  this._updateWaiting();
898
929
  }
@@ -906,7 +937,7 @@ class AparteChatBubble extends HTMLElement {
906
937
  updateSegment(segmentId, updates) {
907
938
  const index = this._segments.findIndex((s2) => s2.id === segmentId);
908
939
  if (index !== -1) {
909
- const updated = { ...this._segments[index], ...updates };
940
+ const updated = mergeSegmentUpdate(this._segments[index], updates);
910
941
  this._segments[index] = updated;
911
942
  this._applySegmentUpdate(segmentId, updated, updates);
912
943
  }
@@ -1135,9 +1166,12 @@ class AparteChatBubble extends HTMLElement {
1135
1166
  * (same null-guarded degradation as the other region hooks).
1136
1167
  */
1137
1168
  _updateWaiting() {
1169
+ const empty = this._segments.length === 0 && !this._content.trim();
1170
+ const waiting = this._streaming && this._role !== "user" && empty;
1171
+ const box = this.querySelector(".aparte-message-content");
1172
+ if (box) box.hidden = empty && !waiting;
1138
1173
  const el = this.querySelector(".aparte-waiting");
1139
1174
  if (!el) return;
1140
- const waiting = this._streaming && this._role !== "user" && this._segments.length === 0 && !this._content.trim();
1141
1175
  el.hidden = !waiting;
1142
1176
  if (!waiting) return;
1143
1177
  const label = this._cfg.getLocale().typing;
@@ -1145,9 +1179,13 @@ class AparteChatBubble extends HTMLElement {
1145
1179
  if (sr && sr.textContent !== label) sr.textContent = label;
1146
1180
  }
1147
1181
  /**
1148
- * Hand the avatar host element off to the registered AvatarProvider, if
1149
- * any. Falls back to the default initial / image rendered by `_render()`
1150
- * when no provider is set.
1182
+ * Hand the avatar host element off to the registered AvatarProvider, if any.
1183
+ *
1184
+ * With no provider the slot is left exactly as the shell rendered it — which for
1185
+ * the default shell means EMPTY, and hidden by `.aparte-avatar:empty`. This used
1186
+ * to claim it "falls back to the default initial rendered by `_render()`"; there
1187
+ * is no such initial, and believing there was is what made `_updateRole` write
1188
+ * one.
1151
1189
  */
1152
1190
  _renderAvatar() {
1153
1191
  const avatar = this.querySelector(".aparte-avatar");
@@ -1175,7 +1213,7 @@ class AparteChatBubble extends HTMLElement {
1175
1213
  }
1176
1214
  if (avatar) {
1177
1215
  avatar.setAttribute("data-role", this._role);
1178
- avatar.textContent = this._getAvatarInitial();
1216
+ if (avatar.textContent) avatar.textContent = this._getAvatarInitial();
1179
1217
  }
1180
1218
  if (nameEl) {
1181
1219
  nameEl.textContent = this._getDisplayName();
@@ -1186,7 +1224,9 @@ class AparteChatBubble extends HTMLElement {
1186
1224
  _updateName() {
1187
1225
  const avatar = this.querySelector(".aparte-avatar");
1188
1226
  const nameEl = this.querySelector(".aparte-name");
1189
- if (avatar && !this._cfg.getAvatarProvider()) avatar.textContent = this._getAvatarInitial();
1227
+ if (avatar && avatar.textContent && !this._cfg.getAvatarProvider()) {
1228
+ avatar.textContent = this._getAvatarInitial();
1229
+ }
1190
1230
  if (nameEl) nameEl.textContent = this._getDisplayName();
1191
1231
  }
1192
1232
  _updateContent() {
@@ -1271,7 +1311,7 @@ class AparteChatBubble extends HTMLElement {
1271
1311
  if (!timestampEl || !value) return;
1272
1312
  try {
1273
1313
  const date = new Date(isNaN(Number(value)) ? value : Number(value));
1274
- timestampEl.textContent = date.toLocaleTimeString(void 0, {
1314
+ timestampEl.textContent = date.toLocaleTimeString(this._cfg.getLocale().tag || void 0, {
1275
1315
  hour: "2-digit",
1276
1316
  minute: "2-digit"
1277
1317
  });
@@ -1978,7 +2018,13 @@ class AparteChatViewport extends HTMLElement {
1978
2018
  if (segment && "content" in segment) {
1979
2019
  message.segments[index] = {
1980
2020
  ...segment,
1981
- content: segment.content + chunk
2021
+ content: segment.content + chunk,
2022
+ // Content arriving IS the segment's activity, so this is what
2023
+ // `endedAt` measures. Without it a thinking block's end would be
2024
+ // whenever someone happened to notice it had stopped — the end of
2025
+ // the turn, or the start of the next segment — and both of those
2026
+ // silently fold the waiting that followed into the duration.
2027
+ ...stampSegmentActivity(segment)
1982
2028
  };
1983
2029
  }
1984
2030
  this.dispatchEvent(new CustomEvent("aparte-segment-update", {
@@ -2017,8 +2063,16 @@ class AparteChatViewport extends HTMLElement {
2017
2063
  if (!message.segments) {
2018
2064
  message.segments = [];
2019
2065
  }
2020
- message.segments.push(segment);
2021
- this._notifyBubble(messageId, "addSegment", segment);
2066
+ const stamped = stampSegmentOnInsert(
2067
+ message.segments,
2068
+ segment,
2069
+ messageId,
2070
+ // THIS chat's defaults, not the page's: two chats on one page can be
2071
+ // configured differently, and the config seam is per instance.
2072
+ resolveConfig(this).getSegmentDefaults(segment.type)
2073
+ );
2074
+ message.segments.push(stamped);
2075
+ this._notifyBubble(messageId, "addSegment", stamped);
2022
2076
  this._autoScroll();
2023
2077
  }
2024
2078
  updateSegment(a2, b2, c2) {
@@ -2031,11 +2085,10 @@ class AparteChatViewport extends HTMLElement {
2031
2085
  if (!message?.segments) return;
2032
2086
  const segmentIndex = message.segments.findIndex((s2) => s2.id === segmentId);
2033
2087
  if (segmentIndex !== -1) {
2034
- message.segments[segmentIndex] = {
2035
- ...message.segments[segmentIndex],
2036
- ...updates
2037
- };
2038
- this._notifyBubble(messageId, "updateSegment", { segmentId, updates });
2088
+ const current = message.segments[segmentIndex];
2089
+ const stamped = stampSegmentOnUpdate(current, updates);
2090
+ message.segments[segmentIndex] = mergeSegmentUpdate(current, stamped);
2091
+ this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
2039
2092
  }
2040
2093
  }
2041
2094
  removeSegment(a2, b2) {
@@ -2046,7 +2099,10 @@ class AparteChatViewport extends HTMLElement {
2046
2099
  const message = this._repo.getMessageById(messageId);
2047
2100
  if (message?.segments) {
2048
2101
  const idx = message.segments.findIndex((s2) => s2.id === segmentId);
2049
- if (idx !== -1) message.segments.splice(idx, 1);
2102
+ if (idx !== -1) {
2103
+ message.segments.splice(idx, 1);
2104
+ renumberSegments(message.segments);
2105
+ }
2050
2106
  }
2051
2107
  this._notifyBubble(messageId, "removeSegment", segmentId);
2052
2108
  }
@@ -2086,13 +2142,26 @@ class AparteChatViewport extends HTMLElement {
2086
2142
  if (message) {
2087
2143
  message.isStreaming = false;
2088
2144
  message.status = "completed";
2089
- message.segments?.forEach((s2) => {
2090
- s2.isStreaming = false;
2091
- });
2145
+ this._settleSegments(messageId, message);
2092
2146
  this._notifyBubble(messageId, "complete", { status: "completed" });
2093
2147
  this._recalculateSpacer();
2094
2148
  }
2095
2149
  }
2150
+ /**
2151
+ * Close a finished message's still-open segments, one `updateSegment` each.
2152
+ *
2153
+ * Deliberately NOT a loop that writes `isStreaming` onto the objects: that path
2154
+ * stamps the model and tells the bubble nothing, so a renderer never learns its
2155
+ * segment settled — no `endedAt` in the rendered label, and no final Markdown
2156
+ * flush either. Going through `updateSegment` reuses the one path that does
2157
+ * both.
2158
+ */
2159
+ _settleSegments(messageId, message) {
2160
+ if (!message.segments) return;
2161
+ for (const id of openSegmentIds(message.segments)) {
2162
+ this.updateSegment(messageId, id, { isStreaming: false });
2163
+ }
2164
+ }
2096
2165
  /**
2097
2166
  * Atomic update for a message by ID
2098
2167
  * Supports updating content, status, segments, and other metadata
@@ -2103,6 +2172,7 @@ class AparteChatViewport extends HTMLElement {
2103
2172
  Object.assign(message, updates);
2104
2173
  if (updates.status) {
2105
2174
  message.isStreaming = updates.status === "streaming" || updates.status === "pending";
2175
+ if (isTerminalStatus(updates.status)) this._settleSegments(messageId, message);
2106
2176
  }
2107
2177
  this._notifyBubble(messageId, "update", updates);
2108
2178
  this._autoScroll();
@@ -2117,7 +2187,7 @@ class AparteChatViewport extends HTMLElement {
2117
2187
  * which both records the message and creates its bubble element.
2118
2188
  */
2119
2189
  addMessage(message) {
2120
- this._repo.addOrUpdateMessage(this._repo.headId, { ...message });
2190
+ this._repo.addOrUpdateMessage(this._repo.headId, adoptMessageSegments({ ...message }));
2121
2191
  this._pruneRenderedBubbles();
2122
2192
  this._autoScroll();
2123
2193
  }
@@ -2128,8 +2198,20 @@ class AparteChatViewport extends HTMLElement {
2128
2198
  * When `_frameworkManagedDOM` is true, only the internal repo is updated —
2129
2199
  * the framework owns the DOM and will create the bubble element itself.
2130
2200
  */
2131
- appendMessage(message) {
2132
- this._repo.addOrUpdateMessage(this._repo.headId, { ...message });
2201
+ appendMessage(message, options) {
2202
+ const stored = options?.historical ? adoptMessageSegments(message) : message.segments?.length ? { ...message, segments: message.segments.reduce(
2203
+ (acc, segment) => {
2204
+ acc.push(stampSegmentOnInsert(
2205
+ acc,
2206
+ segment,
2207
+ message.id,
2208
+ resolveConfig(this).getSegmentDefaults(segment.type)
2209
+ ));
2210
+ return acc;
2211
+ },
2212
+ []
2213
+ ) } : { ...message };
2214
+ this._repo.addOrUpdateMessage(this._repo.headId, stored);
2133
2215
  if (!this._frameworkManagedDOM) {
2134
2216
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2135
2217
  if (wrapper) {
@@ -2146,7 +2228,7 @@ class AparteChatViewport extends HTMLElement {
2146
2228
  } else {
2147
2229
  wrapper.appendChild(bubble);
2148
2230
  }
2149
- populateBubbleFromMessage(bubble, message);
2231
+ populateBubbleFromMessage(bubble, stored);
2150
2232
  }
2151
2233
  }
2152
2234
  this._pruneRenderedBubbles();
@@ -2296,7 +2378,13 @@ class AparteChatViewport extends HTMLElement {
2296
2378
  */
2297
2379
  importTree(tree) {
2298
2380
  this.clearAll({ revokeAttachments: false });
2299
- this._repo.import(tree);
2381
+ this._repo.import({
2382
+ ...tree,
2383
+ messages: tree.messages.map((entry) => ({
2384
+ ...entry,
2385
+ message: adoptMessageSegments(entry.message)
2386
+ }))
2387
+ });
2300
2388
  this._reRenderActivePath();
2301
2389
  }
2302
2390
  /**
@@ -2344,7 +2432,7 @@ class AparteChatViewport extends HTMLElement {
2344
2432
  setMessages(messages) {
2345
2433
  this.clearAll({ revokeAttachments: false });
2346
2434
  for (const m2 of messages) {
2347
- this.appendMessage(m2);
2435
+ this.appendMessage(m2, { historical: true });
2348
2436
  }
2349
2437
  }
2350
2438
  /**
@@ -3205,6 +3293,7 @@ class AparteComposerInput extends HTMLElement {
3205
3293
  this._render();
3206
3294
  this._connectToRoot();
3207
3295
  this._scheduleInitialReflow();
3296
+ this._unsubscribes.push(subscribeConfigChange(this, () => this._updatePlaceholder()));
3208
3297
  }
3209
3298
  disconnectedCallback() {
3210
3299
  this._editor?.removeEventListener("input", this._onInput);
@@ -3408,6 +3497,16 @@ if (!customElements.get("aparte-composer-input")) {
3408
3497
  class AparteComposerSend extends HTMLElement {
3409
3498
  _button = null;
3410
3499
  _unsubscribes = [];
3500
+ /**
3501
+ * The last `panel-change` payload.
3502
+ *
3503
+ * This button has four meanings — send, stop, submit an answer, advance to the
3504
+ * next question — and three of them are decided by state it does not own: the
3505
+ * root's `streaming`, and this payload. It was read straight out of the event's
3506
+ * arguments and thrown away, so nothing could recompute the button's chrome
3507
+ * afterwards; a config change had no way to know which of the four to write.
3508
+ */
3509
+ _panel = null;
3411
3510
  // Bound handler
3412
3511
  _onClick = this._handleClick.bind(this);
3413
3512
  connectedCallback() {
@@ -3457,27 +3556,56 @@ class AparteComposerSend extends HTMLElement {
3457
3556
  root._on("attachments-change", () => this._syncState())
3458
3557
  );
3459
3558
  this._unsubscribes.push(
3460
- root._on("panel-change", ({ active, submitEnabled, mode }) => {
3461
- if (!this._button) return;
3462
- if (active) {
3463
- this._button.disabled = !submitEnabled;
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";
3468
- this._button.setAttribute("aria-label", label);
3469
- this._button.setAttribute("title", label);
3470
- this._button.classList.remove("is-streaming");
3471
- } else {
3472
- const root2 = this._getRoot();
3473
- if (root2?.streaming) {
3474
- this._syncStreamingState(true);
3475
- } else {
3476
- this._syncState();
3477
- }
3478
- }
3559
+ root._on("panel-change", (payload) => {
3560
+ this._panel = payload;
3561
+ this._refreshChrome();
3479
3562
  })
3480
3563
  );
3564
+ this._unsubscribes.push(subscribeConfigChange(this, () => this._refreshChrome()));
3565
+ }
3566
+ /**
3567
+ * Write the chrome for the mode the button is IN, deciding before writing.
3568
+ *
3569
+ * Never `_render()`: it returns early once the button exists, and its own
3570
+ * disabled/icon computation consults neither `root.streaming` nor the panel — so
3571
+ * rebuilding mid-turn would put a paper plane back while a reply was still
3572
+ * streaming, and rebuilding with the question panel open would silently drop out
3573
+ * of answer mode. It would also take the focus off the one control in this
3574
+ * composer most likely to be holding it.
3575
+ */
3576
+ _refreshChrome() {
3577
+ if (!this._button) return;
3578
+ if (this._panel?.active) {
3579
+ this._syncPanelState();
3580
+ return;
3581
+ }
3582
+ if (this._getRoot()?.streaming) {
3583
+ this._syncStreamingState(true);
3584
+ return;
3585
+ }
3586
+ this._syncState();
3587
+ }
3588
+ /**
3589
+ * Panel open: this one button now means "answer", and WHICH answer depends on
3590
+ * where you are in the form.
3591
+ *
3592
+ * The icon has to move with the meaning: it drew a paper plane while the label
3593
+ * already said "Submit", so it read as "send a message" while it meant "answer
3594
+ * this question". And a check on a form with three questions left was just as
3595
+ * wrong — hence a chevron while there is more ahead. The visual is what a user
3596
+ * reads.
3597
+ */
3598
+ _syncPanelState() {
3599
+ const panel = this._panel;
3600
+ if (!this._button || !panel?.active) return;
3601
+ const cfg = resolveConfig(this);
3602
+ const advancing = panel.mode === "advance";
3603
+ this._button.disabled = !panel.submitEnabled;
3604
+ this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
3605
+ const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
3606
+ this._button.setAttribute("aria-label", label);
3607
+ this._button.setAttribute("title", label);
3608
+ this._button.classList.remove("is-streaming");
3481
3609
  }
3482
3610
  _handleClick(e2) {
3483
3611
  e2.preventDefault();
@@ -3500,8 +3628,9 @@ class AparteComposerSend extends HTMLElement {
3500
3628
  if (streaming) {
3501
3629
  this._button.disabled = false;
3502
3630
  this._button.innerHTML = this._getStopIcon();
3503
- this._button.setAttribute("aria-label", "Stop");
3504
- this._button.setAttribute("title", "Stop");
3631
+ const label = resolveConfig(this).t("stopButton") || "Stop";
3632
+ this._button.setAttribute("aria-label", label);
3633
+ this._button.setAttribute("title", label);
3505
3634
  this._button.classList.add("is-streaming");
3506
3635
  } else {
3507
3636
  this._syncState();
@@ -3532,6 +3661,7 @@ class AparteComposerCancel extends HTMLElement {
3532
3661
  connectedCallback() {
3533
3662
  this._render();
3534
3663
  this._connectToRoot();
3664
+ this._unsubscribes.push(subscribeConfigChange(this, () => this._refreshChrome()));
3535
3665
  }
3536
3666
  disconnectedCallback() {
3537
3667
  this._button?.removeEventListener("click", this._onClick);
@@ -3569,6 +3699,20 @@ class AparteComposerCancel extends HTMLElement {
3569
3699
  e2.preventDefault();
3570
3700
  this._getRoot()?.cancel();
3571
3701
  }
3702
+ /**
3703
+ * Re-read the accessible name and the icon in place.
3704
+ *
3705
+ * `hidden` is NOT touched: `_render()` always renders this button hidden and only
3706
+ * the root's `streaming-change` listener ever un-hides it, so a rebuild would make
3707
+ * the stop button vanish in the middle of a turn.
3708
+ */
3709
+ _refreshChrome() {
3710
+ if (!this._button) return;
3711
+ const label = resolveConfig(this).t("stopButton") || "Stop";
3712
+ this._button.setAttribute("aria-label", label);
3713
+ this._button.setAttribute("title", label);
3714
+ this._button.innerHTML = this._getStopIcon();
3715
+ }
3572
3716
  _getStopIcon() {
3573
3717
  return resolveConfig(this).getIcon("stop");
3574
3718
  }
@@ -3674,6 +3818,7 @@ class AparteComposerAddAttachment extends HTMLElement {
3674
3818
  this._render();
3675
3819
  this._connectToRoot();
3676
3820
  this._setupDragDrop();
3821
+ this._unsubscribes.push(subscribeConfigChange(this, () => this._refreshChrome()));
3677
3822
  }
3678
3823
  disconnectedCallback() {
3679
3824
  this._button?.removeEventListener("click", this._onClick);
@@ -3690,6 +3835,24 @@ class AparteComposerAddAttachment extends HTMLElement {
3690
3835
  _getRoot() {
3691
3836
  return this.closest("aparte-composer");
3692
3837
  }
3838
+ /**
3839
+ * Re-read the label and the icon on the button that already exists.
3840
+ *
3841
+ * Deliberately not a re-render: `_render()` returns early once the button is
3842
+ * there, and its `disabled` computation ignores `root.streaming` while the
3843
+ * streaming listener sets `disabled` directly — so rebuilding would silently
3844
+ * re-enable the attach button mid-turn, and drop focus if the user were on it.
3845
+ * The native file input and the drag listeners live outside this element
3846
+ * (on `document.body` and on the composer root), so they are untouched either way.
3847
+ */
3848
+ _refreshChrome() {
3849
+ if (!this._button) return;
3850
+ const cfg = resolveConfig(this);
3851
+ const label = cfg.t("actionUpload") || "Attach file";
3852
+ this._button.setAttribute("aria-label", label);
3853
+ this._button.setAttribute("title", label);
3854
+ this._button.innerHTML = cfg.getIcon("paperclip") || this._defaultIcon();
3855
+ }
3693
3856
  _render() {
3694
3857
  if (this.querySelector(".aparte-caa-button")) return;
3695
3858
  const label = resolveConfig(this).t("actionUpload") || "Attach file";
@@ -3783,6 +3946,9 @@ class AparteComposerAction extends HTMLElement {
3783
3946
  connectedCallback() {
3784
3947
  this._render();
3785
3948
  this._connectToRoot();
3949
+ this._unsubscribes.push(subscribeConfigChange(this, () => {
3950
+ if (this._button) this._button.innerHTML = this._resolveIcon(this.getAttribute("icon") ?? "");
3951
+ }));
3786
3952
  }
3787
3953
  disconnectedCallback() {
3788
3954
  this._button?.removeEventListener("click", this._onClick);
@@ -4064,8 +4230,10 @@ class AparteElicitation extends HTMLElement {
4064
4230
  if (evtTargetId && own && evtTargetId !== own) return;
4065
4231
  this._cancelPending();
4066
4232
  };
4233
+ _unsubscribeConfig = null;
4067
4234
  connectedCallback() {
4068
4235
  this.style.display = "none";
4236
+ this._unsubscribeConfig = subscribeConfigChange(this, () => this._relabelPending());
4069
4237
  resolveConfig(this).setElicitationPresenter(this._present);
4070
4238
  window.addEventListener("aparte-message-aborted", this._onTurnEnd);
4071
4239
  window.addEventListener("aparte-message-error", this._onTurnEnd);
@@ -4091,8 +4259,24 @@ class AparteElicitation extends HTMLElement {
4091
4259
  if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
4092
4260
  window.removeEventListener("aparte-message-aborted", this._onTurnEnd);
4093
4261
  window.removeEventListener("aparte-message-error", this._onTurnEnd);
4262
+ this._unsubscribeConfig?.();
4263
+ this._unsubscribeConfig = null;
4094
4264
  this._cancelPending();
4095
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
+ }
4096
4280
  _present = (request) => {
4097
4281
  if (this._pending) return Promise.resolve({ action: "cancel" });
4098
4282
  const composer = this._getComposer();
@@ -4123,7 +4307,7 @@ class AparteElicitation extends HTMLElement {
4123
4307
  skip.textContent = cfg.t("elicitationSkip");
4124
4308
  skip.addEventListener("click", () => settle({ action: "decline" }));
4125
4309
  panel.dismiss.appendChild(skip);
4126
- this._pending = { settle, composer };
4310
+ this._pending = { settle, composer, panel, skip };
4127
4311
  composer.showPanel(panel.el, {
4128
4312
  submitEnabled: panel.canProceed(),
4129
4313
  mode: panel.mode(),
@@ -4207,21 +4391,22 @@ function registerAllComponents() {
4207
4391
  }
4208
4392
  }
4209
4393
  export {
4210
- A as APARTE_CONVERSATION_SCHEMA_VERSION,
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,
4394
+ A as APARTE_CONFIG_CHANGE,
4395
+ a as APARTE_CONVERSATION_SCHEMA_VERSION,
4396
+ b as APARTE_DEFAULT_BUBBLE_ACTIONS,
4397
+ c as APARTE_DEFAULT_HOST_HANDLERS,
4398
+ d as APARTE_DEFAULT_ICON_FALLBACKS,
4399
+ e as APARTE_DEFAULT_LOCALE,
4400
+ f as APARTE_DEFAULT_SKELETON_FALLBACKS,
4401
+ g as APARTE_DEFAULT_UI_EVENTS,
4402
+ h as APARTE_HOST_ATTR,
4403
+ i as AparteBackendTransport,
4219
4404
  AparteChat,
4220
4405
  AparteChatBubble,
4221
- i as AparteChatHost,
4406
+ j as AparteChatHost,
4222
4407
  AparteChatStatus,
4223
4408
  AparteChatViewport,
4224
- j as AparteClient,
4409
+ k as AparteClient,
4225
4410
  AparteComposer,
4226
4411
  AparteComposerAction,
4227
4412
  AparteComposerAddAttachment,
@@ -4230,55 +4415,59 @@ export {
4230
4415
  AparteComposerInput,
4231
4416
  AparteComposerSend,
4232
4417
  AparteComposerToolbar,
4233
- k as AparteConfig,
4234
- l as AparteConversationController,
4418
+ l as AparteConfig,
4419
+ m as AparteConversationController,
4235
4420
  AparteConversationList,
4236
- m as AparteConversationManager,
4237
- n as AparteDirectTransport,
4421
+ n as AparteConversationManager,
4422
+ o as AparteDirectTransport,
4238
4423
  AparteElicitation,
4239
- o as AparteError,
4240
- p as AparteErrorCode,
4424
+ p as AparteError,
4425
+ q as AparteErrorCode,
4241
4426
  AparteMessageRepository,
4242
4427
  AparteOptgroup,
4243
4428
  AparteOption,
4244
4429
  AparteProgressSpinner,
4245
4430
  AparteSelect,
4246
- r as AparteStreamParser,
4247
- s as aparteGlobalConfig,
4248
- t as applyElementProps,
4249
- u as attachConfig,
4431
+ s as AparteStreamParser,
4432
+ t as aparteGlobalConfig,
4433
+ u as applyElementProps,
4434
+ v as attachConfig,
4250
4435
  buildElicitationPanel,
4251
- w as collectRendererStyles,
4252
- x as contentToText,
4253
- y as contextConfig,
4254
- z as createAparteChatHandler,
4255
- B as createStreamAdapter,
4436
+ x as collectRendererStyles,
4437
+ y as contentToText,
4438
+ z as contextConfig,
4439
+ B as createAparteChatHandler,
4440
+ C as createStreamAdapter,
4256
4441
  cssEscape,
4257
- D as declineDefaultRenderers,
4258
- E as defaultSanitizer,
4259
- F as deriveArtifactKind,
4260
- G as detachConfig,
4442
+ E as declineDefaultRenderers,
4443
+ F as defaultSanitizer,
4444
+ G as deriveArtifactKind,
4445
+ H as detachConfig,
4261
4446
  escapeAttr,
4262
4447
  escapeHtml,
4263
- J as filesToAttachments,
4264
- K as getAllRenderers,
4448
+ K as filesToAttachments,
4449
+ L as getAllRenderers,
4265
4450
  getSegmentRenderer,
4266
4451
  installDefaultRenderersOnce,
4267
4452
  isAwaitingReply,
4268
- O as isFormatAdapter,
4269
- P as isSafeUrl,
4270
- Q as parseAparteEventStream,
4271
- R as parseMarkdownToSegments,
4453
+ P as isFormatAdapter,
4454
+ Q as isSafeUrl,
4455
+ R as isSegmentSettled,
4456
+ S as parseAparteEventStream,
4457
+ T as parseMarkdownToSegments,
4272
4458
  populateBubbleFromMessage,
4273
- T as readableToAsyncIterable,
4459
+ V as readableToAsyncIterable,
4274
4460
  registerAllComponents,
4275
- U as registerDefaultRenderers,
4276
- V as registerSegmentRenderer,
4277
- W as requestUserInput,
4461
+ W as registerDefaultRenderers,
4462
+ X as registerSegmentRenderer,
4463
+ Y as requestUserInput,
4278
4464
  resolveConfig,
4279
4465
  revokeAttachmentUrls,
4280
4466
  runWithConfig,
4281
- _ as unregisterSegmentRenderer,
4467
+ a0 as segmentDuration,
4468
+ a1 as segmentTiming,
4469
+ subscribeConfigChange,
4470
+ a3 as unregisterSegmentRenderer,
4282
4471
  uuid
4283
4472
  };
4284
4473
  //# sourceMappingURL=index.js.map