@aparte/core 0.13.0 → 0.14.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 (97) hide show
  1. package/README.md +2 -2
  2. package/dist/client/aparte-client.d.ts +36 -35
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/client/stream-adapter.d.ts +37 -145
  5. package/dist/client/stream-adapter.d.ts.map +1 -1
  6. package/dist/components/bubble/aparte-chat-bubble.d.ts +10 -2
  7. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-add-attachment.d.ts +6 -6
  9. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts +9 -1
  11. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer.d.ts +12 -3
  13. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  14. package/dist/components/context/aparte-context.d.ts +81 -0
  15. package/dist/components/context/aparte-context.d.ts.map +1 -0
  16. package/dist/components/context/index.d.ts +3 -0
  17. package/dist/components/context/index.d.ts.map +1 -0
  18. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  19. package/dist/components/index.d.ts +4 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/suggestions/aparte-suggestions.d.ts +95 -0
  22. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -0
  23. package/dist/components/suggestions/index.d.ts +3 -0
  24. package/dist/components/suggestions/index.d.ts.map +1 -0
  25. package/dist/components/viewport/aparte-chat-viewport.d.ts +37 -7
  26. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  27. package/dist/config/aparte-config.d.ts +1 -21
  28. package/dist/config/aparte-config.d.ts.map +1 -1
  29. package/dist/config/index.d.ts +0 -2
  30. package/dist/config/index.d.ts.map +1 -1
  31. package/dist/config/locale.d.ts +30 -1
  32. package/dist/config/locale.d.ts.map +1 -1
  33. package/dist/conversations/index.d.ts +1 -1
  34. package/dist/conversations/index.d.ts.map +1 -1
  35. package/dist/conversations/types.d.ts +9 -57
  36. package/dist/conversations/types.d.ts.map +1 -1
  37. package/dist/custom-elements.json +6484 -6103
  38. package/dist/generated/element-attributes.d.ts +34 -6
  39. package/dist/generated/element-attributes.d.ts.map +1 -1
  40. package/dist/{glyphs-QXfbL3n_.js → glyphs-B3BZ2TIb.js} +21 -21
  41. package/dist/glyphs-B3BZ2TIb.js.map +1 -0
  42. package/dist/host/aparte-chat-host.d.ts +1 -1
  43. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  44. package/dist/icons/extended.d.ts +87 -85
  45. package/dist/icons/extended.d.ts.map +1 -1
  46. package/dist/icons/glyphs.d.ts +53 -50
  47. package/dist/icons/glyphs.d.ts.map +1 -1
  48. package/dist/icons.js +41 -41
  49. package/dist/icons.js.map +1 -1
  50. package/dist/index.css +539 -172
  51. package/dist/index.d.ts +10 -4
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +609 -183
  54. package/dist/index.js.map +1 -1
  55. package/dist/index.node.d.ts +6 -4
  56. package/dist/index.node.d.ts.map +1 -1
  57. package/dist/index.node.js +56 -56
  58. package/dist/index.node.js.map +1 -1
  59. package/dist/{is-awaiting-reply-DrO_oR32.js → is-awaiting-reply-BZ_iJ3s3.js} +188 -1017
  60. package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +1 -0
  61. package/dist/parsers/aparte-stream-parser.d.ts +6 -17
  62. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  63. package/dist/primitives/select/aparte-select.d.ts +2 -2
  64. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  65. package/dist/renderers/segment-renderers.d.ts +18 -0
  66. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  67. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  68. package/dist/runtime/message-repository.d.ts +7 -0
  69. package/dist/runtime/message-repository.d.ts.map +1 -1
  70. package/dist/transport/types.d.ts +8 -0
  71. package/dist/transport/types.d.ts.map +1 -1
  72. package/dist/types/chat.d.ts +32 -38
  73. package/dist/types/chat.d.ts.map +1 -1
  74. package/dist/types/errors.d.ts +16 -1
  75. package/dist/types/errors.d.ts.map +1 -1
  76. package/dist/types/event-map.d.ts +4 -0
  77. package/dist/types/event-map.d.ts.map +1 -1
  78. package/dist/types/events.d.ts +15 -0
  79. package/dist/types/events.d.ts.map +1 -1
  80. package/dist/types/index.d.ts +1 -1
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/dist/types/models.d.ts +1 -8
  83. package/dist/types/models.d.ts.map +1 -1
  84. package/dist/types/segments.d.ts +1 -13
  85. package/dist/types/segments.d.ts.map +1 -1
  86. package/dist/types/tools.d.ts +1 -1
  87. package/dist/utils/chat-boundary.d.ts +8 -4
  88. package/dist/utils/chat-boundary.d.ts.map +1 -1
  89. package/package.json +5 -2
  90. package/dist/client/xml-artifact-feed.d.ts +0 -76
  91. package/dist/client/xml-artifact-feed.d.ts.map +0 -1
  92. package/dist/config/skeleton-provider.d.ts +0 -26
  93. package/dist/config/skeleton-provider.d.ts.map +0 -1
  94. package/dist/glyphs-QXfbL3n_.js.map +0 -1
  95. package/dist/is-awaiting-reply-DrO_oR32.js.map +0 -1
  96. package/dist/renderers/segments/pipeline-waiting.d.ts +0 -3
  97. package/dist/renderers/segments/pipeline-waiting.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { $ as resolveConfig, a4 as subscribeConfigChange, a1 as runWithConfig, p as AparteElicitationAbortError, x as buildApprovalPanel, y as buildElicitationPanel, K as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, L as escapeHtml, a8 as writeStreamedMarkdown, O as getSegmentRenderer, P as installDefaultRenderersOnce, s as AparteMessageRepository, a9 as stampSegmentActivity, aa as stampSegmentOnInsert, ab as stampSegmentOnUpdate, ac as renumberSegments, ad as openSegmentIds, ae as isTerminalStatus, af as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls } from "./is-awaiting-reply-DrO_oR32.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-DrO_oR32.js";
