@aparte/core 0.3.0-alpha.0 → 0.5.0-alpha.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 (50) hide show
  1. package/README.md +26 -2
  2. package/dist/client/aparte-client.d.ts +7 -1
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +26 -2
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/chat/aparte-chat.d.ts +13 -0
  7. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-attachments.d.ts +3 -2
  9. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  10. package/dist/components/viewport/aparte-chat-viewport.d.ts +17 -4
  11. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  12. package/dist/config/aparte-config.d.ts +64 -7
  13. package/dist/config/aparte-config.d.ts.map +1 -1
  14. package/dist/config/icon-provider.d.ts +21 -21
  15. package/dist/config/icon-provider.d.ts.map +1 -1
  16. package/dist/config/index.d.ts +1 -1
  17. package/dist/config/index.d.ts.map +1 -1
  18. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  19. package/dist/custom-elements.json +3435 -3000
  20. package/dist/host/aparte-chat-host.d.ts +58 -1
  21. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  22. package/dist/{index-CW2BCAqn.js → index-Dn7TH14G.js} +282 -81
  23. package/dist/index-Dn7TH14G.js.map +1 -0
  24. package/dist/index.css +75 -8
  25. package/dist/index.d.ts +11 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +216 -62
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.node.d.ts +3 -1
  30. package/dist/index.node.d.ts.map +1 -1
  31. package/dist/index.node.js +34 -30
  32. package/dist/index.node.js.map +1 -1
  33. package/dist/primitives/select/aparte-optgroup.d.ts +2 -0
  34. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  35. package/dist/primitives/select/aparte-select.d.ts +2 -0
  36. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  37. package/dist/renderers/index.d.ts +1 -1
  38. package/dist/renderers/index.d.ts.map +1 -1
  39. package/dist/renderers/segment-renderers.d.ts +23 -0
  40. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  41. package/dist/types/imperative-api.d.ts +13 -0
  42. package/dist/types/imperative-api.d.ts.map +1 -1
  43. package/dist/types/models.d.ts +56 -9
  44. package/dist/types/models.d.ts.map +1 -1
  45. package/dist/utils/files-to-attachments.d.ts +27 -0
  46. package/dist/utils/files-to-attachments.d.ts.map +1 -0
  47. package/dist/utils/is-awaiting-reply.d.ts +22 -0
  48. package/dist/utils/is-awaiting-reply.d.ts.map +1 -0
  49. package/package.json +8 -1
  50. package/dist/index-CW2BCAqn.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { N as resolveConfig, y as getSegmentRenderer, O as runWithConfig, M as MessageRepository, H as populateBubbleFromMessage, d as AparteConfig, p as buildElicitationPanel } from "./index-CW2BCAqn.js";
2
- import { A, a, b, c, e, f, g, h, i, B, C, D, j, k, l, m, n, o, q, r, s, t, u, v, w, x, z, E, F, G, I, J, K, L, P } from "./index-CW2BCAqn.js";
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";
3
3
  class AparteOption extends HTMLElement {
4
4
  static get observedAttributes() {
5
5
  return ["value", "disabled", "selected", "data-status"];
@@ -73,6 +73,8 @@ if (!customElements.get("aparte-option")) {
73
73
  customElements.define("aparte-option", AparteOption);
74
74
  }
75
75
  class AparteOptgroup extends HTMLElement {
76
+ /** Ids for the label span the group points `aria-labelledby` at. */
77
+ static _labelIdSeq = 0;
76
78
  static get observedAttributes() {
77
79
  return ["label", "collapsible", "collapsed", "loading"];
78
80
  }
@@ -121,10 +123,11 @@ class AparteOptgroup extends HTMLElement {
121
123
  if (!existingHeader) {
122
124
  const header = document.createElement("div");
123
125
  header.className = "aparte-optgroup-header";
124
- header.setAttribute("aria-label", this.label);
125
126
  const labelSpan = document.createElement("span");
126
127
  labelSpan.className = "aparte-optgroup-label";
128
+ labelSpan.id = `aparte-optgroup-label-${++AparteOptgroup._labelIdSeq}`;
127
129
  labelSpan.textContent = this.label;
130
+ this.setAttribute("aria-labelledby", labelSpan.id);
128
131
  header.appendChild(labelSpan);
129
132
  if (this.collapsible) {
130
133
  const chevron = document.createElement("span");
@@ -179,6 +182,8 @@ if (!customElements.get("aparte-optgroup")) {
179
182
  }
180
183
  class AparteSelect extends HTMLElement {
181
184
  static _optIdSeq = 0;
185
+ /** Fallback ids for the listbox `aria-controls` target when the host has no id. */
186
+ static _listboxSeq = 0;
182
187
  _value = "";
183
188
  _isOpen = false;
184
189
  _activeIndex = -1;
@@ -271,17 +276,21 @@ class AparteSelect extends HTMLElement {
271
276
  trigger.append(labelSpan, chevronSpan);
272
277
  const dropdown = document.createElement("div");
273
278
  dropdown.className = "aparte-select-dropdown";
274
- dropdown.setAttribute("role", "listbox");
275
279
  dropdown.hidden = !this._isOpen;
276
280
  if (searchable) {
277
281
  const searchInput = document.createElement("input");
278
282
  searchInput.type = "text";
279
283
  searchInput.className = "aparte-select-search";
280
284
  searchInput.placeholder = "Search...";
285
+ searchInput.setAttribute("aria-label", "Search options");
281
286
  dropdown.appendChild(searchInput);
282
287
  }
283
288
  const optionsContainer = document.createElement("div");
284
289
  optionsContainer.className = "aparte-select-options";
290
+ optionsContainer.setAttribute("role", "listbox");
291
+ optionsContainer.setAttribute("aria-label", trigger.getAttribute("aria-label") ?? placeholder);
292
+ optionsContainer.id = this.id ? `${this.id}-listbox` : `aparte-listbox-${++AparteSelect._listboxSeq}`;
293
+ trigger.setAttribute("aria-controls", optionsContainer.id);
285
294
  slottedChildren.forEach((child) => {
286
295
  if (child.tagName === "APARTE-OPTION" || child.tagName === "APARTE-OPTGROUP") {
287
296
  optionsContainer.appendChild(child);
@@ -589,9 +598,15 @@ if (!customElements.get("aparte-progress-spinner")) {
589
598
  }
590
599
  class AparteChat extends HTMLElement {
591
600
  static get observedAttributes() {
592
- return ["placeholder", "disabled", "center-empty"];
601
+ return ["placeholder", "disabled", "center-empty", "attachments"];
593
602
  }
594
603
  _observer = null;
604
+ /**
605
+ * True only for the composition THIS element injected. An author-provided
606
+ * composer (or a `framework-managed` host) is never edited by the attachments
607
+ * toggle below — those own their own markup.
608
+ */
609
+ _ownsShell = false;
595
610
  connectedCallback() {
596
611
  this._render();
597
612
  this._forwardAttr("placeholder");
@@ -608,6 +623,10 @@ class AparteChat extends HTMLElement {
608
623
  this._syncEmptyWatch();
609
624
  return;
610
625
  }
626
+ if (name === "attachments") {
627
+ this._syncAttachments();
628
+ return;
629
+ }
611
630
  const composer = this.querySelector("aparte-composer");
612
631
  if (!composer) return;
613
632
  if (newValue !== null) composer.setAttribute(name, newValue);
@@ -626,17 +645,43 @@ class AparteChat extends HTMLElement {
626
645
  if (this.querySelector("aparte-chat-viewport")) return;
627
646
  const placeholder = this.getAttribute("placeholder");
628
647
  const composerAttrs = (placeholder !== null ? ` placeholder="${this._escapeAttr(placeholder)}"` : "") + (this.hasAttribute("disabled") ? " disabled" : "");
648
+ const attachments = this.hasAttribute("attachments");
629
649
  this.innerHTML = `
630
650
  <aparte-chat-viewport></aparte-chat-viewport>
631
651
  <aparte-composer${composerAttrs}>
632
652
  <div class="aparte-composer-shell">
653
+ ${attachments ? "<aparte-composer-attachments></aparte-composer-attachments>" : ""}
633
654
  <div class="aparte-composer-row">
655
+ ${attachments ? "<aparte-composer-add-attachment></aparte-composer-add-attachment>" : ""}
634
656
  <aparte-composer-input></aparte-composer-input>
635
657
  <aparte-composer-send></aparte-composer-send>
636
658
  </div>
637
659
  </div>
638
660
  </aparte-composer>
639
661
  `;
662
+ this._ownsShell = true;
663
+ }
664
+ /**
665
+ * Add/remove the two attachment primitives on the composition we injected, so
666
+ * toggling the attribute after mount works like the wrappers' reactive prop
667
+ * (there, a re-render does it). Author-provided markup is left alone.
668
+ */
669
+ _syncAttachments() {
670
+ if (!this._ownsShell) return;
671
+ const composer = this.composer;
672
+ const shell = composer?.querySelector(".aparte-composer-shell");
673
+ const row = shell?.querySelector(".aparte-composer-row");
674
+ if (!composer || !shell || !row) return;
675
+ const strip = shell.querySelector("aparte-composer-attachments");
676
+ const picker = row.querySelector("aparte-composer-add-attachment");
677
+ if (this.hasAttribute("attachments")) {
678
+ if (!strip) shell.insertBefore(document.createElement("aparte-composer-attachments"), row);
679
+ if (!picker) row.insertBefore(document.createElement("aparte-composer-add-attachment"), row.firstChild);
680
+ return;
681
+ }
682
+ strip?.remove();
683
+ picker?.remove();
684
+ composer.clearAttachments();
640
685
  }
641
686
  /** Set an attribute on the inner composer only when the shell carries it. */
642
687
  _forwardAttr(name) {
@@ -676,7 +721,13 @@ let _warnedNoRenderer = false;
676
721
  function warnMissingRenderer(type) {
677
722
  if (_warnedNoRenderer) return;
678
723
  _warnedNoRenderer = true;
679
- console.warn(`[aparte] No renderer for segment "${type}". Did you call registerDefaultRenderers() from @aparte/core?`);
724
+ 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`);
725
+ }
726
+ function resolveSegmentRenderer(type) {
727
+ const renderer = getSegmentRenderer(type);
728
+ if (renderer) return renderer;
729
+ installDefaultRenderersOnce();
730
+ return getSegmentRenderer(type);
680
731
  }
681
732
  function segmentRenderResultToElement(result) {
682
733
  if (result instanceof HTMLElement) return result;
@@ -691,6 +742,7 @@ class AparteChatBubble extends HTMLElement {
691
742
  _attachmentsEl = null;
692
743
  _actionBarEl = null;
693
744
  _branchPickerEl = null;
745
+ _footerEl = null;
694
746
  _content = "";
695
747
  _streaming = false;
696
748
  _segments = [];
@@ -795,11 +847,13 @@ class AparteChatBubble extends HTMLElement {
795
847
  setSegments(segments) {
796
848
  this._segments = segments;
797
849
  this._renderSegments();
850
+ this._updateWaiting();
798
851
  }
799
852
  /** Add a segment */
800
853
  addSegment(segment) {
801
854
  this._segments.push(segment);
802
855
  this._appendSegmentEl(segment);
856
+ this._updateWaiting();
803
857
  }
804
858
  /** Update a specific segment */
805
859
  updateSegment(segmentId, updates) {
@@ -830,6 +884,7 @@ class AparteChatBubble extends HTMLElement {
830
884
  }
831
885
  const el = this._segmentsEl?.querySelector(`[data-segment-id="${cssEscape(segmentId)}"]`);
832
886
  el?.remove();
887
+ this._updateWaiting();
833
888
  }
834
889
  /** Set attachments (chips shown above message content, user role only) */
835
890
  setAttachments(attachments) {
@@ -838,8 +893,12 @@ class AparteChatBubble extends HTMLElement {
838
893
  }
839
894
  /**
840
895
  * Set token usage + timing for this message (assistant only).
841
- * Renders the info ("i") action in the action bar; clicking it opens
842
- * the app-owned stats popover (`aparte-message-info`).
896
+ *
897
+ * This is the *precondition* for the info ("i") action, not the trigger: the
898
+ * button appears only if the app also declared it wants it —
899
+ * `AparteConfig.setBubbleActions({ info: true })` — because the stats popover it
900
+ * opens (`aparte-message-info`) is the app's, and core has none. Without usage
901
+ * there is nothing to show, so the button never renders either way.
843
902
  */
844
903
  setUsage(usage) {
845
904
  this._usage = usage ?? null;
@@ -892,7 +951,7 @@ class AparteChatBubble extends HTMLElement {
892
951
  console.warn(`[AparteChatBubble] _appendSegmentEl ABORT: _segmentsEl is null`);
893
952
  return;
894
953
  }
895
- const renderer = getSegmentRenderer(segment.type);
954
+ const renderer = resolveSegmentRenderer(segment.type);
896
955
  if (renderer) {
897
956
  const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
898
957
  if (el) {
@@ -915,7 +974,7 @@ class AparteChatBubble extends HTMLElement {
915
974
  this._renderSegments();
916
975
  return;
917
976
  }
918
- const renderer = getSegmentRenderer(segment.type);
977
+ const renderer = resolveSegmentRenderer(segment.type);
919
978
  if (!renderer) return;
920
979
  if (renderer.update) {
921
980
  runWithConfig(this._cfg, () => renderer.update(el, segment));
@@ -976,6 +1035,10 @@ class AparteChatBubble extends HTMLElement {
976
1035
  <div class="aparte-message-content">
977
1036
  <div class="aparte-segments"></div>
978
1037
  <div class="aparte-content"></div>
1038
+ <div class="aparte-waiting" hidden>
1039
+ <span class="aparte-dots" aria-hidden="true"><span class="aparte-dot"></span><span class="aparte-dot"></span><span class="aparte-dot"></span></span>
1040
+ <span class="aparte-sr-only"></span>
1041
+ </div>
979
1042
  </div>
980
1043
  <div class="aparte-footer">
981
1044
  <div class="aparte-branch-picker" hidden>
@@ -994,9 +1057,32 @@ class AparteChatBubble extends HTMLElement {
994
1057
  this._attachmentsEl = this.querySelector(".aparte-attachments");
995
1058
  this._actionBarEl = this.querySelector(".aparte-action-bar");
996
1059
  this._branchPickerEl = this.querySelector(".aparte-branch-picker");
1060
+ this._footerEl = this.querySelector(".aparte-footer");
997
1061
  this._setupBranchPickerListeners();
998
1062
  this._updateActionBar();
999
1063
  this._renderAvatar();
1064
+ if (this._streaming) this._updateStreaming(true);
1065
+ this._updateWaiting();
1066
+ }
1067
+ /**
1068
+ * Show the built-in waiting indicator while this bubble is in flight and has
1069
+ * nothing to show yet — the gap between "user sends" and the first token, which
1070
+ * used to be a bubble with a name and an empty body.
1071
+ *
1072
+ * The dots are CSS (no per-token work, themable, honours reduced-motion); the
1073
+ * accessible name is `locale.typing`, next to the `aria-busy` the streaming state
1074
+ * already sets. A custom bubble shell without the region simply has no indicator
1075
+ * (same null-guarded degradation as the other region hooks).
1076
+ */
1077
+ _updateWaiting() {
1078
+ const el = this.querySelector(".aparte-waiting");
1079
+ if (!el) return;
1080
+ const waiting = this._streaming && this._role !== "user" && this._segments.length === 0 && !this._content.trim();
1081
+ el.hidden = !waiting;
1082
+ if (!waiting) return;
1083
+ const label = this._cfg.getLocale().typing;
1084
+ const sr = el.querySelector(".aparte-sr-only");
1085
+ if (sr && sr.textContent !== label) sr.textContent = label;
1000
1086
  }
1001
1087
  /**
1002
1088
  * Hand the avatar host element off to the registered AvatarProvider, if
@@ -1047,10 +1133,12 @@ class AparteChatBubble extends HTMLElement {
1047
1133
  if (!this._contentEl) return;
1048
1134
  if (this._segments.length > 0) {
1049
1135
  this._contentEl.style.display = "none";
1136
+ this._updateWaiting();
1050
1137
  return;
1051
1138
  }
1052
1139
  this._contentEl.style.display = "";
1053
1140
  this._contentEl.innerHTML = this._cfg.renderMarkdown(this._content);
1141
+ this._updateWaiting();
1054
1142
  if (!this._streaming) this._highlightContentCode();
1055
1143
  }
1056
1144
  /**
@@ -1084,7 +1172,7 @@ class AparteChatBubble extends HTMLElement {
1084
1172
  if (!this._segmentsEl) return;
1085
1173
  this._segmentsEl.innerHTML = "";
1086
1174
  for (const segment of this._segments) {
1087
- const renderer = getSegmentRenderer(segment.type);
1175
+ const renderer = resolveSegmentRenderer(segment.type);
1088
1176
  if (renderer) {
1089
1177
  const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1090
1178
  if (el) {
@@ -1162,8 +1250,11 @@ class AparteChatBubble extends HTMLElement {
1162
1250
  }
1163
1251
  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>`;
1164
1252
  }).join("");
1253
+ if (!this._cfg.getHostHandlers().attachmentPreview) return;
1165
1254
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
1166
- tile.addEventListener("click", () => {
1255
+ tile.setAttribute("role", "button");
1256
+ tile.setAttribute("tabindex", "0");
1257
+ const open = () => {
1167
1258
  const img = tile.querySelector(".aparte-thumb__img");
1168
1259
  if (!img) return;
1169
1260
  this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
@@ -1171,6 +1262,13 @@ class AparteChatBubble extends HTMLElement {
1171
1262
  composed: true,
1172
1263
  detail: { url: img.src, name: tile.getAttribute("title") ?? "" }
1173
1264
  }));
1265
+ };
1266
+ tile.addEventListener("click", open);
1267
+ tile.addEventListener("keydown", (e2) => {
1268
+ const key = e2.key;
1269
+ if (key !== "Enter" && key !== " ") return;
1270
+ e2.preventDefault();
1271
+ open();
1174
1272
  });
1175
1273
  });
1176
1274
  }
@@ -1202,9 +1300,11 @@ class AparteChatBubble extends HTMLElement {
1202
1300
  if (!this._branchPickerEl) return;
1203
1301
  if (this._siblingCount <= 1 || this._role !== "assistant") {
1204
1302
  this._branchPickerEl.hidden = true;
1303
+ this._syncFooterVisibility();
1205
1304
  return;
1206
1305
  }
1207
1306
  this._branchPickerEl.hidden = false;
1307
+ this._syncFooterVisibility();
1208
1308
  const label = this._branchPickerEl.querySelector(".aparte-branch-label");
1209
1309
  if (label) {
1210
1310
  const customNav = this._cfg.getSiblingNavRenderer?.();
@@ -1251,12 +1351,7 @@ class AparteChatBubble extends HTMLElement {
1251
1351
  buttons.push(this._actionButtonHtml("thumbUp", icons, locale));
1252
1352
  buttons.push(this._actionButtonHtml("thumbDown", icons, locale));
1253
1353
  }
1254
- if (this._usage) {
1255
- const infoLabel = locale.messageInfo ?? "Details";
1256
- buttons.push(`<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${infoLabel}" title="${infoLabel}">
1257
- ${INFO_ICON_SVG}
1258
- </button>`);
1259
- }
1354
+ if (config.info) buttons.push(this._actionButtonHtml("info", icons, locale));
1260
1355
  }
1261
1356
  }
1262
1357
  this._actionBarEl.innerHTML = buttons.join("");
@@ -1264,6 +1359,21 @@ class AparteChatBubble extends HTMLElement {
1264
1359
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1265
1360
  btn.addEventListener("click", (e2) => this._handleActionClick(e2));
1266
1361
  });
1362
+ this._syncFooterVisibility();
1363
+ }
1364
+ /**
1365
+ * An empty action bar is not a bar: with every action off it was still a
1366
+ * `role="toolbar"` with nothing in it (announced as such), and it still reserved
1367
+ * its fixed height plus the footer's under every bubble. So both follow their
1368
+ * contents — the footer stays as long as the branch picker or the bar has
1369
+ * something to show.
1370
+ */
1371
+ _syncFooterVisibility() {
1372
+ if (this._actionBarEl) this._actionBarEl.hidden = this._actionBarEl.children.length === 0;
1373
+ if (!this._footerEl) return;
1374
+ const barEmpty = !this._actionBarEl || this._actionBarEl.hidden;
1375
+ const pickerHidden = !this._branchPickerEl || this._branchPickerEl.hidden;
1376
+ this._footerEl.hidden = barEmpty && pickerHidden;
1267
1377
  }
1268
1378
  /** Append the registered custom action buttons for this bubble's role. */
1269
1379
  _appendCustomActions(icons) {
@@ -1304,6 +1414,11 @@ class AparteChatBubble extends HTMLElement {
1304
1414
  const l2 = locale.feedbackNegative ?? "Bad response";
1305
1415
  return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${l2}" title="${l2}">${icons.thumbDown()}</button>`;
1306
1416
  }
1417
+ case "info": {
1418
+ if (!this._usage) return "";
1419
+ const l2 = locale.messageInfo ?? "Details";
1420
+ return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${l2}" title="${l2}">${INFO_ICON_SVG}</button>`;
1421
+ }
1307
1422
  default:
1308
1423
  return "";
1309
1424
  }
@@ -1318,6 +1433,7 @@ class AparteChatBubble extends HTMLElement {
1318
1433
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1319
1434
  btn.addEventListener("click", (e2) => this._handleActionClick(e2));
1320
1435
  });
1436
+ this._syncFooterVisibility();
1321
1437
  }
1322
1438
  _handleActionClick(e2) {
1323
1439
  const btn = e2.currentTarget;
@@ -1505,6 +1621,7 @@ class AparteChatBubble extends HTMLElement {
1505
1621
  message.classList.remove("aparte-message-streaming");
1506
1622
  }
1507
1623
  }
1624
+ this._updateWaiting();
1508
1625
  if (wasStreaming && !streaming) this._highlightContentCode();
1509
1626
  }
1510
1627
  }
@@ -1754,9 +1871,13 @@ class AparteChatViewport extends HTMLElement {
1754
1871
  if (!message.segments) {
1755
1872
  message.segments = [];
1756
1873
  }
1757
- const segment = message.segments.find((s2) => s2.id === segmentId);
1874
+ const index = message.segments.findIndex((s2) => s2.id === segmentId);
1875
+ const segment = index === -1 ? void 0 : message.segments[index];
1758
1876
  if (segment && "content" in segment) {
1759
- segment.content += chunk;
1877
+ message.segments[index] = {
1878
+ ...segment,
1879
+ content: segment.content + chunk
1880
+ };
1760
1881
  }
1761
1882
  this.dispatchEvent(new CustomEvent("aparte-segment-update", {
1762
1883
  bubbles: true,
@@ -1832,8 +1953,10 @@ class AparteChatViewport extends HTMLElement {
1832
1953
  this.updateSegment(messageId, segmentId, { isStreaming: false });
1833
1954
  }
1834
1955
  /**
1835
- * Persist token usage on a message and propagate to the live bubble so the
1836
- * perf chip (tokens/sec) renders in the action bar.
1956
+ * Persist token usage on a message and propagate to the live bubble, which is
1957
+ * what allows the info ("i") action to render — provided the app declared it
1958
+ * with `AparteConfig.setBubbleActions({ info: true })`; it is off by default,
1959
+ * since the popover it opens belongs to the app.
1837
1960
  */
1838
1961
  setUsage(messageId, usage) {
1839
1962
  const message = this._repo.getMessageById(messageId);
@@ -1903,7 +2026,7 @@ class AparteChatViewport extends HTMLElement {
1903
2026
  bubble.setAttribute("role", message.role);
1904
2027
  if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
1905
2028
  if (message.content) bubble.setAttribute("content", message.content);
1906
- if (message.status === "streaming" || message.status === "pending") {
2029
+ if (isAwaitingReply(message)) {
1907
2030
  bubble.setAttribute("streaming", "");
1908
2031
  }
1909
2032
  if (this._bottomSpacer && this._bottomSpacer.parentNode === wrapper) {
@@ -1911,6 +2034,7 @@ class AparteChatViewport extends HTMLElement {
1911
2034
  } else {
1912
2035
  wrapper.appendChild(bubble);
1913
2036
  }
2037
+ populateBubbleFromMessage(bubble, message);
1914
2038
  }
1915
2039
  }
1916
2040
  this._pruneRenderedBubbles();
@@ -1991,7 +2115,7 @@ class AparteChatViewport extends HTMLElement {
1991
2115
  if (currentIdx === -1) return;
1992
2116
  const targetIdx = direction === "prev" ? currentIdx - 1 : currentIdx + 1;
1993
2117
  if (targetIdx < 0 || targetIdx >= siblings.length) return;
1994
- this._isAutoScrollEnabled = false;
2118
+ this._isAutoScrollEnabled = this._isAtBottom();
1995
2119
  this._updateScrollButton();
1996
2120
  this._repo.switchToBranch(siblings[targetIdx]);
1997
2121
  this._reRenderActivePath();
@@ -2181,11 +2305,14 @@ class AparteChatViewport extends HTMLElement {
2181
2305
  case "setUsage":
2182
2306
  bubble.setUsage?.(payload);
2183
2307
  break;
2184
- case "update":
2185
- if ("status" in payload || "segments" in payload) {
2308
+ case "update": {
2309
+ const updates = payload;
2310
+ const renderable = ["status", "segments", "content", "attachments", "usage"];
2311
+ if (renderable.some((key) => key in updates)) {
2186
2312
  bubble.updateMessage?.(payload);
2187
2313
  }
2188
2314
  break;
2315
+ }
2189
2316
  case "complete":
2190
2317
  bubble.updateMessage?.(payload);
2191
2318
  break;
@@ -2222,7 +2349,7 @@ class AparteChatViewport extends HTMLElement {
2222
2349
  bubble.setAttribute("message-id", message.id);
2223
2350
  bubble.setAttribute("role", message.role);
2224
2351
  if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
2225
- if (message.status === "streaming" || message.status === "pending") {
2352
+ if (isAwaitingReply(message)) {
2226
2353
  bubble.setAttribute("streaming", "");
2227
2354
  }
2228
2355
  wrapper.appendChild(bubble);
@@ -2230,7 +2357,6 @@ class AparteChatViewport extends HTMLElement {
2230
2357
  }
2231
2358
  this._dispatchPathChanged(activeMessages, siblingsInfo);
2232
2359
  this._recalculateSpacer();
2233
- requestAnimationFrame(() => this._handleScroll());
2234
2360
  }
2235
2361
  _dispatchPathChanged(messages, siblings) {
2236
2362
  const detail = { messages, siblings };
@@ -2400,11 +2526,15 @@ class AparteChatViewport extends HTMLElement {
2400
2526
  });
2401
2527
  }
2402
2528
  }
2529
+ /** Is the scroll surface within `_scrollThreshold` of its bottom, right now? */
2530
+ _isAtBottom() {
2531
+ if (!this._container) return true;
2532
+ const { scrollTop, scrollHeight, clientHeight } = this._container;
2533
+ return scrollHeight - scrollTop - clientHeight <= this._scrollThreshold;
2534
+ }
2403
2535
  _handleScroll() {
2404
2536
  if (!this._container) return;
2405
- const { scrollTop, scrollHeight, clientHeight } = this._container;
2406
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
2407
- this._isAutoScrollEnabled = distanceFromBottom <= this._scrollThreshold;
2537
+ this._isAutoScrollEnabled = this._isAtBottom();
2408
2538
  this._updateScrollButton();
2409
2539
  }
2410
2540
  _scrollToBottom() {
@@ -2423,11 +2553,20 @@ class AparteChatViewport extends HTMLElement {
2423
2553
  return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
2424
2554
  }
2425
2555
  /**
2426
- * Show/hide the scroll-to-bottom button based on the current auto-scroll state.
2427
- * Hidden when already at bottom (_isAutoScrollEnabled = true).
2556
+ * Show/hide the scroll-to-bottom button from the **current geometry**, not from
2557
+ * `_isAutoScrollEnabled`.
2558
+ *
2559
+ * The two answer different questions: the flag is intent ("should new content
2560
+ * pull the view down"), the button is a fact ("is there anything below the
2561
+ * fold"). Mirroring the flag made the button lie whenever the two diverged —
2562
+ * `navigateBranch` deliberately disarms auto-follow, so swapping a branch while
2563
+ * already at the bottom of a scrollable transcript left the button offering to
2564
+ * scroll nowhere (reported from bonaparte, React). Re-derived on scroll, on the
2565
+ * post-mutation frame and after a path swap, so it converges to the truth
2566
+ * whatever a framework's render timing does in between.
2428
2567
  */
2429
2568
  _updateScrollButton() {
2430
- this._scrollBtn?.classList.toggle("aparte-scroll-btn--hidden", this._isAutoScrollEnabled);
2569
+ this._scrollBtn?.classList.toggle("aparte-scroll-btn--hidden", this._isAtBottom());
2431
2570
  }
2432
2571
  /**
2433
2572
  * Recalculate the bottom spacer height so the last user message is always
@@ -2486,6 +2625,7 @@ class AparteChatViewport extends HTMLElement {
2486
2625
  this._spacerRafId = requestAnimationFrame(() => {
2487
2626
  this._spacerRafId = null;
2488
2627
  this._recalculateSpacer();
2628
+ this._updateScrollButton();
2489
2629
  });
2490
2630
  }
2491
2631
  /**
@@ -2993,9 +3133,9 @@ class AparteComposerInput extends HTMLElement {
2993
3133
  }
2994
3134
  _updatePlaceholder() {
2995
3135
  if (this._editor) {
2996
- const p = this._getPlaceholder();
2997
- this._editor.setAttribute("data-placeholder", p);
2998
- this._editor.setAttribute("aria-label", p);
3136
+ const p2 = this._getPlaceholder();
3137
+ this._editor.setAttribute("data-placeholder", p2);
3138
+ this._editor.setAttribute("aria-label", p2);
2999
3139
  }
3000
3140
  }
3001
3141
  _updatePlaceholderVisibility() {
@@ -3226,8 +3366,11 @@ class AparteComposerAttachments extends HTMLElement {
3226
3366
  if (root) root.removeAttachment(root.attachments[i2]);
3227
3367
  });
3228
3368
  });
3369
+ if (!resolveConfig(this).getHostHandlers().attachmentPreview) return;
3229
3370
  this.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
3230
- tile.addEventListener("click", () => {
3371
+ tile.setAttribute("role", "button");
3372
+ tile.setAttribute("tabindex", "0");
3373
+ const open = () => {
3231
3374
  const img = tile.querySelector(".aparte-thumb__img");
3232
3375
  if (!img) return;
3233
3376
  this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
@@ -3235,6 +3378,13 @@ class AparteComposerAttachments extends HTMLElement {
3235
3378
  composed: true,
3236
3379
  detail: { url: img.src, name: tile.getAttribute("title") ?? "" }
3237
3380
  }));
3381
+ };
3382
+ tile.addEventListener("click", open);
3383
+ tile.addEventListener("keydown", (e2) => {
3384
+ const key = e2.key;
3385
+ if (key !== "Enter" && key !== " ") return;
3386
+ e2.preventDefault();
3387
+ open();
3238
3388
  });
3239
3389
  });
3240
3390
  }
@@ -3729,36 +3879,40 @@ export {
3729
3879
  i as AparteStreamParser,
3730
3880
  B as BackendTransport,
3731
3881
  C as ConversationManager,
3732
- D as DEFAULT_ICON_FALLBACKS,
3733
- j as DEFAULT_LOCALE,
3734
- k as DEFAULT_SKELETON_FALLBACKS,
3735
- l as DEFAULT_UI_EVENTS,
3736
- m as DirectTransport,
3882
+ D as DEFAULT_BUBBLE_ACTIONS,
3883
+ j as DEFAULT_HOST_HANDLERS,
3884
+ k as DEFAULT_ICON_FALLBACKS,
3885
+ l as DEFAULT_LOCALE,
3886
+ m as DEFAULT_SKELETON_FALLBACKS,
3887
+ n as DEFAULT_UI_EVENTS,
3888
+ o as DirectTransport,
3737
3889
  MessageRepository,
3738
- n as applyElementProps,
3739
- o as attachConfig,
3890
+ p as applyElementProps,
3891
+ q as attachConfig,
3740
3892
  buildElicitationPanel,
3741
- q as collectRendererStyles,
3742
- r as contentToText,
3743
- s as contextConfig,
3744
- t as createAparteChatHandler,
3745
- u as createStreamAdapter,
3746
- v as defaultSanitizer,
3747
- w as deriveArtifactKind,
3748
- x as detachConfig,
3893
+ s as collectRendererStyles,
3894
+ t as contentToText,
3895
+ u as contextConfig,
3896
+ v as createAparteChatHandler,
3897
+ w as createStreamAdapter,
3898
+ x as defaultSanitizer,
3899
+ y as deriveArtifactKind,
3900
+ z as detachConfig,
3901
+ E as filesToAttachments,
3749
3902
  getSegmentRenderer,
3750
- z as isFormatAdapter,
3751
- E as isSafeUrl,
3752
- F as parseAparteEventStream,
3753
- G as parseMarkdownToSegments,
3903
+ isAwaitingReply,
3904
+ H as isFormatAdapter,
3905
+ I as isSafeUrl,
3906
+ J as parseAparteEventStream,
3907
+ K as parseMarkdownToSegments,
3754
3908
  populateBubbleFromMessage,
3755
- I as readableToAsyncIterable,
3909
+ N as readableToAsyncIterable,
3756
3910
  registerAllComponents,
3757
- J as registerDefaultRenderers,
3758
- K as registerSegmentRenderer,
3759
- L as requestUserInput,
3911
+ O as registerDefaultRenderers,
3912
+ P as registerSegmentRenderer,
3913
+ Q as requestUserInput,
3760
3914
  resolveConfig,
3761
3915
  runWithConfig,
3762
- P as unregisterSegmentRenderer
3916
+ T as unregisterSegmentRenderer
3763
3917
  };
3764
3918
  //# sourceMappingURL=index.js.map