1
+ import { Z as resolveConfig, a2 as subscribeConfigChange, $ as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, I as escapeAttr, E as cssEscape, a5 as mergeSegmentUpdate, J as escapeHtml, a6 as writeStreamedMarkdown, M as getSegmentRenderer, N as installDefaultRenderersOnce, r as AparteMessageRepository, a7 as stampSegmentActivity, a8 as stampSegmentOnInsert, a9 as stampSegmentOnUpdate, aa as renumberSegments, ab as openSegmentIds, ac as isTerminalStatus, ad as adoptMessageSegments, O as isAwaitingReply, U as populateBubbleFromMessage, a4 as uuid, _ as revokeAttachmentUrls } from "./is-awaiting-reply-BZ_iJ3s3.js";
2
+ import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, p, q, s, t, u, v, y, z, B, C, D, F, G, H, K, L, P, Q, R, S, T, V, W, X, Y, a0, a1, a3 } from "./is-awaiting-reply-BZ_iJ3s3.js";
3
+ import { deriveArtifactKind } from "@aparte/engine";
3
4
  class AparteOption extends HTMLElement {
4
5
  static get observedAttributes() {
5
6
  return ["value", "disabled", "selected", "data-status"];
@@ -286,7 +287,13 @@ class AparteSelect extends HTMLElement {
286
287
  trigger.setAttribute("aria-label", this.getAttribute("aria-label") || placeholder);
287
288
  const labelSpan = document.createElement("span");
288
289
  labelSpan.className = "aparte-select-label";
289
- labelSpan.textContent = placeholder;
290
+ const labelText = document.createElement("span");
291
+ labelText.className = "aparte-select-label-text";
292
+ labelText.textContent = placeholder;
293
+ const sizer = document.createElement("span");
294
+ sizer.className = "aparte-select-label-sizer";
295
+ sizer.setAttribute("aria-hidden", "true");
296
+ labelSpan.append(labelText, sizer);
290
297
  const chevronSpan = document.createElement("span");
291
298
  chevronSpan.className = "aparte-select-chevron";
292
299
  chevronSpan.innerHTML = resolveConfig(this).getIcon("expand");
@@ -329,6 +336,7 @@ class AparteSelect extends HTMLElement {
329
336
  _setupMutationObserver() {
330
337
  this._observer = new MutationObserver(() => {
331
338
  this._updateDropdownContent();
339
+ this._updateTriggerLabel();
332
340
  this._restoreActive();
333
341
  });
334
342
  this._observer.observe(this, { childList: true, subtree: true });
@@ -458,7 +466,7 @@ class AparteSelect extends HTMLElement {
458
466
  /** Highlight the option at `index` (clamped) and point aria-activedescendant at it. */
459
467
  _setActive(index) {
460
468
  const all = this.querySelectorAll("aparte-option");
461
- all.forEach((o2) => o2.removeAttribute("data-active"));
469
+ all.forEach((o) => o.removeAttribute("data-active"));
462
470
  const opts = this._visibleOptions();
463
471
  if (opts.length === 0) {
464
472
  this._activeIndex = -1;
@@ -477,7 +485,7 @@ class AparteSelect extends HTMLElement {
477
485
  _clearActive() {
478
486
  this._activeIndex = -1;
479
487
  this._trigger?.removeAttribute("aria-activedescendant");
480
- this.querySelectorAll("aparte-option").forEach((o2) => o2.removeAttribute("data-active"));
488
+ this.querySelectorAll("aparte-option").forEach((o) => o.removeAttribute("data-active"));
481
489
  }
482
490
  // ─────────────────────────────────────────────────────────────────────────
483
491
  // Actions
@@ -498,7 +506,7 @@ class AparteSelect extends HTMLElement {
498
506
  this._updatePosition();
499
507
  this._searchInput?.focus();
500
508
  const opts = this._visibleOptions();
501
- const selectedIdx = opts.findIndex((o2) => o2.getAttribute("value") === this._value);
509
+ const selectedIdx = opts.findIndex((o) => o.getAttribute("value") === this._value);
502
510
  this._setActive(selectedIdx >= 0 ? selectedIdx : 0);
503
511
  this.dispatchEvent(new CustomEvent("aparte-select-open", { bubbles: true }));
504
512
  }
@@ -551,10 +559,23 @@ class AparteSelect extends HTMLElement {
551
559
  this._trigger?.focus();
552
560
  }
553
561
  _updateTriggerLabel() {
554
- const labelEl = this._trigger?.querySelector(".aparte-select-label");
555
- if (labelEl) {
556
- const selectedLabel = this._getSelectedLabel();
557
- labelEl.textContent = selectedLabel || this.getAttribute("placeholder") || "Select...";
562
+ const placeholder = this.getAttribute("placeholder") || "Select...";
563
+ const labelText = this._trigger?.querySelector(".aparte-select-label-text");
564
+ if (labelText) {
565
+ const next = this._getSelectedLabel() || placeholder;
566
+ if (labelText.textContent !== next) labelText.textContent = next;
567
+ }
568
+ const sizer = this._trigger?.querySelector(".aparte-select-label-sizer");
569
+ if (sizer) {
570
+ const labels = [placeholder, ...Array.from(this.querySelectorAll("aparte-option"), (o) => o.textContent?.trim() || "")];
571
+ const current = Array.from(sizer.children, (s2) => s2.textContent ?? "");
572
+ if (labels.length !== current.length || labels.some((l2, i2) => l2 !== current[i2])) {
573
+ sizer.replaceChildren(...labels.map((text) => {
574
+ const span = document.createElement("span");
575
+ span.textContent = text;
576
+ return span;
577
+ }));
578
+ }
558
579
  }
559
580
  const options = this.querySelectorAll("aparte-option");
560
581
  options.forEach((opt) => {
@@ -731,11 +752,17 @@ class AparteElicitation extends HTMLElement {
731
752
  return new Promise((resolve, reject) => {
732
753
  let done = false;
733
754
  const slot = {};
755
+ const focusedBefore = document.activeElement;
734
756
  const close = () => {
735
757
  if (done) return false;
736
758
  done = true;
737
759
  this._pending = null;
760
+ const active = document.activeElement;
761
+ const focusWasInPanel = active instanceof Node && composer.contains(active);
738
762
  composer.hidePanel(slot.token);
763
+ if (focusWasInPanel && focusedBefore instanceof HTMLElement && focusedBefore.isConnected) {
764
+ focusedBefore.focus();
765
+ }
739
766
  return true;
740
767
  };
741
768
  const settle = (result) => {
@@ -827,12 +854,12 @@ class AparteElicitation extends HTMLElement {
827
854
  * resolves `undefined` on three wrappers out of four.
828
855
  */
829
856
  _pendingTargetId() {
830
- let el = this._pending?.composer ?? null;
831
- while (el) {
832
- const tag = el.tagName?.toLowerCase();
833
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
834
- if (isHost && el.id) return el.id;
835
- el = el.parentElement;
857
+ let el2 = this._pending?.composer ?? null;
858
+ while (el2) {
859
+ const tag = el2.tagName?.toLowerCase();
860
+ const isHost = tag === "aparte-chat" || el2.hasAttribute?.("data-aparte-chat");
861
+ if (isHost && el2.id) return el2.id;
862
+ el2 = el2.parentElement;
836
863
  }
837
864
  return void 0;
838
865
  }
@@ -858,7 +885,7 @@ class AparteElicitation extends HTMLElement {
858
885
  const composer = node.querySelector("aparte-composer");
859
886
  if (composer && typeof composer.showPanel === "function") return composer;
860
887
  const tag = node.tagName?.toLowerCase();
861
- const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
888
+ const isChatBoundary = tag === "aparte-chat" || node.hasAttribute?.("data-aparte-chat");
862
889
  if (isChatBoundary) break;
863
890
  node = node.parentElement;
864
891
  }
@@ -996,10 +1023,10 @@ function warnMissingRenderer(type) {
996
1023
  function unrenderedSegment(segment) {
997
1024
  const fallback = typeof segment.fallback === "string" && segment.fallback.trim() ? segment.fallback : null;
998
1025
  if (!fallback) warnMissingRenderer(segment.type);
999
- const el = document.createElement("div");
1000
- el.className = fallback ? "aparte-segment aparte-segment-fallback" : "aparte-segment aparte-segment-unknown";
1001
- el.textContent = fallback ?? `[Unknown segment type: ${segment.type}]`;
1002
- return el;
1026
+ const el2 = document.createElement("div");
1027
+ el2.className = fallback ? "aparte-segment aparte-segment-fallback" : "aparte-segment aparte-segment-unknown";
1028
+ el2.textContent = fallback ?? `[Unknown segment type: ${segment.type}]`;
1029
+ return el2;
1003
1030
  }
1004
1031
  function resolveSegmentRenderer(type, config) {
1005
1032
  const renderer = getSegmentRenderer(type, config);
@@ -1037,7 +1064,7 @@ class AparteChatBubble extends HTMLElement {
1037
1064
  /** The live inline editor (the composer's contenteditable primitive), present only while `_editing`. */
1038
1065
  _editInput = null;
1039
1066
  static get observedAttributes() {
1040
- return ["role", "data-role", "content", "timestamp", "message-id", "streaming", "name"];
1067
+ return ["data-role", "content", "timestamp", "message-id", "streaming", "name"];
1041
1068
  }
1042
1069
  constructor() {
1043
1070
  super();
@@ -1077,11 +1104,11 @@ class AparteChatBubble extends HTMLElement {
1077
1104
  for (const segment of this._segments) {
1078
1105
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1079
1106
  if (!renderer?.relabel) continue;
1080
- const el = this._segmentsEl.querySelector(
1107
+ const el2 = this._segmentsEl.querySelector(
1081
1108
  `:scope > [data-segment-id="${cssEscape(segment.id)}"]`
1082
1109
  );
1083
- if (!el) continue;
1084
- runWithConfig(this._cfg, () => renderer.relabel(el, segment));
1110
+ if (!el2) continue;
1111
+ runWithConfig(this._cfg, () => renderer.relabel(el2, segment));
1085
1112
  }
1086
1113
  }
1087
1114
  /**
@@ -1128,9 +1155,7 @@ class AparteChatBubble extends HTMLElement {
1128
1155
  attributeChangedCallback(name, oldValue, newValue) {
1129
1156
  if (oldValue === newValue) return;
1130
1157
  switch (name) {
1131
- case "role":
1132
1158
  case "data-role":
1133
- if (newValue === "article") return;
1134
1159
  if (newValue === "user" || newValue === "assistant") {
1135
1160
  this._role = newValue;
1136
1161
  this._updateRole();
@@ -1196,7 +1221,7 @@ class AparteChatBubble extends HTMLElement {
1196
1221
  }
1197
1222
  /** Update a specific segment */
1198
1223
  updateSegment(segmentId, updates) {
1199
- const index = this._segments.findIndex((s) => s.id === segmentId);
1224
+ const index = this._segments.findIndex((s2) => s2.id === segmentId);
1200
1225
  if (index !== -1) {
1201
1226
  const updated = mergeSegmentUpdate(this._segments[index], updates);
1202
1227
  this._segments[index] = updated;
@@ -1205,7 +1230,7 @@ class AparteChatBubble extends HTMLElement {
1205
1230
  }
1206
1231
  /** Append content to a segment */
1207
1232
  appendToSegment(segmentId, content) {
1208
- const segment = this._segments.find((s) => s.id === segmentId);
1233
+ const segment = this._segments.find((s2) => s2.id === segmentId);
1209
1234
  if (segment && "content" in segment) {
1210
1235
  segment.content += content;
1211
1236
  this._applySegmentUpdate(segmentId, segment, { content: segment.content });
@@ -1231,18 +1256,19 @@ class AparteChatBubble extends HTMLElement {
1231
1256
  * the human-in-the-loop control.
1232
1257
  */
1233
1258
  removeSegment(segmentId) {
1234
- const index = this._segments.findIndex((s) => s.id === segmentId);
1259
+ const index = this._segments.findIndex((s2) => s2.id === segmentId);
1235
1260
  if (index !== -1) {
1236
1261
  this._segments.splice(index, 1);
1237
1262
  }
1238
- const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1239
- el?.remove();
1263
+ const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1264
+ el2?.remove();
1240
1265
  this._updateWaiting();
1241
1266
  }
1242
1267
  /** Set attachments (chips shown above message content, user role only) */
1243
1268
  setAttachments(attachments) {
1244
1269
  this._attachments = attachments;
1245
1270
  this._updateAttachments();
1271
+ this._updateWaiting();
1246
1272
  }
1247
1273
  /**
1248
1274
  * Set token usage + timing for this message (assistant only).
@@ -1294,6 +1320,7 @@ class AparteChatBubble extends HTMLElement {
1294
1320
  if ("attachments" in updates) {
1295
1321
  this._attachments = updates.attachments ?? [];
1296
1322
  this._updateAttachments();
1323
+ this._updateWaiting();
1297
1324
  }
1298
1325
  }
1299
1326
  // ─────────────────────────────────────────────────────────────────────────
@@ -1306,10 +1333,10 @@ class AparteChatBubble extends HTMLElement {
1306
1333
  }
1307
1334
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1308
1335
  if (renderer) {
1309
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1310
- if (el) {
1311
- this._segmentsEl.appendChild(el);
1312
- runWithConfig(this._cfg, () => renderer.setup?.(el, segment));
1336
+ const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1337
+ if (el2) {
1338
+ this._segmentsEl.appendChild(el2);
1339
+ runWithConfig(this._cfg, () => renderer.setup?.(el2, segment));
1313
1340
  }
1314
1341
  } else {
1315
1342
  this._segmentsEl.appendChild(unrenderedSegment(segment));
@@ -1318,27 +1345,27 @@ class AparteChatBubble extends HTMLElement {
1318
1345
  this._reflectError();
1319
1346
  }
1320
1347
  _applySegmentUpdate(segmentId, segment, updates) {
1321
- const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1322
- if (!el) {
1348
+ const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1349
+ if (!el2) {
1323
1350
  this._renderSegments();
1324
1351
  return;
1325
1352
  }
1326
1353
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1327
1354
  if (!renderer) return;
1328
1355
  if (renderer.update) {
1329
- runWithConfig(this._cfg, () => renderer.update(el, segment));
1356
+ runWithConfig(this._cfg, () => renderer.update(el2, segment));
1330
1357
  } else {
1331
1358
  const newEl = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1332
1359
  if (newEl) {
1333
- el.replaceWith(newEl);
1360
+ el2.replaceWith(newEl);
1334
1361
  runWithConfig(this._cfg, () => renderer.setup?.(newEl, segment));
1335
1362
  }
1336
1363
  }
1337
1364
  if ("collapsed" in updates) {
1338
1365
  if (updates.collapsed) {
1339
- el.removeAttribute("open");
1366
+ el2.removeAttribute("open");
1340
1367
  } else {
1341
- el.setAttribute("open", "");
1368
+ el2.setAttribute("open", "");
1342
1369
  }
1343
1370
  }
1344
1371
  }
@@ -1354,8 +1381,7 @@ class AparteChatBubble extends HTMLElement {
1354
1381
  }
1355
1382
  _render() {
1356
1383
  const dataRole = this.getAttribute("data-role");
1357
- const legacyRole = this.getAttribute("role");
1358
- const role = dataRole && dataRole !== "article" ? dataRole : legacyRole && legacyRole !== "article" ? legacyRole : "assistant";
1384
+ const role = dataRole === "user" || dataRole === "assistant" ? dataRole : "assistant";
1359
1385
  this._role = role;
1360
1386
  if (this.getAttribute("role") !== "article") {
1361
1387
  this.setAttribute("role", "article");
@@ -1414,6 +1440,7 @@ class AparteChatBubble extends HTMLElement {
1414
1440
  this._actionBarEl = this.querySelector(".aparte-action-bar");
1415
1441
  this._branchPickerEl = this.querySelector(".aparte-branch-picker");
1416
1442
  this._footerEl = this.querySelector(".aparte-footer");
1443
+ this._transcriptBusy = !!this.closest("aparte-chat-viewport")?.hasAttribute("data-busy");
1417
1444
  this._updateActionBar();
1418
1445
  this._renderAvatar();
1419
1446
  if (this._streaming) this._updateStreaming(true);
@@ -1434,12 +1461,16 @@ class AparteChatBubble extends HTMLElement {
1434
1461
  const waiting = this._streaming && this._role !== "user" && empty;
1435
1462
  const box = this.querySelector(".aparte-message-content");
1436
1463
  if (box) box.hidden = empty && !waiting;
1437
- const el = this.querySelector(".aparte-waiting");
1438
- if (!el) return;
1439
- el.hidden = !waiting;
1464
+ const message = this.querySelector(".aparte-message");
1465
+ if (message) {
1466
+ message.toggleAttribute("data-empty", empty && !this._streaming && this._attachments.length === 0);
1467
+ }
1468
+ const el2 = this.querySelector(".aparte-waiting");
1469
+ if (!el2) return;
1470
+ el2.hidden = !waiting;
1440
1471
  if (!waiting) return;
1441
1472
  const label = this._cfg.getLocale().typing;
1442
- const sr = el.querySelector(".aparte-sr-only");
1473
+ const sr = el2.querySelector(".aparte-sr-only");
1443
1474
  if (sr && sr.textContent !== label) sr.textContent = label;
1444
1475
  }
1445
1476
  /**
@@ -1541,10 +1572,10 @@ class AparteChatBubble extends HTMLElement {
1541
1572
  for (const segment of this._segments) {
1542
1573
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1543
1574
  if (renderer) {
1544
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1545
- if (el) {
1546
- this._segmentsEl.appendChild(el);
1547
- runWithConfig(this._cfg, () => renderer.setup?.(el, segment));
1575
+ const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1576
+ if (el2) {
1577
+ this._segmentsEl.appendChild(el2);
1578
+ runWithConfig(this._cfg, () => renderer.setup?.(el2, segment));
1548
1579
  }
1549
1580
  } else {
1550
1581
  this._segmentsEl.appendChild(unrenderedSegment(segment));
@@ -1565,7 +1596,7 @@ class AparteChatBubble extends HTMLElement {
1565
1596
  _reflectError() {
1566
1597
  const message = this.querySelector(".aparte-message");
1567
1598
  if (!message) return;
1568
- const hasError = this._segments.some((s) => s.type === "error");
1599
+ const hasError = this._segments.some((s2) => s2.type === "error");
1569
1600
  if (hasError) message.setAttribute("data-error", "");
1570
1601
  else message.removeAttribute("data-error");
1571
1602
  }
@@ -1600,18 +1631,18 @@ class AparteChatBubble extends HTMLElement {
1600
1631
  const customAttachment = this._cfg.getAttachmentRenderer?.();
1601
1632
  if (customAttachment) {
1602
1633
  this._attachmentsEl.replaceChildren();
1603
- for (const a4 of this._attachments) {
1604
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a4)));
1605
- if (el) this._attachmentsEl.appendChild(el);
1634
+ for (const a2 of this._attachments) {
1635
+ const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a2)));
1636
+ if (el2) this._attachmentsEl.appendChild(el2);
1606
1637
  }
1607
1638
  return;
1608
1639
  }
1609
- this._attachmentsEl.innerHTML = this._attachments.map((a4) => {
1610
- const name = escapeHtml(a4.name);
1611
- if (a4.type.startsWith("image/")) {
1612
- 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>`;
1640
+ this._attachmentsEl.innerHTML = this._attachments.map((a2) => {
1641
+ const name = escapeHtml(a2.name);
1642
+ if (a2.type.startsWith("image/")) {
1643
+ return `<div class="aparte-thumbnail 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>`;
1613
1644
  }
1614
- 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>`;
1645
+ return `<div class="aparte-thumbnail 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>`;
1615
1646
  }).join("");
1616
1647
  if (!this._cfg.getHostHandlers().attachmentPreview) return;
1617
1648
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
@@ -1676,6 +1707,25 @@ class AparteChatBubble extends HTMLElement {
1676
1707
  detail
1677
1708
  }));
1678
1709
  };
1710
+ /**
1711
+ * Whether a reply is streaming somewhere in this bubble's transcript. Set by the
1712
+ * viewport (`data-busy` on it, pushed here), read once on connect for a bubble
1713
+ * mounted while the flag was already up. While busy, the branch arrows and the
1714
+ * retry/edit actions are disabled — the transcript is read-only except for Stop.
1715
+ */
1716
+ _transcriptBusy = false;
1717
+ setTranscriptBusy(busy) {
1718
+ if (this._transcriptBusy === busy) return;
1719
+ this._transcriptBusy = busy;
1720
+ this._applyTranscriptBusy();
1721
+ this._updateBranchPicker();
1722
+ }
1723
+ _applyTranscriptBusy() {
1724
+ if (!this._actionBarEl) return;
1725
+ for (const btn of this._actionBarEl.querySelectorAll('[data-action="retry"], [data-action="edit"]')) {
1726
+ btn.disabled = this._transcriptBusy;
1727
+ }
1728
+ }
1679
1729
  _updateBranchPicker() {
1680
1730
  if (!this._branchPickerEl) return;
1681
1731
  if (this._siblingCount <= 1 || this._role !== "assistant") {
@@ -1703,8 +1753,8 @@ class AparteChatBubble extends HTMLElement {
1703
1753
  }
1704
1754
  const prevBtn = this._branchPickerEl.querySelector(".aparte-branch-prev");
1705
1755
  const nextBtn = this._branchPickerEl.querySelector(".aparte-branch-next");
1706
- if (prevBtn) prevBtn.disabled = this._siblingIndex === 0;
1707
- if (nextBtn) nextBtn.disabled = this._siblingIndex === this._siblingCount - 1;
1756
+ if (prevBtn) prevBtn.disabled = this._transcriptBusy || this._siblingIndex === 0;
1757
+ if (nextBtn) nextBtn.disabled = this._transcriptBusy || this._siblingIndex === this._siblingCount - 1;
1708
1758
  }
1709
1759
  // ─────────────────────────────────────────────────────────────────────────
1710
1760
  // Action Bar
@@ -1721,14 +1771,14 @@ class AparteChatBubble extends HTMLElement {
1721
1771
  const buttons = [];
1722
1772
  if (this._role === "user") {
1723
1773
  if (config.user) {
1724
- for (const a4 of config.user) buttons.push(this._actionButtonHtml(a4, icons, locale));
1774
+ for (const a2 of config.user) buttons.push(this._actionButtonHtml(a2, icons, locale));
1725
1775
  } else {
1726
1776
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
1727
1777
  if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
1728
1778
  }
1729
1779
  } else if (this._role === "assistant") {
1730
1780
  if (config.assistant) {
1731
- for (const a4 of config.assistant) buttons.push(this._actionButtonHtml(a4, icons, locale));
1781
+ for (const a2 of config.assistant) buttons.push(this._actionButtonHtml(a2, icons, locale));
1732
1782
  } else {
1733
1783
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
1734
1784
  if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
@@ -1741,6 +1791,7 @@ class AparteChatBubble extends HTMLElement {
1741
1791
  }
1742
1792
  this._actionBarEl.innerHTML = buttons.join("");
1743
1793
  this._appendCustomActions(icons);
1794
+ this._applyTranscriptBusy();
1744
1795
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1745
1796
  btn.addEventListener("click", (e2) => this._handleActionClick(e2));
1746
1797
  });
@@ -1759,20 +1810,21 @@ class AparteChatBubble extends HTMLElement {
1759
1810
  const barEmpty = !this._actionBarEl || this._actionBarEl.hidden;
1760
1811
  const pickerHidden = !this._branchPickerEl || this._branchPickerEl.hidden;
1761
1812
  this._footerEl.hidden = barEmpty && pickerHidden;
1813
+ this.querySelector(".aparte-message")?.toggleAttribute("data-branches", !pickerHidden);
1762
1814
  }
1763
1815
  /** Append the registered custom action buttons for this bubble's role. */
1764
1816
  _appendCustomActions(icons) {
1765
1817
  if (!this._actionBarEl) return;
1766
- for (const a4 of this._cfg.getActions("bubble")) {
1767
- const roles = a4.bubble?.roles ?? ["user", "assistant"];
1818
+ for (const a2 of this._cfg.getActions("bubble")) {
1819
+ const roles = a2.bubble?.roles ?? ["user", "assistant"];
1768
1820
  if (!roles.includes(this._role)) continue;
1769
1821
  const btn = document.createElement("button");
1770
1822
  btn.className = "aparte-btn aparte-btn--icon aparte-action-btn aparte-action-custom";
1771
- btn.dataset["action"] = `custom:${a4.id}`;
1772
- btn.setAttribute("aria-label", a4.label);
1773
- btn.setAttribute("title", a4.label);
1774
- const fromProvider = icons[a4.icon];
1775
- btn.innerHTML = a4.icon.startsWith("<") ? a4.icon : typeof fromProvider === "function" ? fromProvider() : a4.iconFallback ?? "";
1823
+ btn.dataset["action"] = `custom:${a2.id}`;
1824
+ btn.setAttribute("aria-label", a2.label);
1825
+ btn.setAttribute("title", a2.label);
1826
+ const fromProvider = icons[a2.icon];
1827
+ btn.innerHTML = a2.icon.startsWith("<") ? a2.icon : typeof fromProvider === "function" ? fromProvider() : a2.iconFallback ?? "";
1776
1828
  this._actionBarEl.appendChild(btn);
1777
1829
  }
1778
1830
  }
@@ -1843,7 +1895,7 @@ class AparteChatBubble extends HTMLElement {
1843
1895
  }
1844
1896
  switch (action) {
1845
1897
  case "copy": {
1846
- const text = this._content || this._segments.map((s) => s.content ?? "").join("\n");
1898
+ const text = this._content || this._segments.filter((s2) => s2.type !== "thinking").map((s2) => s2.content ?? "").join("\n");
1847
1899
  const icons = this._cfg.getIconProvider();
1848
1900
  const locale = this._cfg.getLocale();
1849
1901
  navigator.clipboard.writeText(text).then(() => {
@@ -1977,12 +2029,12 @@ class AparteChatBubble extends HTMLElement {
1977
2029
  }
1978
2030
  }
1979
2031
  _resolveTargetId() {
1980
- let el = this.parentElement;
1981
- while (el) {
1982
- const tag = el.tagName?.toLowerCase();
1983
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
1984
- if (isHost && el.id) return el.id;
1985
- el = el.parentElement;
2032
+ let el2 = this.parentElement;
2033
+ while (el2) {
2034
+ const tag = el2.tagName?.toLowerCase();
2035
+ const isHost = tag === "aparte-chat" || el2.hasAttribute?.("data-aparte-chat");
2036
+ if (isHost && el2.id) return el2.id;
2037
+ el2 = el2.parentElement;
1986
2038
  }
1987
2039
  return void 0;
1988
2040
  }
@@ -2152,6 +2204,19 @@ class AparteChatViewport extends HTMLElement {
2152
2204
  _layoutTransitionMs = 0;
2153
2205
  _repo = new AparteMessageRepository();
2154
2206
  _isAutoScrollEnabled = true;
2207
+ /** The last scroll position we saw, so growth can be told from a gesture. */
2208
+ _lastScrollTop = 0;
2209
+ /** The scroll height at the last scroll event: how much it moved since bounds what churn can do. */
2210
+ _lastScrollHeight = 0;
2211
+ /** When this component last moved `scrollTop` itself — see `_handleScroll`. "Never" is
2212
+ * -Infinity rather than 0: `performance.now()` can be under a second old. */
2213
+ _ownScrollAt = Number.NEGATIVE_INFINITY;
2214
+ /** When the reader last touched the transcript (wheel, touch, pointer, key). */
2215
+ _readerInputAt = Number.NEGATIVE_INFINITY;
2216
+ /** A decrease inside this window after our own scroll, with no gesture, is the browser's. */
2217
+ _ownScrollWindowMs = 1e3;
2218
+ /** The keys that scroll a focused scroll container — the only keydowns that are a gesture. */
2219
+ _scrollKeys = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "]);
2155
2220
  _scrollThreshold = 50;
2156
2221
  /** When true, the next _autoScroll() call uses smooth instead of instant, then resets. */
2157
2222
  _smoothScrollOnce = false;
@@ -2162,8 +2227,6 @@ class AparteChatViewport extends HTMLElement {
2162
2227
  * snapshot stay intact; only the oldest rendered bubbles are dropped from view.
2163
2228
  */
2164
2229
  _maxRenderedBubbles = 1e3;
2165
- /** One-time guard for the deprecated `maxMessages` warning. */
2166
- _warnedMaxMessagesDeprecation = false;
2167
2230
  _resizeObserver = null;
2168
2231
  _mutationObserver = null;
2169
2232
  _boundResetHandler = null;
@@ -2174,7 +2237,7 @@ class AparteChatViewport extends HTMLElement {
2174
2237
  */
2175
2238
  _frameworkManagedDOM = false;
2176
2239
  static get observedAttributes() {
2177
- return ["scroll-threshold", "max-rendered-bubbles", "max-messages"];
2240
+ return ["scroll-threshold", "max-rendered-bubbles"];
2178
2241
  }
2179
2242
  constructor() {
2180
2243
  super();
@@ -2198,6 +2261,7 @@ class AparteChatViewport extends HTMLElement {
2198
2261
  const detail = e2.detail;
2199
2262
  if (detail?.config && detail.config !== resolveConfig(this)) return;
2200
2263
  this._applyDirection();
2264
+ this._scrollBtn?.setAttribute("aria-label", resolveConfig(this).t("scrollToBottom"));
2201
2265
  };
2202
2266
  /** Mirror `locale.direction` onto the scroll container. */
2203
2267
  _applyDirection() {
@@ -2225,11 +2289,6 @@ class AparteChatViewport extends HTMLElement {
2225
2289
  this._maxRenderedBubbles = parseInt(newValue || "1000", 10);
2226
2290
  this._pruneRenderedBubbles();
2227
2291
  break;
2228
- case "max-messages":
2229
- this._warnMaxMessagesDeprecated();
2230
- this._maxRenderedBubbles = parseInt(newValue || "1000", 10);
2231
- this._pruneRenderedBubbles();
2232
- break;
2233
2292
  }
2234
2293
  }
2235
2294
  /**
@@ -2243,11 +2302,6 @@ class AparteChatViewport extends HTMLElement {
2243
2302
  this._maxRenderedBubbles = config.maxRenderedBubbles;
2244
2303
  this._pruneRenderedBubbles();
2245
2304
  }
2246
- if (config.maxMessages !== void 0) {
2247
- this._warnMaxMessagesDeprecated();
2248
- this._maxRenderedBubbles = config.maxMessages;
2249
- this._pruneRenderedBubbles();
2250
- }
2251
2305
  if (config.layoutTransitionMs !== void 0) {
2252
2306
  this._layoutTransitionMs = config.layoutTransitionMs;
2253
2307
  }
@@ -2281,7 +2335,7 @@ class AparteChatViewport extends HTMLElement {
2281
2335
  if (!message.segments) {
2282
2336
  message.segments = [];
2283
2337
  }
2284
- const index = message.segments.findIndex((s) => s.id === segmentId);
2338
+ const index = message.segments.findIndex((s2) => s2.id === segmentId);
2285
2339
  const segment = index === -1 ? void 0 : message.segments[index];
2286
2340
  if (segment && "content" in segment) {
2287
2341
  message.segments[index] = {
@@ -2323,6 +2377,24 @@ class AparteChatViewport extends HTMLElement {
2323
2377
  }
2324
2378
  return null;
2325
2379
  }
2380
+ /**
2381
+ * While a reply streams, the transcript is read-only except for Stop.
2382
+ *
2383
+ * The streaming message's own footer was already hidden; every OTHER message kept
2384
+ * its branch picker and its retry/edit buttons live. Swapping a branch mid-stream
2385
+ * re-rendered the active path under the reply being written, and a retry cut that
2386
+ * reply off to start another — both seen on the landing page. The state that
2387
+ * decides it is the whole transcript's, so it lives on the viewport (`data-busy`)
2388
+ * and is pushed to the bubbles it holds; a bubble mounted later, under a
2389
+ * framework's DOM, reads the attribute itself when it connects.
2390
+ */
2391
+ _syncBusy() {
2392
+ const busy = this._streamingMessageId() !== null;
2393
+ this.toggleAttribute("data-busy", busy);
2394
+ for (const bubble of this.querySelectorAll("aparte-chat-bubble")) {
2395
+ bubble.setTranscriptBusy?.(busy);
2396
+ }
2397
+ }
2326
2398
  addSegment(messageIdOrSegment, maybeSegment) {
2327
2399
  const messageId = typeof messageIdOrSegment === "string" ? messageIdOrSegment : this._activeMessageId();
2328
2400
  const segment = typeof messageIdOrSegment === "string" ? maybeSegment : messageIdOrSegment;
@@ -2343,15 +2415,15 @@ class AparteChatViewport extends HTMLElement {
2343
2415
  this._notifyBubble(messageId, "addSegment", stamped);
2344
2416
  this._autoScroll();
2345
2417
  }
2346
- updateSegment(a4, b2, c2) {
2418
+ updateSegment(a2, b2, c2) {
2347
2419
  const clientForm = c2 === void 0 && typeof b2 === "object";
2348
- const messageId = clientForm ? this._activeMessageId() : a4;
2349
- const segmentId = clientForm ? a4 : b2;
2420
+ const messageId = clientForm ? this._activeMessageId() : a2;
2421
+ const segmentId = clientForm ? a2 : b2;
2350
2422
  const updates = clientForm ? b2 : c2;
2351
2423
  if (!messageId) return;
2352
2424
  const message = this._repo.getMessageById(messageId);
2353
2425
  if (!message?.segments) return;
2354
- const segmentIndex = message.segments.findIndex((s) => s.id === segmentId);
2426
+ const segmentIndex = message.segments.findIndex((s2) => s2.id === segmentId);
2355
2427
  if (segmentIndex !== -1) {
2356
2428
  const current = message.segments[segmentIndex];
2357
2429
  const stamped = stampSegmentOnUpdate(current, updates);
@@ -2359,14 +2431,14 @@ class AparteChatViewport extends HTMLElement {
2359
2431
  this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
2360
2432
  }
2361
2433
  }
2362
- removeSegment(a4, b2) {
2434
+ removeSegment(a2, b2) {
2363
2435
  const clientForm = b2 === void 0;
2364
- const messageId = clientForm ? this._activeMessageId() : a4;
2365
- const segmentId = clientForm ? a4 : b2;
2436
+ const messageId = clientForm ? this._activeMessageId() : a2;
2437
+ const segmentId = clientForm ? a2 : b2;
2366
2438
  if (!messageId || !segmentId) return;
2367
2439
  const message = this._repo.getMessageById(messageId);
2368
2440
  if (message?.segments) {
2369
- const idx = message.segments.findIndex((s) => s.id === segmentId);
2441
+ const idx = message.segments.findIndex((s2) => s2.id === segmentId);
2370
2442
  if (idx !== -1) {
2371
2443
  message.segments.splice(idx, 1);
2372
2444
  renumberSegments(message.segments);
@@ -2412,6 +2484,7 @@ class AparteChatViewport extends HTMLElement {
2412
2484
  message.status = "completed";
2413
2485
  this._settleSegments(messageId, message);
2414
2486
  this._notifyBubble(messageId, "complete", { status: "completed" });
2487
+ this._syncBusy();
2415
2488
  this._recalculateSpacer();
2416
2489
  }
2417
2490
  }
@@ -2443,6 +2516,7 @@ class AparteChatViewport extends HTMLElement {
2443
2516
  if (isTerminalStatus(updates.status)) this._settleSegments(messageId, message);
2444
2517
  }
2445
2518
  this._notifyBubble(messageId, "update", updates);
2519
+ if (updates.status) this._syncBusy();
2446
2520
  this._autoScroll();
2447
2521
  }
2448
2522
  /**
@@ -2457,6 +2531,7 @@ class AparteChatViewport extends HTMLElement {
2457
2531
  addMessage(message) {
2458
2532
  this._repo.addOrUpdateMessage(this._repo.headId, adoptMessageSegments({ ...message }));
2459
2533
  this._pruneRenderedBubbles();
2534
+ this._syncBusy();
2460
2535
  this._autoScroll();
2461
2536
  }
2462
2537
  /**
@@ -2479,13 +2554,14 @@ class AparteChatViewport extends HTMLElement {
2479
2554
  },
2480
2555
  []
2481
2556
  ) } : { ...message };
2557
+ if (!options?.historical && isAwaitingReply(message)) stored.isStreaming = true;
2482
2558
  this._repo.addOrUpdateMessage(this._repo.headId, stored);
2483
2559
  if (!this._frameworkManagedDOM) {
2484
2560
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2485
2561
  if (wrapper) {
2486
2562
  const bubble = document.createElement("aparte-chat-bubble");
2487
2563
  bubble.setAttribute("message-id", message.id);
2488
- bubble.setAttribute("role", message.role);
2564
+ bubble.setAttribute("data-role", message.role);
2489
2565
  if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
2490
2566
  if (message.content) bubble.setAttribute("content", message.content);
2491
2567
  if (isAwaitingReply(message)) {
@@ -2500,10 +2576,13 @@ class AparteChatViewport extends HTMLElement {
2500
2576
  }
2501
2577
  }
2502
2578
  this._pruneRenderedBubbles();
2579
+ this._syncBusy();
2503
2580
  this._recalculateSpacer();
2504
2581
  if (message.role === "user") {
2505
2582
  this._isAutoScrollEnabled = true;
2506
- requestAnimationFrame(() => this._smoothScrollToBottom());
2583
+ requestAnimationFrame(() => {
2584
+ if (this._isAutoScrollEnabled) this._smoothScrollToBottom();
2585
+ });
2507
2586
  } else {
2508
2587
  this._autoScroll();
2509
2588
  }
@@ -2562,7 +2641,9 @@ class AparteChatViewport extends HTMLElement {
2562
2641
  const meta = this._repo.getMessage(existingId);
2563
2642
  if (!meta) return null;
2564
2643
  const parentId = meta.message.role === "user" ? existingId : meta.parentId;
2565
- this._repo.addOrUpdateMessage(parentId, { ...newMessage });
2644
+ const stored = { ...newMessage };
2645
+ if (isAwaitingReply(newMessage)) stored.isStreaming = true;
2646
+ this._repo.addOrUpdateMessage(parentId, stored);
2566
2647
  this._repo.switchToBranch(newMessage.id);
2567
2648
  this._reRenderActivePath();
2568
2649
  return newMessage.id;
@@ -2572,6 +2653,7 @@ class AparteChatViewport extends HTMLElement {
2572
2653
  * Triggers a full re-render of the active path.
2573
2654
  */
2574
2655
  navigateBranch(messageId, direction) {
2656
+ if (this._streamingMessageId() !== null) return;
2575
2657
  const siblings = this._repo.getBranches(messageId);
2576
2658
  const currentIdx = siblings.indexOf(messageId);
2577
2659
  if (currentIdx === -1) return;
@@ -2676,6 +2758,7 @@ class AparteChatViewport extends HTMLElement {
2676
2758
  }
2677
2759
  }
2678
2760
  this._repo.clear();
2761
+ this._syncBusy();
2679
2762
  if (!this._frameworkManagedDOM) {
2680
2763
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2681
2764
  if (wrapper) {
@@ -2825,7 +2908,7 @@ class AparteChatViewport extends HTMLElement {
2825
2908
  const sibInfo = siblingsInfo[i2];
2826
2909
  const bubble = document.createElement("aparte-chat-bubble");
2827
2910
  bubble.setAttribute("message-id", message.id);
2828
- bubble.setAttribute("role", message.role);
2911
+ bubble.setAttribute("data-role", message.role);
2829
2912
  if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
2830
2913
  if (isAwaitingReply(message)) {
2831
2914
  bubble.setAttribute("streaming", "");
@@ -2834,6 +2917,7 @@ class AparteChatViewport extends HTMLElement {
2834
2917
  populateBubbleFromMessage(bubble, message, sibInfo);
2835
2918
  }
2836
2919
  this._dispatchPathChanged(activeMessages, siblingsInfo);
2920
+ this._syncBusy();
2837
2921
  this._recalculateSpacer();
2838
2922
  }
2839
2923
  _dispatchPathChanged(messages, siblings) {
@@ -2848,9 +2932,13 @@ class AparteChatViewport extends HTMLElement {
2848
2932
  if (this._isAutoScrollEnabled) {
2849
2933
  if (this._smoothScrollOnce) {
2850
2934
  this._smoothScrollOnce = false;
2851
- requestAnimationFrame(() => this._smoothScrollToBottom());
2935
+ requestAnimationFrame(() => {
2936
+ if (this._isAutoScrollEnabled) this._smoothScrollToBottom();
2937
+ });
2852
2938
  } else {
2853
- requestAnimationFrame(() => this._scrollToBottom());
2939
+ requestAnimationFrame(() => {
2940
+ if (this._isAutoScrollEnabled) this._scrollToBottom();
2941
+ });
2854
2942
  }
2855
2943
  }
2856
2944
  this._pruneRenderedBubbles();
@@ -2895,7 +2983,7 @@ class AparteChatViewport extends HTMLElement {
2895
2983
  this._scrollBtn = document.createElement("button");
2896
2984
  this._scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
2897
2985
  this._scrollBtn.setAttribute("type", "button");
2898
- this._scrollBtn.setAttribute("aria-label", "Scroll to bottom");
2986
+ this._scrollBtn.setAttribute("aria-label", resolveConfig(this).t("scrollToBottom"));
2899
2987
  const scrollIcon = resolveConfig(this).getIcon("scrollDown");
2900
2988
  this._scrollBtn.innerHTML = scrollIcon;
2901
2989
  this.appendChild(this._scrollBtn);
@@ -2928,7 +3016,7 @@ class AparteChatViewport extends HTMLElement {
2928
3016
  const scrollBtn = document.createElement("button");
2929
3017
  scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
2930
3018
  scrollBtn.setAttribute("type", "button");
2931
- scrollBtn.setAttribute("aria-label", "Scroll to bottom");
3019
+ scrollBtn.setAttribute("aria-label", resolveConfig(this).t("scrollToBottom"));
2932
3020
  const scrollIcon = resolveConfig(this).getIcon("scrollDown");
2933
3021
  scrollBtn.innerHTML = scrollIcon;
2934
3022
  this.appendChild(scrollBtn);
@@ -2973,6 +3061,27 @@ class AparteChatViewport extends HTMLElement {
2973
3061
  this._smoothScrollToBottom();
2974
3062
  this._updateScrollButton();
2975
3063
  };
3064
+ /**
3065
+ * Only the inputs that can SCROLL count as the reader's hand: a wheel notch, a
3066
+ * touch, a navigation key, a press in the scrollbar's gutter. A press on a control
3067
+ * inside the transcript — a branch arrow, a copy button — is not a scroll gesture,
3068
+ * and counting it disarmed the follow through the swap it triggered (react-webkit,
3069
+ * 1 run in 6: the rebuild's height churn moved scrollTop within the second after
3070
+ * the click, and the click made that look like the reader's).
3071
+ */
3072
+ _noteReaderInput = (e2) => {
3073
+ if (e2.type === "keydown" && !this._scrollKeys.has(e2.key)) return;
3074
+ if (e2.type === "pointerdown") {
3075
+ const container = this._container;
3076
+ if (!container) return;
3077
+ const { clientX } = e2;
3078
+ const rect = container.getBoundingClientRect();
3079
+ const rtl = getComputedStyle(container).direction === "rtl";
3080
+ const inGutter = rtl ? clientX <= rect.right - container.clientWidth : clientX >= rect.left + container.clientWidth;
3081
+ if (!inGutter) return;
3082
+ }
3083
+ this._readerInputAt = performance.now();
3084
+ };
2976
3085
  _onBranchNavigate = (e2) => {
2977
3086
  const evt = e2;
2978
3087
  evt.stopPropagation();
@@ -2980,6 +3089,9 @@ class AparteChatViewport extends HTMLElement {
2980
3089
  };
2981
3090
  _setupEventListeners() {
2982
3091
  this._container?.addEventListener("scroll", this._handleScroll, { passive: true });
3092
+ for (const type of ["wheel", "touchmove", "pointerdown", "keydown"]) {
3093
+ this._container?.addEventListener(type, this._noteReaderInput, { passive: true });
3094
+ }
2983
3095
  this._scrollBtn?.addEventListener("click", this._onScrollBtnClick);
2984
3096
  this.addEventListener("aparte-branch-navigate", this._onBranchNavigate);
2985
3097
  }
@@ -2989,6 +3101,7 @@ class AparteChatViewport extends HTMLElement {
2989
3101
  this._scrollToBottom();
2990
3102
  }
2991
3103
  this._recalculateSpacer();
3104
+ this._updateScrollButton();
2992
3105
  });
2993
3106
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2994
3107
  if (this._container) {
@@ -3001,7 +3114,9 @@ class AparteChatViewport extends HTMLElement {
3001
3114
  this._mutationObserver = new MutationObserver(() => {
3002
3115
  if (this._frameworkManagedDOM) this._keepScrollButtonLast();
3003
3116
  if (this._isAutoScrollEnabled) {
3004
- requestAnimationFrame(() => this._scrollToBottom());
3117
+ requestAnimationFrame(() => {
3118
+ if (this._isAutoScrollEnabled) this._scrollToBottom();
3119
+ });
3005
3120
  }
3006
3121
  this._scheduleSpacerUpdate();
3007
3122
  });
@@ -3021,11 +3136,25 @@ class AparteChatViewport extends HTMLElement {
3021
3136
  }
3022
3137
  _handleScroll() {
3023
3138
  if (!this._container) return;
3024
- this._isAutoScrollEnabled = this._isAtBottom();
3139
+ const top = this._container.scrollTop;
3140
+ const height = this._container.scrollHeight;
3141
+ const drop = this._lastScrollTop - top;
3142
+ const churn = Math.abs(height - this._lastScrollHeight);
3143
+ const now = performance.now();
3144
+ const settlingOurs = drop <= churn + 2 && now - this._ownScrollAt < this._ownScrollWindowMs && now - this._readerInputAt >= this._ownScrollWindowMs;
3145
+ const readerWentUp = drop > 1 && !settlingOurs;
3146
+ this._lastScrollTop = top;
3147
+ this._lastScrollHeight = height;
3148
+ if (this._isAtBottom()) {
3149
+ this._isAutoScrollEnabled = true;
3150
+ } else if (readerWentUp) {
3151
+ this._isAutoScrollEnabled = false;
3152
+ }
3025
3153
  this._updateScrollButton();
3026
3154
  }
3027
3155
  _scrollToBottom() {
3028
3156
  if (!this._container) return;
3157
+ this._ownScrollAt = performance.now();
3029
3158
  this._container.scrollTop = this._container.scrollHeight;
3030
3159
  this._settleAtBottom(4);
3031
3160
  }
@@ -3059,12 +3188,14 @@ class AparteChatViewport extends HTMLElement {
3059
3188
  if (!this._container || !this._isAutoScrollEnabled) return;
3060
3189
  const max = this._container.scrollHeight - this._container.clientHeight;
3061
3190
  if (max - this._container.scrollTop <= 1) return;
3191
+ this._ownScrollAt = performance.now();
3062
3192
  this._container.scrollTop = max;
3063
3193
  this._settleAtBottom(framesLeft - 1);
3064
3194
  });
3065
3195
  }
3066
3196
  _smoothScrollToBottom() {
3067
3197
  if (!this._container) return;
3198
+ this._ownScrollAt = performance.now();
3068
3199
  if (typeof this._container.scrollTo === "function" && !this._prefersReducedMotion()) {
3069
3200
  this._container.scrollTo({ top: this._container.scrollHeight, behavior: "smooth" });
3070
3201
  } else {
@@ -3083,7 +3214,7 @@ class AparteChatViewport extends HTMLElement {
3083
3214
  * fold"). Mirroring the flag made the button lie whenever the two diverged —
3084
3215
  * `navigateBranch` deliberately disarms auto-follow, so swapping a branch while
3085
3216
  * already at the bottom of a scrollable transcript left the button offering to
3086
- * scroll nowhere (reported from bonaparte, React). Re-derived on scroll, on the
3217
+ * scroll nowhere (reported from a React consumer). Re-derived on scroll, on the
3087
3218
  * post-mutation frame and after a path swap, so it converges to the truth
3088
3219
  * whatever a framework's render timing does in between.
3089
3220
  */
@@ -3110,7 +3241,7 @@ class AparteChatViewport extends HTMLElement {
3110
3241
  this._setSpacerHeight(0);
3111
3242
  return;
3112
3243
  }
3113
- const lastUserBubble = [...allBubbles].reverse().find((b2) => b2.getAttribute("role") === "user");
3244
+ const lastUserBubble = [...allBubbles].reverse().find((b2) => b2.getAttribute("data-role") === "user");
3114
3245
  if (!lastUserBubble) {
3115
3246
  this._setSpacerHeight(0);
3116
3247
  return;
@@ -3169,15 +3300,11 @@ class AparteChatViewport extends HTMLElement {
3169
3300
  bubbles[i2]?.remove();
3170
3301
  }
3171
3302
  }
3172
- _warnMaxMessagesDeprecated() {
3173
- if (this._warnedMaxMessagesDeprecation) return;
3174
- this._warnedMaxMessagesDeprecation = true;
3175
- console.warn(
3176
- "[Aparte] `maxMessages` / `max-messages` on aparte-chat-viewport is deprecated: it used to silently evict messages from the conversation model. It now only caps rendered bubbles in the DOM — use `maxRenderedBubbles` / `max-rendered-bubbles`. For actual history retention, configure it on your AparteConversationManager instead."
3177
- );
3178
- }
3179
3303
  _cleanup() {
3180
3304
  this._container?.removeEventListener("scroll", this._handleScroll);
3305
+ for (const type of ["wheel", "touchmove", "pointerdown", "keydown"]) {
3306
+ this._container?.removeEventListener(type, this._noteReaderInput);
3307
+ }
3181
3308
  this._scrollBtn?.removeEventListener("click", this._onScrollBtnClick);
3182
3309
  this.removeEventListener("aparte-branch-navigate", this._onBranchNavigate);
3183
3310
  this._resizeObserver?.disconnect();
@@ -3282,6 +3409,7 @@ class AparteComposer extends HTMLElement {
3282
3409
  window.addEventListener("aparte-message-error", this._onMessageDone);
3283
3410
  window.addEventListener("aparte-message-aborted", this._onMessageDone);
3284
3411
  window.addEventListener("aparte-config-change", this._onConfigChangeEvent);
3412
+ queueMicrotask(() => this._warnIfUnattached());
3285
3413
  this._evaluateModelGate();
3286
3414
  this._applyDirection();
3287
3415
  }
@@ -3297,6 +3425,11 @@ class AparteComposer extends HTMLElement {
3297
3425
  if (name === "disabled") {
3298
3426
  this._emit("disabled-change", { disabled: value !== null });
3299
3427
  }
3428
+ if (name === "placeholder") {
3429
+ for (const input of this.querySelectorAll("aparte-composer-input")) {
3430
+ input.syncPlaceholder?.();
3431
+ }
3432
+ }
3300
3433
  }
3301
3434
  // ── Public API ─────────────────────────────────────────────────────────
3302
3435
  get value() {
@@ -3414,10 +3547,13 @@ class AparteComposer extends HTMLElement {
3414
3547
  */
3415
3548
  showPanel(panel, options) {
3416
3549
  this._evictPanel();
3550
+ const host = this.querySelector("[data-aparte-panel-host]");
3417
3551
  const inputEl = this.querySelector("aparte-composer-input");
3418
3552
  this.setAttribute("data-panel-active", "");
3419
3553
  panel.dataset["apartePanel"] = "true";
3420
- if (inputEl) {
3554
+ if (host) {
3555
+ host.appendChild(panel);
3556
+ } else if (inputEl) {
3421
3557
  inputEl.insertAdjacentElement("afterend", panel);
3422
3558
  } else {
3423
3559
  this.appendChild(panel);
@@ -3627,15 +3763,26 @@ class AparteComposer extends HTMLElement {
3627
3763
  * for the reason written there — Angular's wrapper root IS `<aparte-chat>`, the
3628
3764
  * other three render a `[data-aparte-chat]` div.
3629
3765
  */
3766
+ /**
3767
+ * A signal, not a guard: on a page with several chats, a composer attached to none
3768
+ * answers to every chat's lifecycle events (see `_ownTargetId`), and the symptom —
3769
+ * one chat's Stop evicting another's open question — is nowhere near its cause.
3770
+ * Said once, at the console, where the developer is.
3771
+ */
3772
+ _warnIfUnattached() {
3773
+ if (!this.isConnected || this._ownTargetId()) return;
3774
+ if (document.querySelectorAll("aparte-chat, [data-aparte-chat]").length < 2) return;
3775
+ console.warn(`[aparte-composer] this composer belongs to no chat while the page has several: give it target="<chat id>" or place it under a chat host with an id, or it will answer to every chat's events.`);
3776
+ }
3630
3777
  _ownTargetId() {
3631
3778
  const attr = this.targetId;
3632
3779
  if (attr) return attr;
3633
- let el = this.parentElement;
3634
- while (el) {
3635
- const tag = el.tagName?.toLowerCase();
3636
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
3637
- if (isHost && el.id) return el.id;
3638
- el = el.parentElement;
3780
+ let el2 = this.parentElement;
3781
+ while (el2) {
3782
+ const tag = el2.tagName?.toLowerCase();
3783
+ const isHost = tag === "aparte-chat" || el2.hasAttribute?.("data-aparte-chat");
3784
+ if (isHost && el2.id) return el2.id;
3785
+ el2 = el2.parentElement;
3639
3786
  }
3640
3787
  return void 0;
3641
3788
  }
@@ -3723,6 +3870,14 @@ class AparteComposerInput extends HTMLElement {
3723
3870
  this._adjustHeight();
3724
3871
  this._getRoot()?.setValue("");
3725
3872
  }
3873
+ /**
3874
+ * Re-read the placeholder fallback chain. Called by the composer when ITS
3875
+ * `placeholder` changes — this element observes only its own attribute, and the
3876
+ * composer's is one of the three sources it falls back to.
3877
+ */
3878
+ syncPlaceholder() {
3879
+ this._updatePlaceholder();
3880
+ }
3726
3881
  /** Focus the inner contenteditable rather than the host element. */
3727
3882
  focus() {
3728
3883
  this._editor?.focus();
@@ -3760,7 +3915,7 @@ class AparteComposerInput extends HTMLElement {
3760
3915
  role="textbox"
3761
3916
  aria-multiline="true"
3762
3917
  aria-label="${placeholder}"
3763
- tabindex="0"
3918
+ tabindex="${escapeAttr(String(disabled ? -1 : 0))}"
3764
3919
  aria-disabled="${disabled}"
3765
3920
  data-placeholder="${placeholder}"
3766
3921
  ></div>`;
@@ -3785,11 +3940,6 @@ class AparteComposerInput extends HTMLElement {
3785
3940
  else this.setValue(value);
3786
3941
  })
3787
3942
  );
3788
- this._unsubscribes.push(
3789
- root._on("streaming-change", ({ streaming }) => {
3790
- this._updateDisabled(streaming || root.disabled);
3791
- })
3792
- );
3793
3943
  }
3794
3944
  _handleInput() {
3795
3945
  this._adjustHeight();
@@ -3809,6 +3959,7 @@ class AparteComposerInput extends HTMLElement {
3809
3959
  e2.preventDefault();
3810
3960
  const root = this._getRoot();
3811
3961
  if (root) {
3962
+ if (root.streaming) return;
3812
3963
  root.submit();
3813
3964
  } else {
3814
3965
  this.dispatchEvent(new CustomEvent("aparte-composer-submit", { bubbles: true }));
@@ -3866,9 +4017,9 @@ class AparteComposerInput extends HTMLElement {
3866
4017
  }
3867
4018
  _updatePlaceholder() {
3868
4019
  if (this._editor) {
3869
- const p = this._getPlaceholder();
3870
- this._editor.setAttribute("data-placeholder", p);
3871
- this._editor.setAttribute("aria-label", p);
4020
+ const p2 = this._getPlaceholder();
4021
+ this._editor.setAttribute("data-placeholder", p2);
4022
+ this._editor.setAttribute("aria-label", p2);
3872
4023
  }
3873
4024
  }
3874
4025
  _updatePlaceholderVisibility() {
@@ -3877,6 +4028,8 @@ class AparteComposerInput extends HTMLElement {
3877
4028
  if (!this._editor) return;
3878
4029
  this._editor.setAttribute("contenteditable", String(!disabled));
3879
4030
  this._editor.setAttribute("aria-disabled", String(disabled));
4031
+ this._editor.setAttribute("tabindex", disabled ? "-1" : "0");
4032
+ if (disabled && this.ownerDocument?.activeElement === this._editor) this._editor.blur();
3880
4033
  }
3881
4034
  /** Escape a value before it lands in a double-quoted HTML attribute. */
3882
4035
  }
@@ -4231,9 +4384,8 @@ class AparteComposerAddAttachment extends HTMLElement {
4231
4384
  * Re-read the label and the icon on the button that already exists.
4232
4385
  *
4233
4386
  * Deliberately not a re-render: `_render()` returns early once the button is
4234
- * there, and its `disabled` computation ignores `root.streaming` while the
4235
- * streaming listener sets `disabled` directly so rebuilding would silently
4236
- * re-enable the attach button mid-turn, and drop focus if the user were on it.
4387
+ * there, and rebuilding it would drop focus if the user were on it — a locale or
4388
+ * icon change is not a reason to lose the keyboard.
4237
4389
  * The native file input and the drag listeners live outside this element
4238
4390
  * (on `document.body` and on the composer root), so they are untouched either way.
4239
4391
  */
@@ -4270,7 +4422,7 @@ class AparteComposerAddAttachment extends HTMLElement {
4270
4422
  );
4271
4423
  this._unsubscribes.push(
4272
4424
  root._on("streaming-change", ({ streaming }) => {
4273
- if (this._button) this._button.disabled = streaming || root.disabled;
4425
+ if (this._button) this._button.disabled = root.disabled;
4274
4426
  })
4275
4427
  );
4276
4428
  }
@@ -4579,10 +4731,10 @@ class AparteConversationList extends HTMLElement {
4579
4731
  /** Update active class without full re-render (perf optimisation). */
4580
4732
  _updateActiveState() {
4581
4733
  const items = this.querySelectorAll("[data-conv-id]");
4582
- items.forEach((el) => {
4583
- const isActive = el.dataset["convId"] === this._activeId;
4584
- el.classList.toggle("aparte-conv-item--active", isActive);
4585
- el.setAttribute("aria-current", isActive ? "page" : "false");
4734
+ items.forEach((el2) => {
4735
+ const isActive = el2.dataset["convId"] === this._activeId;
4736
+ el2.classList.toggle("aparte-conv-item--active", isActive);
4737
+ el2.setAttribute("aria-current", isActive ? "page" : "false");
4586
4738
  });
4587
4739
  }
4588
4740
  // ─── Helpers ──────────────────────────────────────────────────────────
@@ -4591,6 +4743,279 @@ class AparteConversationList extends HTMLElement {
4591
4743
  }
4592
4744
  }
4593
4745
  if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
4746
+ const isSuggestion = (value) => typeof value === "string" || typeof value === "object" && value !== null && typeof value.label === "string";
4747
+ const labelOf = (s2) => typeof s2 === "string" ? s2 : s2.label;
4748
+ const promptOf = (s2) => typeof s2 === "string" ? s2 : s2.prompt ?? s2.label;
4749
+ class AparteSuggestions extends HTMLElement {
4750
+ static get observedAttributes() {
4751
+ return ["suggestions", "target"];
4752
+ }
4753
+ _suggestions = [];
4754
+ _composer = null;
4755
+ _onSend = () => {
4756
+ if (this.hasAttribute("empty-only")) this.hidden = true;
4757
+ };
4758
+ _onConfigChange = () => {
4759
+ const row = this.querySelector(".aparte-suggestions");
4760
+ if (row) row.setAttribute("aria-label", this._groupLabel());
4761
+ };
4762
+ /** The starters. Setting it re-renders; the `suggestions` attribute is the JSON form. */
4763
+ get suggestions() {
4764
+ return this._suggestions;
4765
+ }
4766
+ set suggestions(value) {
4767
+ this._suggestions = Array.isArray(value) ? value.filter(isSuggestion) : [];
4768
+ if (this.isConnected) this._render();
4769
+ }
4770
+ connectedCallback() {
4771
+ if (this.hasAttribute("suggestions")) this._readAttribute();
4772
+ this._render();
4773
+ this._watchComposer();
4774
+ window.addEventListener("aparte-config-change", this._onConfigChange);
4775
+ }
4776
+ disconnectedCallback() {
4777
+ this._composer?.removeEventListener("aparte-send", this._onSend);
4778
+ this._composer = null;
4779
+ window.removeEventListener("aparte-config-change", this._onConfigChange);
4780
+ }
4781
+ attributeChangedCallback(name) {
4782
+ if (!this.isConnected) return;
4783
+ if (name === "suggestions") {
4784
+ this._readAttribute();
4785
+ this._render();
4786
+ } else if (name === "target") {
4787
+ this._watchComposer();
4788
+ }
4789
+ }
4790
+ _groupLabel() {
4791
+ return resolveConfig(this).t("suggestionsLabel") || "Suggested prompts";
4792
+ }
4793
+ _parsed = null;
4794
+ /**
4795
+ * Parse the `suggestions` attribute once per value. The attribute callback and
4796
+ * `connectedCallback` both arrive for a parsed element, in an order the parser
4797
+ * decides; without this, an invalid value warned twice.
4798
+ */
4799
+ _readAttribute() {
4800
+ const raw = this.getAttribute("suggestions");
4801
+ if (raw === null || raw === this._parsed) return;
4802
+ this._parsed = raw;
4803
+ try {
4804
+ const parsed = JSON.parse(raw);
4805
+ this._suggestions = Array.isArray(parsed) ? parsed.filter(isSuggestion) : [];
4806
+ } catch {
4807
+ console.warn('[aparte-suggestions] `suggestions` is not a JSON array. Use ["…"] or [{"label": "…", "prompt": "…"}].');
4808
+ this._suggestions = [];
4809
+ }
4810
+ }
4811
+ /**
4812
+ * The composer the click goes through, resolved the way the composer's own parts
4813
+ * resolve it — by ancestry first. Outside a composer, `target` names the chat and the
4814
+ * composer serving it is either inside that chat or points at it; the first composer
4815
+ * on the page is the last resort, so the one-chat page needs no attribute at all.
4816
+ */
4817
+ _resolveComposer() {
4818
+ const nearest = this.closest("aparte-composer");
4819
+ if (nearest) return nearest;
4820
+ const target = this.getAttribute("target");
4821
+ if (target) {
4822
+ const scope = document.getElementById(target);
4823
+ const inside = scope?.querySelector("aparte-composer");
4824
+ if (inside) return inside;
4825
+ for (const composer of document.querySelectorAll("aparte-composer[target]")) {
4826
+ if (composer.getAttribute("target") === target) return composer;
4827
+ }
4828
+ }
4829
+ return document.querySelector("aparte-composer");
4830
+ }
4831
+ _watchComposer() {
4832
+ this._composer?.removeEventListener("aparte-send", this._onSend);
4833
+ this._composer = this._resolveComposer();
4834
+ this._composer?.addEventListener("aparte-send", this._onSend);
4835
+ }
4836
+ _render() {
4837
+ this.replaceChildren();
4838
+ if (this._suggestions.length === 0) {
4839
+ this.setAttribute("data-empty", "");
4840
+ return;
4841
+ }
4842
+ this.removeAttribute("data-empty");
4843
+ const row = document.createElement("div");
4844
+ row.className = "aparte-suggestions";
4845
+ row.setAttribute("role", "group");
4846
+ row.setAttribute("aria-label", this._groupLabel());
4847
+ for (const suggestion of this._suggestions) {
4848
+ const button = document.createElement("button");
4849
+ button.type = "button";
4850
+ button.className = "aparte-btn aparte-btn--surface aparte-btn--pill aparte-suggestion";
4851
+ button.textContent = labelOf(suggestion);
4852
+ const prompt = promptOf(suggestion);
4853
+ if (prompt !== button.textContent) button.title = prompt;
4854
+ button.addEventListener("click", () => this._activate(suggestion));
4855
+ row.appendChild(button);
4856
+ }
4857
+ this.appendChild(row);
4858
+ }
4859
+ _activate(suggestion) {
4860
+ const detail = { label: labelOf(suggestion), prompt: promptOf(suggestion) };
4861
+ const proceed = this.dispatchEvent(new CustomEvent("aparte-suggestion", {
4862
+ detail,
4863
+ bubbles: true,
4864
+ composed: true,
4865
+ cancelable: true
4866
+ }));
4867
+ if (!proceed) return;
4868
+ const composer = this._composer ?? this._resolveComposer();
4869
+ if (!composer) {
4870
+ console.warn("[aparte-suggestions] no <aparte-composer> to send through — put the element inside one, or point `target` at the chat it serves.");
4871
+ return;
4872
+ }
4873
+ if (composer !== this._composer) this._watchComposer();
4874
+ composer.setValue(detail.prompt);
4875
+ if (this.getAttribute("mode") === "fill") composer.focus();
4876
+ else composer.submit();
4877
+ }
4878
+ }
4879
+ if (!customElements.get("aparte-suggestions")) {
4880
+ customElements.define("aparte-suggestions", AparteSuggestions);
4881
+ }
4882
+ const fraction = (raw, fallback) => {
4883
+ const n2 = Number(raw);
4884
+ return raw !== null && Number.isFinite(n2) && n2 > 0 && n2 <= 1 ? n2 : fallback;
4885
+ };
4886
+ const el = (tag, className) => {
4887
+ const node = document.createElement(tag);
4888
+ node.className = className;
4889
+ return node;
4890
+ };
4891
+ class AparteContext extends HTMLElement {
4892
+ static get observedAttributes() {
4893
+ return ["window", "warn", "danger", "target"];
4894
+ }
4895
+ _used = null;
4896
+ _level = null;
4897
+ _compactRequested = false;
4898
+ _onDone = (e2) => {
4899
+ const detail = e2.detail;
4900
+ if (!detail?.usage || !this._isMine(detail.targetId)) return;
4901
+ const { usage } = detail;
4902
+ this._used = usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
4903
+ this._render();
4904
+ };
4905
+ /**
4906
+ * After a real compaction the context is smaller by an amount nobody has measured
4907
+ * yet — the next turn's usage will say. Until then the gauge shows nothing rather
4908
+ * than a number it knows to be wrong. (`aparte-compact-done` names no chat: on a
4909
+ * multi-chat page every gauge resets, and each is right again one turn later.)
4910
+ */
4911
+ _onCompacted = (e2) => {
4912
+ const detail = e2.detail;
4913
+ if (detail?.skipped) return;
4914
+ this._used = null;
4915
+ this._compactRequested = false;
4916
+ this._render();
4917
+ };
4918
+ _onRerender = () => this._render();
4919
+ /** Tokens the last turn reported, or `null` before the first turn. */
4920
+ get used() {
4921
+ return this._used;
4922
+ }
4923
+ /** The window in force — the attribute, else the current model's. */
4924
+ get window() {
4925
+ return this._window();
4926
+ }
4927
+ /** The current level, or `null` while nothing is shown. */
4928
+ get level() {
4929
+ return this._level;
4930
+ }
4931
+ connectedCallback() {
4932
+ window.addEventListener("aparte-message-done", this._onDone);
4933
+ window.addEventListener("aparte-compact-done", this._onCompacted);
4934
+ window.addEventListener("aparte-config-change", this._onRerender);
4935
+ this._render();
4936
+ }
4937
+ disconnectedCallback() {
4938
+ window.removeEventListener("aparte-message-done", this._onDone);
4939
+ window.removeEventListener("aparte-compact-done", this._onCompacted);
4940
+ window.removeEventListener("aparte-config-change", this._onRerender);
4941
+ }
4942
+ attributeChangedCallback() {
4943
+ if (this.isConnected) this._render();
4944
+ }
4945
+ _window() {
4946
+ const attr = Number(this.getAttribute("window"));
4947
+ if (this.hasAttribute("window") && Number.isFinite(attr) && attr > 0) return attr;
4948
+ const model = resolveConfig(this).getCurrentModel();
4949
+ return model?.contextWindow && model.contextWindow > 0 ? model.contextWindow : null;
4950
+ }
4951
+ /** The chat this gauge watches: `target`, else the chat host above it — the composer's rule. */
4952
+ _ownTargetId() {
4953
+ const attr = this.getAttribute("target");
4954
+ if (attr) return attr;
4955
+ let node = this.parentElement;
4956
+ while (node) {
4957
+ const isHost = node.tagName.toLowerCase() === "aparte-chat" || node.hasAttribute("data-aparte-chat");
4958
+ if (isHost && node.id) return node.id;
4959
+ node = node.parentElement;
4960
+ }
4961
+ return void 0;
4962
+ }
4963
+ _isMine(targetId) {
4964
+ const own = this._ownTargetId();
4965
+ return !targetId || !own || targetId === own;
4966
+ }
4967
+ _render() {
4968
+ const capacity = this._window();
4969
+ if (capacity === null || this._used === null) {
4970
+ this.setAttribute("data-empty", "");
4971
+ this.removeAttribute("data-level");
4972
+ this.replaceChildren();
4973
+ this._level = null;
4974
+ return;
4975
+ }
4976
+ this.removeAttribute("data-empty");
4977
+ const used = this._used;
4978
+ const ratio = Math.min(1, used / capacity);
4979
+ const warn = fraction(this.getAttribute("warn"), 0.75);
4980
+ const danger = fraction(this.getAttribute("danger"), 0.9);
4981
+ const level = ratio >= danger ? "danger" : ratio >= warn ? "warn" : "ok";
4982
+ this.setAttribute("data-level", level);
4983
+ let bar = this.querySelector(".aparte-progress");
4984
+ let text = this.querySelector(".aparte-context__text");
4985
+ if (!bar || !text) {
4986
+ const root = el("div", "aparte-context");
4987
+ bar = el("div", "aparte-progress");
4988
+ bar.setAttribute("role", "meter");
4989
+ bar.appendChild(el("div", "aparte-progress__bar"));
4990
+ text = el("span", "aparte-context__text");
4991
+ root.append(bar, text);
4992
+ this.replaceChildren(root);
4993
+ }
4994
+ const format = new Intl.NumberFormat(void 0, { notation: "compact", maximumFractionDigits: 1 });
4995
+ const reading = `${format.format(used)} / ${format.format(capacity)}`;
4996
+ const label = resolveConfig(this).t("contextLabel") || "Context window";
4997
+ bar.style.setProperty("--aparte-progress-value", String(Math.round(ratio * 100)));
4998
+ bar.setAttribute("aria-valuemin", "0");
4999
+ bar.setAttribute("aria-valuemax", String(capacity));
5000
+ bar.setAttribute("aria-valuenow", String(used));
5001
+ bar.setAttribute("aria-label", `${label}: ${reading}`);
5002
+ text.textContent = reading;
5003
+ if (level !== this._level) {
5004
+ this._level = level;
5005
+ const targetId = this._ownTargetId();
5006
+ const detail = { level, used, window: capacity, ratio, targetId };
5007
+ this.dispatchEvent(new CustomEvent("aparte-context-threshold", { detail, bubbles: true, composed: true }));
5008
+ if (level === "danger" && this.hasAttribute("auto-compact") && !this._compactRequested) {
5009
+ this._compactRequested = true;
5010
+ window.dispatchEvent(new CustomEvent("aparte-compact", { detail: { targetId } }));
5011
+ }
5012
+ if (level !== "danger") this._compactRequested = false;
5013
+ }
5014
+ }
5015
+ }
5016
+ if (!customElements.get("aparte-context")) {
5017
+ customElements.define("aparte-context", AparteContext);
5018
+ }
4594
5019
  function registerAllComponents() {
4595
5020
  const _chat = customElements.get("aparte-chat");
4596
5021
  const _viewport = customElements.get("aparte-chat-viewport");
@@ -4607,16 +5032,15 @@ export {
4607
5032
  c as APARTE_DEFAULT_HOST_HANDLERS,
4608
5033
  d as APARTE_DEFAULT_ICON_FALLBACKS,
4609
5034
  e as APARTE_DEFAULT_LOCALE,
4610
- f as APARTE_DEFAULT_SKELETON_FALLBACKS,
4611
- g as APARTE_DEFAULT_UI_EVENTS,
4612
- h as APARTE_HOST_ATTR,
4613
- i as AparteBackendTransport,
5035
+ f as APARTE_DEFAULT_UI_EVENTS,
5036
+ g as APARTE_HOST_ATTR,
5037
+ h as AparteBackendTransport,
4614
5038
  AparteChat,
4615
5039
  AparteChatBubble,
4616
- j as AparteChatHost,
5040
+ i as AparteChatHost,
4617
5041
  AparteChatStatus,
4618
5042
  AparteChatViewport,
4619
- k as AparteClient,
5043
+ j as AparteClient,
4620
5044
  AparteComposer,
4621
5045
  AparteComposerAction,
4622
5046
  AparteComposerAddAttachment,
@@ -4625,62 +5049,64 @@ export {
4625
5049
  AparteComposerInput,
4626
5050
  AparteComposerSend,
4627
5051
  AparteComposerToolbar,
4628
- l as AparteConfig,
4629
- m as AparteConversationController,
5052
+ k as AparteConfig,
5053
+ AparteContext,
5054
+ l as AparteConversationController,
4630
5055
  AparteConversationList,
4631
- n as AparteConversationManager,
4632
- o as AparteDirectTransport,
5056
+ m as AparteConversationManager,
5057
+ n as AparteDirectTransport,
4633
5058
  AparteElicitation,
4634
5059
  AparteElicitationAbortError,
4635
- q as AparteError,
4636
- r as AparteErrorCode,
5060
+ p as AparteError,
5061
+ q as AparteErrorCode,
4637
5062
  AparteIcon,
4638
5063
  AparteMessageRepository,
4639
5064
  AparteOptgroup,
4640
5065
  AparteOption,
4641
5066
  AparteProgressSpinner,
4642
5067
  AparteSelect,
4643
- t as AparteStreamParser,
4644
- u as aparteGlobalConfig,
4645
- v as applyElementProps,
4646
- w as attachConfig,
5068
+ s as AparteStreamParser,
5069
+ AparteSuggestions,
5070
+ t as aparteGlobalConfig,
5071
+ u as applyElementProps,
5072
+ v as attachConfig,
4647
5073
  buildApprovalPanel,
4648
5074
  buildElicitationPanel,
4649
- z as collectRendererStyles,
4650
- B as contentToText,
4651
- C as contextConfig,
4652
- D as createAparteChatHandler,
4653
- E as createStreamAdapter,
5075
+ y as collectRendererStyles,
5076
+ z as contentToText,
5077
+ B as contextConfig,
5078
+ C as createAparteChatHandler,
5079
+ D as createStreamAdapter,
4654
5080
  cssEscape,
4655
- G as declineDefaultRenderers,
4656
- H as defaultSanitizer,
4657
- I as deriveArtifactKind,
4658
- J as detachConfig,
5081
+ F as declineDefaultRenderers,
5082
+ G as defaultSanitizer,
5083
+ deriveArtifactKind,
5084
+ H as detachConfig,
4659
5085
  escapeAttr,
4660
5086
  escapeHtml,
4661
- M as filesToAttachments,
4662
- N as getAllRenderers,
5087
+ K as filesToAttachments,
5088
+ L as getAllRenderers,
4663
5089
  getSegmentRenderer,
4664
5090
  installDefaultRenderersOnce,
4665
5091
  isAwaitingReply,
4666
- R as isFormatAdapter,
4667
- S as isSafeUrl,
4668
- T as isSegmentSettled,
4669
- U as parseAparteEventStream,
4670
- V as parseMarkdownToSegments,
5092
+ P as isFormatAdapter,
5093
+ Q as isSafeUrl,
5094
+ R as isSegmentSettled,
5095
+ S as parseAparteEventStream,
5096
+ T as parseMarkdownToSegments,
4671
5097
  populateBubbleFromMessage,
4672
- X as readableToAsyncIterable,
5098
+ V as readableToAsyncIterable,
4673
5099
  registerAllComponents,
4674
- Y as registerDefaultRenderers,
4675
- Z as registerSegmentRenderer,
4676
- _ as requestUserInput,
5100
+ W as registerDefaultRenderers,
5101
+ X as registerSegmentRenderer,
5102
+ Y as requestUserInput,
4677
5103
  resolveConfig,
4678
5104
  revokeAttachmentUrls,
4679
5105
  runWithConfig,
4680
- a2 as segmentDuration,
4681
- a3 as segmentTiming,
5106
+ a0 as segmentDuration,
5107
+ a1 as segmentTiming,
4682
5108
  subscribeConfigChange,
4683
- a5 as unregisterSegmentRenderer,
5109
+ a3 as unregisterSegmentRenderer,
4684
5110
  uuid
4685
5111
  };
4686
5112
  //# sourceMappingURL=index.js.map