@aparte/core 0.13.1 → 0.15.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/index.d.ts +4 -0
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/components/suggestions/aparte-suggestions.d.ts +95 -0
  21. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -0
  22. package/dist/components/suggestions/index.d.ts +3 -0
  23. package/dist/components/suggestions/index.d.ts.map +1 -0
  24. package/dist/components/viewport/aparte-chat-viewport.d.ts +35 -7
  25. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  26. package/dist/config/aparte-config.d.ts +1 -21
  27. package/dist/config/aparte-config.d.ts.map +1 -1
  28. package/dist/config/index.d.ts +0 -2
  29. package/dist/config/index.d.ts.map +1 -1
  30. package/dist/config/locale.d.ts +30 -1
  31. package/dist/config/locale.d.ts.map +1 -1
  32. package/dist/conversations/index.d.ts +1 -1
  33. package/dist/conversations/index.d.ts.map +1 -1
  34. package/dist/conversations/types.d.ts +9 -57
  35. package/dist/conversations/types.d.ts.map +1 -1
  36. package/dist/custom-elements.json +5543 -5196
  37. package/dist/generated/element-attributes.d.ts +34 -6
  38. package/dist/generated/element-attributes.d.ts.map +1 -1
  39. package/dist/{glyphs-QXfbL3n_.js → glyphs-B3BZ2TIb.js} +21 -21
  40. package/dist/glyphs-B3BZ2TIb.js.map +1 -0
  41. package/dist/host/aparte-chat-host.d.ts +1 -1
  42. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  43. package/dist/icons/extended.d.ts +87 -85
  44. package/dist/icons/extended.d.ts.map +1 -1
  45. package/dist/icons/glyphs.d.ts +53 -50
  46. package/dist/icons/glyphs.d.ts.map +1 -1
  47. package/dist/icons.js +41 -41
  48. package/dist/icons.js.map +1 -1
  49. package/dist/index.css +532 -171
  50. package/dist/index.d.ts +10 -4
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +593 -183
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.node.d.ts +6 -4
  55. package/dist/index.node.d.ts.map +1 -1
  56. package/dist/index.node.js +56 -56
  57. package/dist/index.node.js.map +1 -1
  58. package/dist/{is-awaiting-reply-CWp1ffHh.js → is-awaiting-reply-BZ_iJ3s3.js} +175 -994
  59. package/dist/is-awaiting-reply-BZ_iJ3s3.js.map +1 -0
  60. package/dist/parsers/aparte-stream-parser.d.ts +6 -17
  61. package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
  62. package/dist/primitives/select/aparte-select.d.ts +2 -2
  63. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  64. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  65. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  66. package/dist/runtime/message-repository.d.ts +7 -0
  67. package/dist/runtime/message-repository.d.ts.map +1 -1
  68. package/dist/transport/types.d.ts +8 -0
  69. package/dist/transport/types.d.ts.map +1 -1
  70. package/dist/types/chat.d.ts +32 -38
  71. package/dist/types/chat.d.ts.map +1 -1
  72. package/dist/types/errors.d.ts +16 -1
  73. package/dist/types/errors.d.ts.map +1 -1
  74. package/dist/types/event-map.d.ts +4 -0
  75. package/dist/types/event-map.d.ts.map +1 -1
  76. package/dist/types/events.d.ts +15 -0
  77. package/dist/types/events.d.ts.map +1 -1
  78. package/dist/types/imperative-api.d.ts +10 -1
  79. package/dist/types/imperative-api.d.ts.map +1 -1
  80. package/dist/types/index.d.ts +2 -2
  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-CWp1ffHh.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-CWp1ffHh.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-CWp1ffHh.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) => {
@@ -833,12 +854,12 @@ class AparteElicitation extends HTMLElement {
833
854
  * resolves `undefined` on three wrappers out of four.
834
855
  */
835
856
  _pendingTargetId() {
836
- let el = this._pending?.composer ?? null;
837
- while (el) {
838
- const tag = el.tagName?.toLowerCase();
839
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
840
- if (isHost && el.id) return el.id;
841
- 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;
842
863
  }
843
864
  return void 0;
844
865
  }
@@ -864,7 +885,7 @@ class AparteElicitation extends HTMLElement {
864
885
  const composer = node.querySelector("aparte-composer");
865
886
  if (composer && typeof composer.showPanel === "function") return composer;
866
887
  const tag = node.tagName?.toLowerCase();
867
- 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");
868
889
  if (isChatBoundary) break;
869
890
  node = node.parentElement;
870
891
  }
@@ -1002,10 +1023,10 @@ function warnMissingRenderer(type) {
1002
1023
  function unrenderedSegment(segment) {
1003
1024
  const fallback = typeof segment.fallback === "string" && segment.fallback.trim() ? segment.fallback : null;
1004
1025
  if (!fallback) warnMissingRenderer(segment.type);
1005
- const el = document.createElement("div");
1006
- el.className = fallback ? "aparte-segment aparte-segment-fallback" : "aparte-segment aparte-segment-unknown";
1007
- el.textContent = fallback ?? `[Unknown segment type: ${segment.type}]`;
1008
- 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;
1009
1030
  }
1010
1031
  function resolveSegmentRenderer(type, config) {
1011
1032
  const renderer = getSegmentRenderer(type, config);
@@ -1043,7 +1064,7 @@ class AparteChatBubble extends HTMLElement {
1043
1064
  /** The live inline editor (the composer's contenteditable primitive), present only while `_editing`. */
1044
1065
  _editInput = null;
1045
1066
  static get observedAttributes() {
1046
- return ["role", "data-role", "content", "timestamp", "message-id", "streaming", "name"];
1067
+ return ["data-role", "content", "timestamp", "message-id", "streaming", "name"];
1047
1068
  }
1048
1069
  constructor() {
1049
1070
  super();
@@ -1083,11 +1104,11 @@ class AparteChatBubble extends HTMLElement {
1083
1104
  for (const segment of this._segments) {
1084
1105
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1085
1106
  if (!renderer?.relabel) continue;
1086
- const el = this._segmentsEl.querySelector(
1107
+ const el2 = this._segmentsEl.querySelector(
1087
1108
  `:scope > [data-segment-id="${cssEscape(segment.id)}"]`
1088
1109
  );
1089
- if (!el) continue;
1090
- runWithConfig(this._cfg, () => renderer.relabel(el, segment));
1110
+ if (!el2) continue;
1111
+ runWithConfig(this._cfg, () => renderer.relabel(el2, segment));
1091
1112
  }
1092
1113
  }
1093
1114
  /**
@@ -1134,9 +1155,7 @@ class AparteChatBubble extends HTMLElement {
1134
1155
  attributeChangedCallback(name, oldValue, newValue) {
1135
1156
  if (oldValue === newValue) return;
1136
1157
  switch (name) {
1137
- case "role":
1138
1158
  case "data-role":
1139
- if (newValue === "article") return;
1140
1159
  if (newValue === "user" || newValue === "assistant") {
1141
1160
  this._role = newValue;
1142
1161
  this._updateRole();
@@ -1202,7 +1221,7 @@ class AparteChatBubble extends HTMLElement {
1202
1221
  }
1203
1222
  /** Update a specific segment */
1204
1223
  updateSegment(segmentId, updates) {
1205
- const index = this._segments.findIndex((s) => s.id === segmentId);
1224
+ const index = this._segments.findIndex((s2) => s2.id === segmentId);
1206
1225
  if (index !== -1) {
1207
1226
  const updated = mergeSegmentUpdate(this._segments[index], updates);
1208
1227
  this._segments[index] = updated;
@@ -1211,7 +1230,7 @@ class AparteChatBubble extends HTMLElement {
1211
1230
  }
1212
1231
  /** Append content to a segment */
1213
1232
  appendToSegment(segmentId, content) {
1214
- const segment = this._segments.find((s) => s.id === segmentId);
1233
+ const segment = this._segments.find((s2) => s2.id === segmentId);
1215
1234
  if (segment && "content" in segment) {
1216
1235
  segment.content += content;
1217
1236
  this._applySegmentUpdate(segmentId, segment, { content: segment.content });
@@ -1237,18 +1256,19 @@ class AparteChatBubble extends HTMLElement {
1237
1256
  * the human-in-the-loop control.
1238
1257
  */
1239
1258
  removeSegment(segmentId) {
1240
- const index = this._segments.findIndex((s) => s.id === segmentId);
1259
+ const index = this._segments.findIndex((s2) => s2.id === segmentId);
1241
1260
  if (index !== -1) {
1242
1261
  this._segments.splice(index, 1);
1243
1262
  }
1244
- const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1245
- el?.remove();
1263
+ const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1264
+ el2?.remove();
1246
1265
  this._updateWaiting();
1247
1266
  }
1248
1267
  /** Set attachments (chips shown above message content, user role only) */
1249
1268
  setAttachments(attachments) {
1250
1269
  this._attachments = attachments;
1251
1270
  this._updateAttachments();
1271
+ this._updateWaiting();
1252
1272
  }
1253
1273
  /**
1254
1274
  * Set token usage + timing for this message (assistant only).
@@ -1300,6 +1320,7 @@ class AparteChatBubble extends HTMLElement {
1300
1320
  if ("attachments" in updates) {
1301
1321
  this._attachments = updates.attachments ?? [];
1302
1322
  this._updateAttachments();
1323
+ this._updateWaiting();
1303
1324
  }
1304
1325
  }
1305
1326
  // ─────────────────────────────────────────────────────────────────────────
@@ -1312,10 +1333,10 @@ class AparteChatBubble extends HTMLElement {
1312
1333
  }
1313
1334
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1314
1335
  if (renderer) {
1315
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1316
- if (el) {
1317
- this._segmentsEl.appendChild(el);
1318
- 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));
1319
1340
  }
1320
1341
  } else {
1321
1342
  this._segmentsEl.appendChild(unrenderedSegment(segment));
@@ -1324,27 +1345,27 @@ class AparteChatBubble extends HTMLElement {
1324
1345
  this._reflectError();
1325
1346
  }
1326
1347
  _applySegmentUpdate(segmentId, segment, updates) {
1327
- const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1328
- if (!el) {
1348
+ const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1349
+ if (!el2) {
1329
1350
  this._renderSegments();
1330
1351
  return;
1331
1352
  }
1332
1353
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1333
1354
  if (!renderer) return;
1334
1355
  if (renderer.update) {
1335
- runWithConfig(this._cfg, () => renderer.update(el, segment));
1356
+ runWithConfig(this._cfg, () => renderer.update(el2, segment));
1336
1357
  } else {
1337
1358
  const newEl = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1338
1359
  if (newEl) {
1339
- el.replaceWith(newEl);
1360
+ el2.replaceWith(newEl);
1340
1361
  runWithConfig(this._cfg, () => renderer.setup?.(newEl, segment));
1341
1362
  }
1342
1363
  }
1343
1364
  if ("collapsed" in updates) {
1344
1365
  if (updates.collapsed) {
1345
- el.removeAttribute("open");
1366
+ el2.removeAttribute("open");
1346
1367
  } else {
1347
- el.setAttribute("open", "");
1368
+ el2.setAttribute("open", "");
1348
1369
  }
1349
1370
  }
1350
1371
  }
@@ -1360,8 +1381,7 @@ class AparteChatBubble extends HTMLElement {
1360
1381
  }
1361
1382
  _render() {
1362
1383
  const dataRole = this.getAttribute("data-role");
1363
- const legacyRole = this.getAttribute("role");
1364
- const role = dataRole && dataRole !== "article" ? dataRole : legacyRole && legacyRole !== "article" ? legacyRole : "assistant";
1384
+ const role = dataRole === "user" || dataRole === "assistant" ? dataRole : "assistant";
1365
1385
  this._role = role;
1366
1386
  if (this.getAttribute("role") !== "article") {
1367
1387
  this.setAttribute("role", "article");
@@ -1420,6 +1440,7 @@ class AparteChatBubble extends HTMLElement {
1420
1440
  this._actionBarEl = this.querySelector(".aparte-action-bar");
1421
1441
  this._branchPickerEl = this.querySelector(".aparte-branch-picker");
1422
1442
  this._footerEl = this.querySelector(".aparte-footer");
1443
+ this._transcriptBusy = !!this.closest("aparte-chat-viewport")?.hasAttribute("data-busy");
1423
1444
  this._updateActionBar();
1424
1445
  this._renderAvatar();
1425
1446
  if (this._streaming) this._updateStreaming(true);
@@ -1440,12 +1461,16 @@ class AparteChatBubble extends HTMLElement {
1440
1461
  const waiting = this._streaming && this._role !== "user" && empty;
1441
1462
  const box = this.querySelector(".aparte-message-content");
1442
1463
  if (box) box.hidden = empty && !waiting;
1443
- const el = this.querySelector(".aparte-waiting");
1444
- if (!el) return;
1445
- 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;
1446
1471
  if (!waiting) return;
1447
1472
  const label = this._cfg.getLocale().typing;
1448
- const sr = el.querySelector(".aparte-sr-only");
1473
+ const sr = el2.querySelector(".aparte-sr-only");
1449
1474
  if (sr && sr.textContent !== label) sr.textContent = label;
1450
1475
  }
1451
1476
  /**
@@ -1547,10 +1572,10 @@ class AparteChatBubble extends HTMLElement {
1547
1572
  for (const segment of this._segments) {
1548
1573
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1549
1574
  if (renderer) {
1550
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
1551
- if (el) {
1552
- this._segmentsEl.appendChild(el);
1553
- 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));
1554
1579
  }
1555
1580
  } else {
1556
1581
  this._segmentsEl.appendChild(unrenderedSegment(segment));
@@ -1571,7 +1596,7 @@ class AparteChatBubble extends HTMLElement {
1571
1596
  _reflectError() {
1572
1597
  const message = this.querySelector(".aparte-message");
1573
1598
  if (!message) return;
1574
- const hasError = this._segments.some((s) => s.type === "error");
1599
+ const hasError = this._segments.some((s2) => s2.type === "error");
1575
1600
  if (hasError) message.setAttribute("data-error", "");
1576
1601
  else message.removeAttribute("data-error");
1577
1602
  }
@@ -1606,18 +1631,18 @@ class AparteChatBubble extends HTMLElement {
1606
1631
  const customAttachment = this._cfg.getAttachmentRenderer?.();
1607
1632
  if (customAttachment) {
1608
1633
  this._attachmentsEl.replaceChildren();
1609
- for (const a4 of this._attachments) {
1610
- const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a4)));
1611
- 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);
1612
1637
  }
1613
1638
  return;
1614
1639
  }
1615
- this._attachmentsEl.innerHTML = this._attachments.map((a4) => {
1616
- const name = escapeHtml(a4.name);
1617
- if (a4.type.startsWith("image/")) {
1618
- 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>`;
1619
1644
  }
1620
- 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>`;
1621
1646
  }).join("");
1622
1647
  if (!this._cfg.getHostHandlers().attachmentPreview) return;
1623
1648
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
@@ -1682,6 +1707,25 @@ class AparteChatBubble extends HTMLElement {
1682
1707
  detail
1683
1708
  }));
1684
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
+ }
1685
1729
  _updateBranchPicker() {
1686
1730
  if (!this._branchPickerEl) return;
1687
1731
  if (this._siblingCount <= 1 || this._role !== "assistant") {
@@ -1709,8 +1753,8 @@ class AparteChatBubble extends HTMLElement {
1709
1753
  }
1710
1754
  const prevBtn = this._branchPickerEl.querySelector(".aparte-branch-prev");
1711
1755
  const nextBtn = this._branchPickerEl.querySelector(".aparte-branch-next");
1712
- if (prevBtn) prevBtn.disabled = this._siblingIndex === 0;
1713
- 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;
1714
1758
  }
1715
1759
  // ─────────────────────────────────────────────────────────────────────────
1716
1760
  // Action Bar
@@ -1727,14 +1771,14 @@ class AparteChatBubble extends HTMLElement {
1727
1771
  const buttons = [];
1728
1772
  if (this._role === "user") {
1729
1773
  if (config.user) {
1730
- 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));
1731
1775
  } else {
1732
1776
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
1733
1777
  if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
1734
1778
  }
1735
1779
  } else if (this._role === "assistant") {
1736
1780
  if (config.assistant) {
1737
- 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));
1738
1782
  } else {
1739
1783
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
1740
1784
  if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
@@ -1747,6 +1791,7 @@ class AparteChatBubble extends HTMLElement {
1747
1791
  }
1748
1792
  this._actionBarEl.innerHTML = buttons.join("");
1749
1793
  this._appendCustomActions(icons);
1794
+ this._applyTranscriptBusy();
1750
1795
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1751
1796
  btn.addEventListener("click", (e2) => this._handleActionClick(e2));
1752
1797
  });
@@ -1765,20 +1810,21 @@ class AparteChatBubble extends HTMLElement {
1765
1810
  const barEmpty = !this._actionBarEl || this._actionBarEl.hidden;
1766
1811
  const pickerHidden = !this._branchPickerEl || this._branchPickerEl.hidden;
1767
1812
  this._footerEl.hidden = barEmpty && pickerHidden;
1813
+ this.querySelector(".aparte-message")?.toggleAttribute("data-branches", !pickerHidden);
1768
1814
  }
1769
1815
  /** Append the registered custom action buttons for this bubble's role. */
1770
1816
  _appendCustomActions(icons) {
1771
1817
  if (!this._actionBarEl) return;
1772
- for (const a4 of this._cfg.getActions("bubble")) {
1773
- const roles = a4.bubble?.roles ?? ["user", "assistant"];
1818
+ for (const a2 of this._cfg.getActions("bubble")) {
1819
+ const roles = a2.bubble?.roles ?? ["user", "assistant"];
1774
1820
  if (!roles.includes(this._role)) continue;
1775
1821
  const btn = document.createElement("button");
1776
1822
  btn.className = "aparte-btn aparte-btn--icon aparte-action-btn aparte-action-custom";
1777
- btn.dataset["action"] = `custom:${a4.id}`;
1778
- btn.setAttribute("aria-label", a4.label);
1779
- btn.setAttribute("title", a4.label);
1780
- const fromProvider = icons[a4.icon];
1781
- 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 ?? "";
1782
1828
  this._actionBarEl.appendChild(btn);
1783
1829
  }
1784
1830
  }
@@ -1849,7 +1895,7 @@ class AparteChatBubble extends HTMLElement {
1849
1895
  }
1850
1896
  switch (action) {
1851
1897
  case "copy": {
1852
- 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");
1853
1899
  const icons = this._cfg.getIconProvider();
1854
1900
  const locale = this._cfg.getLocale();
1855
1901
  navigator.clipboard.writeText(text).then(() => {
@@ -1983,12 +2029,12 @@ class AparteChatBubble extends HTMLElement {
1983
2029
  }
1984
2030
  }
1985
2031
  _resolveTargetId() {
1986
- let el = this.parentElement;
1987
- while (el) {
1988
- const tag = el.tagName?.toLowerCase();
1989
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
1990
- if (isHost && el.id) return el.id;
1991
- 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;
1992
2038
  }
1993
2039
  return void 0;
1994
2040
  }
@@ -2160,6 +2206,17 @@ class AparteChatViewport extends HTMLElement {
2160
2206
  _isAutoScrollEnabled = true;
2161
2207
  /** The last scroll position we saw, so growth can be told from a gesture. */
2162
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", " "]);
2163
2220
  _scrollThreshold = 50;
2164
2221
  /** When true, the next _autoScroll() call uses smooth instead of instant, then resets. */
2165
2222
  _smoothScrollOnce = false;
@@ -2170,8 +2227,6 @@ class AparteChatViewport extends HTMLElement {
2170
2227
  * snapshot stay intact; only the oldest rendered bubbles are dropped from view.
2171
2228
  */
2172
2229
  _maxRenderedBubbles = 1e3;
2173
- /** One-time guard for the deprecated `maxMessages` warning. */
2174
- _warnedMaxMessagesDeprecation = false;
2175
2230
  _resizeObserver = null;
2176
2231
  _mutationObserver = null;
2177
2232
  _boundResetHandler = null;
@@ -2182,7 +2237,7 @@ class AparteChatViewport extends HTMLElement {
2182
2237
  */
2183
2238
  _frameworkManagedDOM = false;
2184
2239
  static get observedAttributes() {
2185
- return ["scroll-threshold", "max-rendered-bubbles", "max-messages"];
2240
+ return ["scroll-threshold", "max-rendered-bubbles"];
2186
2241
  }
2187
2242
  constructor() {
2188
2243
  super();
@@ -2206,6 +2261,7 @@ class AparteChatViewport extends HTMLElement {
2206
2261
  const detail = e2.detail;
2207
2262
  if (detail?.config && detail.config !== resolveConfig(this)) return;
2208
2263
  this._applyDirection();
2264
+ this._scrollBtn?.setAttribute("aria-label", resolveConfig(this).t("scrollToBottom"));
2209
2265
  };
2210
2266
  /** Mirror `locale.direction` onto the scroll container. */
2211
2267
  _applyDirection() {
@@ -2233,11 +2289,6 @@ class AparteChatViewport extends HTMLElement {
2233
2289
  this._maxRenderedBubbles = parseInt(newValue || "1000", 10);
2234
2290
  this._pruneRenderedBubbles();
2235
2291
  break;
2236
- case "max-messages":
2237
- this._warnMaxMessagesDeprecated();
2238
- this._maxRenderedBubbles = parseInt(newValue || "1000", 10);
2239
- this._pruneRenderedBubbles();
2240
- break;
2241
2292
  }
2242
2293
  }
2243
2294
  /**
@@ -2251,11 +2302,6 @@ class AparteChatViewport extends HTMLElement {
2251
2302
  this._maxRenderedBubbles = config.maxRenderedBubbles;
2252
2303
  this._pruneRenderedBubbles();
2253
2304
  }
2254
- if (config.maxMessages !== void 0) {
2255
- this._warnMaxMessagesDeprecated();
2256
- this._maxRenderedBubbles = config.maxMessages;
2257
- this._pruneRenderedBubbles();
2258
- }
2259
2305
  if (config.layoutTransitionMs !== void 0) {
2260
2306
  this._layoutTransitionMs = config.layoutTransitionMs;
2261
2307
  }
@@ -2289,7 +2335,7 @@ class AparteChatViewport extends HTMLElement {
2289
2335
  if (!message.segments) {
2290
2336
  message.segments = [];
2291
2337
  }
2292
- const index = message.segments.findIndex((s) => s.id === segmentId);
2338
+ const index = message.segments.findIndex((s2) => s2.id === segmentId);
2293
2339
  const segment = index === -1 ? void 0 : message.segments[index];
2294
2340
  if (segment && "content" in segment) {
2295
2341
  message.segments[index] = {
@@ -2331,6 +2377,24 @@ class AparteChatViewport extends HTMLElement {
2331
2377
  }
2332
2378
  return null;
2333
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
+ }
2334
2398
  addSegment(messageIdOrSegment, maybeSegment) {
2335
2399
  const messageId = typeof messageIdOrSegment === "string" ? messageIdOrSegment : this._activeMessageId();
2336
2400
  const segment = typeof messageIdOrSegment === "string" ? maybeSegment : messageIdOrSegment;
@@ -2351,15 +2415,15 @@ class AparteChatViewport extends HTMLElement {
2351
2415
  this._notifyBubble(messageId, "addSegment", stamped);
2352
2416
  this._autoScroll();
2353
2417
  }
2354
- updateSegment(a4, b2, c2) {
2418
+ updateSegment(a2, b2, c2) {
2355
2419
  const clientForm = c2 === void 0 && typeof b2 === "object";
2356
- const messageId = clientForm ? this._activeMessageId() : a4;
2357
- const segmentId = clientForm ? a4 : b2;
2420
+ const messageId = clientForm ? this._activeMessageId() : a2;
2421
+ const segmentId = clientForm ? a2 : b2;
2358
2422
  const updates = clientForm ? b2 : c2;
2359
2423
  if (!messageId) return;
2360
2424
  const message = this._repo.getMessageById(messageId);
2361
2425
  if (!message?.segments) return;
2362
- const segmentIndex = message.segments.findIndex((s) => s.id === segmentId);
2426
+ const segmentIndex = message.segments.findIndex((s2) => s2.id === segmentId);
2363
2427
  if (segmentIndex !== -1) {
2364
2428
  const current = message.segments[segmentIndex];
2365
2429
  const stamped = stampSegmentOnUpdate(current, updates);
@@ -2367,14 +2431,14 @@ class AparteChatViewport extends HTMLElement {
2367
2431
  this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
2368
2432
  }
2369
2433
  }
2370
- removeSegment(a4, b2) {
2434
+ removeSegment(a2, b2) {
2371
2435
  const clientForm = b2 === void 0;
2372
- const messageId = clientForm ? this._activeMessageId() : a4;
2373
- const segmentId = clientForm ? a4 : b2;
2436
+ const messageId = clientForm ? this._activeMessageId() : a2;
2437
+ const segmentId = clientForm ? a2 : b2;
2374
2438
  if (!messageId || !segmentId) return;
2375
2439
  const message = this._repo.getMessageById(messageId);
2376
2440
  if (message?.segments) {
2377
- const idx = message.segments.findIndex((s) => s.id === segmentId);
2441
+ const idx = message.segments.findIndex((s2) => s2.id === segmentId);
2378
2442
  if (idx !== -1) {
2379
2443
  message.segments.splice(idx, 1);
2380
2444
  renumberSegments(message.segments);
@@ -2420,6 +2484,7 @@ class AparteChatViewport extends HTMLElement {
2420
2484
  message.status = "completed";
2421
2485
  this._settleSegments(messageId, message);
2422
2486
  this._notifyBubble(messageId, "complete", { status: "completed" });
2487
+ this._syncBusy();
2423
2488
  this._recalculateSpacer();
2424
2489
  }
2425
2490
  }
@@ -2451,6 +2516,7 @@ class AparteChatViewport extends HTMLElement {
2451
2516
  if (isTerminalStatus(updates.status)) this._settleSegments(messageId, message);
2452
2517
  }
2453
2518
  this._notifyBubble(messageId, "update", updates);
2519
+ if (updates.status) this._syncBusy();
2454
2520
  this._autoScroll();
2455
2521
  }
2456
2522
  /**
@@ -2465,6 +2531,7 @@ class AparteChatViewport extends HTMLElement {
2465
2531
  addMessage(message) {
2466
2532
  this._repo.addOrUpdateMessage(this._repo.headId, adoptMessageSegments({ ...message }));
2467
2533
  this._pruneRenderedBubbles();
2534
+ this._syncBusy();
2468
2535
  this._autoScroll();
2469
2536
  }
2470
2537
  /**
@@ -2487,13 +2554,14 @@ class AparteChatViewport extends HTMLElement {
2487
2554
  },
2488
2555
  []
2489
2556
  ) } : { ...message };
2557
+ if (!options?.historical && isAwaitingReply(message)) stored.isStreaming = true;
2490
2558
  this._repo.addOrUpdateMessage(this._repo.headId, stored);
2491
2559
  if (!this._frameworkManagedDOM) {
2492
2560
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2493
2561
  if (wrapper) {
2494
2562
  const bubble = document.createElement("aparte-chat-bubble");
2495
2563
  bubble.setAttribute("message-id", message.id);
2496
- bubble.setAttribute("role", message.role);
2564
+ bubble.setAttribute("data-role", message.role);
2497
2565
  if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
2498
2566
  if (message.content) bubble.setAttribute("content", message.content);
2499
2567
  if (isAwaitingReply(message)) {
@@ -2508,10 +2576,13 @@ class AparteChatViewport extends HTMLElement {
2508
2576
  }
2509
2577
  }
2510
2578
  this._pruneRenderedBubbles();
2579
+ this._syncBusy();
2511
2580
  this._recalculateSpacer();
2512
2581
  if (message.role === "user") {
2513
2582
  this._isAutoScrollEnabled = true;
2514
- requestAnimationFrame(() => this._smoothScrollToBottom());
2583
+ requestAnimationFrame(() => {
2584
+ if (this._isAutoScrollEnabled) this._smoothScrollToBottom();
2585
+ });
2515
2586
  } else {
2516
2587
  this._autoScroll();
2517
2588
  }
@@ -2570,7 +2641,9 @@ class AparteChatViewport extends HTMLElement {
2570
2641
  const meta = this._repo.getMessage(existingId);
2571
2642
  if (!meta) return null;
2572
2643
  const parentId = meta.message.role === "user" ? existingId : meta.parentId;
2573
- this._repo.addOrUpdateMessage(parentId, { ...newMessage });
2644
+ const stored = { ...newMessage };
2645
+ if (isAwaitingReply(newMessage)) stored.isStreaming = true;
2646
+ this._repo.addOrUpdateMessage(parentId, stored);
2574
2647
  this._repo.switchToBranch(newMessage.id);
2575
2648
  this._reRenderActivePath();
2576
2649
  return newMessage.id;
@@ -2580,6 +2653,7 @@ class AparteChatViewport extends HTMLElement {
2580
2653
  * Triggers a full re-render of the active path.
2581
2654
  */
2582
2655
  navigateBranch(messageId, direction) {
2656
+ if (this._streamingMessageId() !== null) return;
2583
2657
  const siblings = this._repo.getBranches(messageId);
2584
2658
  const currentIdx = siblings.indexOf(messageId);
2585
2659
  if (currentIdx === -1) return;
@@ -2684,6 +2758,7 @@ class AparteChatViewport extends HTMLElement {
2684
2758
  }
2685
2759
  }
2686
2760
  this._repo.clear();
2761
+ this._syncBusy();
2687
2762
  if (!this._frameworkManagedDOM) {
2688
2763
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2689
2764
  if (wrapper) {
@@ -2833,7 +2908,7 @@ class AparteChatViewport extends HTMLElement {
2833
2908
  const sibInfo = siblingsInfo[i2];
2834
2909
  const bubble = document.createElement("aparte-chat-bubble");
2835
2910
  bubble.setAttribute("message-id", message.id);
2836
- bubble.setAttribute("role", message.role);
2911
+ bubble.setAttribute("data-role", message.role);
2837
2912
  if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
2838
2913
  if (isAwaitingReply(message)) {
2839
2914
  bubble.setAttribute("streaming", "");
@@ -2842,6 +2917,7 @@ class AparteChatViewport extends HTMLElement {
2842
2917
  populateBubbleFromMessage(bubble, message, sibInfo);
2843
2918
  }
2844
2919
  this._dispatchPathChanged(activeMessages, siblingsInfo);
2920
+ this._syncBusy();
2845
2921
  this._recalculateSpacer();
2846
2922
  }
2847
2923
  _dispatchPathChanged(messages, siblings) {
@@ -2856,9 +2932,13 @@ class AparteChatViewport extends HTMLElement {
2856
2932
  if (this._isAutoScrollEnabled) {
2857
2933
  if (this._smoothScrollOnce) {
2858
2934
  this._smoothScrollOnce = false;
2859
- requestAnimationFrame(() => this._smoothScrollToBottom());
2935
+ requestAnimationFrame(() => {
2936
+ if (this._isAutoScrollEnabled) this._smoothScrollToBottom();
2937
+ });
2860
2938
  } else {
2861
- requestAnimationFrame(() => this._scrollToBottom());
2939
+ requestAnimationFrame(() => {
2940
+ if (this._isAutoScrollEnabled) this._scrollToBottom();
2941
+ });
2862
2942
  }
2863
2943
  }
2864
2944
  this._pruneRenderedBubbles();
@@ -2903,7 +2983,7 @@ class AparteChatViewport extends HTMLElement {
2903
2983
  this._scrollBtn = document.createElement("button");
2904
2984
  this._scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
2905
2985
  this._scrollBtn.setAttribute("type", "button");
2906
- this._scrollBtn.setAttribute("aria-label", "Scroll to bottom");
2986
+ this._scrollBtn.setAttribute("aria-label", resolveConfig(this).t("scrollToBottom"));
2907
2987
  const scrollIcon = resolveConfig(this).getIcon("scrollDown");
2908
2988
  this._scrollBtn.innerHTML = scrollIcon;
2909
2989
  this.appendChild(this._scrollBtn);
@@ -2936,7 +3016,7 @@ class AparteChatViewport extends HTMLElement {
2936
3016
  const scrollBtn = document.createElement("button");
2937
3017
  scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
2938
3018
  scrollBtn.setAttribute("type", "button");
2939
- scrollBtn.setAttribute("aria-label", "Scroll to bottom");
3019
+ scrollBtn.setAttribute("aria-label", resolveConfig(this).t("scrollToBottom"));
2940
3020
  const scrollIcon = resolveConfig(this).getIcon("scrollDown");
2941
3021
  scrollBtn.innerHTML = scrollIcon;
2942
3022
  this.appendChild(scrollBtn);
@@ -2981,6 +3061,27 @@ class AparteChatViewport extends HTMLElement {
2981
3061
  this._smoothScrollToBottom();
2982
3062
  this._updateScrollButton();
2983
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
+ };
2984
3085
  _onBranchNavigate = (e2) => {
2985
3086
  const evt = e2;
2986
3087
  evt.stopPropagation();
@@ -2988,6 +3089,9 @@ class AparteChatViewport extends HTMLElement {
2988
3089
  };
2989
3090
  _setupEventListeners() {
2990
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
+ }
2991
3095
  this._scrollBtn?.addEventListener("click", this._onScrollBtnClick);
2992
3096
  this.addEventListener("aparte-branch-navigate", this._onBranchNavigate);
2993
3097
  }
@@ -3010,7 +3114,9 @@ class AparteChatViewport extends HTMLElement {
3010
3114
  this._mutationObserver = new MutationObserver(() => {
3011
3115
  if (this._frameworkManagedDOM) this._keepScrollButtonLast();
3012
3116
  if (this._isAutoScrollEnabled) {
3013
- requestAnimationFrame(() => this._scrollToBottom());
3117
+ requestAnimationFrame(() => {
3118
+ if (this._isAutoScrollEnabled) this._scrollToBottom();
3119
+ });
3014
3120
  }
3015
3121
  this._scheduleSpacerUpdate();
3016
3122
  });
@@ -3031,8 +3137,14 @@ class AparteChatViewport extends HTMLElement {
3031
3137
  _handleScroll() {
3032
3138
  if (!this._container) return;
3033
3139
  const top = this._container.scrollTop;
3034
- const readerWentUp = top < this._lastScrollTop - 1;
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;
3035
3146
  this._lastScrollTop = top;
3147
+ this._lastScrollHeight = height;
3036
3148
  if (this._isAtBottom()) {
3037
3149
  this._isAutoScrollEnabled = true;
3038
3150
  } else if (readerWentUp) {
@@ -3042,6 +3154,7 @@ class AparteChatViewport extends HTMLElement {
3042
3154
  }
3043
3155
  _scrollToBottom() {
3044
3156
  if (!this._container) return;
3157
+ this._ownScrollAt = performance.now();
3045
3158
  this._container.scrollTop = this._container.scrollHeight;
3046
3159
  this._settleAtBottom(4);
3047
3160
  }
@@ -3075,12 +3188,14 @@ class AparteChatViewport extends HTMLElement {
3075
3188
  if (!this._container || !this._isAutoScrollEnabled) return;
3076
3189
  const max = this._container.scrollHeight - this._container.clientHeight;
3077
3190
  if (max - this._container.scrollTop <= 1) return;
3191
+ this._ownScrollAt = performance.now();
3078
3192
  this._container.scrollTop = max;
3079
3193
  this._settleAtBottom(framesLeft - 1);
3080
3194
  });
3081
3195
  }
3082
3196
  _smoothScrollToBottom() {
3083
3197
  if (!this._container) return;
3198
+ this._ownScrollAt = performance.now();
3084
3199
  if (typeof this._container.scrollTo === "function" && !this._prefersReducedMotion()) {
3085
3200
  this._container.scrollTo({ top: this._container.scrollHeight, behavior: "smooth" });
3086
3201
  } else {
@@ -3099,7 +3214,7 @@ class AparteChatViewport extends HTMLElement {
3099
3214
  * fold"). Mirroring the flag made the button lie whenever the two diverged —
3100
3215
  * `navigateBranch` deliberately disarms auto-follow, so swapping a branch while
3101
3216
  * already at the bottom of a scrollable transcript left the button offering to
3102
- * 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
3103
3218
  * post-mutation frame and after a path swap, so it converges to the truth
3104
3219
  * whatever a framework's render timing does in between.
3105
3220
  */
@@ -3126,7 +3241,7 @@ class AparteChatViewport extends HTMLElement {
3126
3241
  this._setSpacerHeight(0);
3127
3242
  return;
3128
3243
  }
3129
- const lastUserBubble = [...allBubbles].reverse().find((b2) => b2.getAttribute("role") === "user");
3244
+ const lastUserBubble = [...allBubbles].reverse().find((b2) => b2.getAttribute("data-role") === "user");
3130
3245
  if (!lastUserBubble) {
3131
3246
  this._setSpacerHeight(0);
3132
3247
  return;
@@ -3185,15 +3300,11 @@ class AparteChatViewport extends HTMLElement {
3185
3300
  bubbles[i2]?.remove();
3186
3301
  }
3187
3302
  }
3188
- _warnMaxMessagesDeprecated() {
3189
- if (this._warnedMaxMessagesDeprecation) return;
3190
- this._warnedMaxMessagesDeprecation = true;
3191
- console.warn(
3192
- "[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."
3193
- );
3194
- }
3195
3303
  _cleanup() {
3196
3304
  this._container?.removeEventListener("scroll", this._handleScroll);
3305
+ for (const type of ["wheel", "touchmove", "pointerdown", "keydown"]) {
3306
+ this._container?.removeEventListener(type, this._noteReaderInput);
3307
+ }
3197
3308
  this._scrollBtn?.removeEventListener("click", this._onScrollBtnClick);
3198
3309
  this.removeEventListener("aparte-branch-navigate", this._onBranchNavigate);
3199
3310
  this._resizeObserver?.disconnect();
@@ -3298,6 +3409,7 @@ class AparteComposer extends HTMLElement {
3298
3409
  window.addEventListener("aparte-message-error", this._onMessageDone);
3299
3410
  window.addEventListener("aparte-message-aborted", this._onMessageDone);
3300
3411
  window.addEventListener("aparte-config-change", this._onConfigChangeEvent);
3412
+ queueMicrotask(() => this._warnIfUnattached());
3301
3413
  this._evaluateModelGate();
3302
3414
  this._applyDirection();
3303
3415
  }
@@ -3313,6 +3425,11 @@ class AparteComposer extends HTMLElement {
3313
3425
  if (name === "disabled") {
3314
3426
  this._emit("disabled-change", { disabled: value !== null });
3315
3427
  }
3428
+ if (name === "placeholder") {
3429
+ for (const input of this.querySelectorAll("aparte-composer-input")) {
3430
+ input.syncPlaceholder?.();
3431
+ }
3432
+ }
3316
3433
  }
3317
3434
  // ── Public API ─────────────────────────────────────────────────────────
3318
3435
  get value() {
@@ -3430,10 +3547,13 @@ class AparteComposer extends HTMLElement {
3430
3547
  */
3431
3548
  showPanel(panel, options) {
3432
3549
  this._evictPanel();
3550
+ const host = this.querySelector("[data-aparte-panel-host]");
3433
3551
  const inputEl = this.querySelector("aparte-composer-input");
3434
3552
  this.setAttribute("data-panel-active", "");
3435
3553
  panel.dataset["apartePanel"] = "true";
3436
- if (inputEl) {
3554
+ if (host) {
3555
+ host.appendChild(panel);
3556
+ } else if (inputEl) {
3437
3557
  inputEl.insertAdjacentElement("afterend", panel);
3438
3558
  } else {
3439
3559
  this.appendChild(panel);
@@ -3643,15 +3763,26 @@ class AparteComposer extends HTMLElement {
3643
3763
  * for the reason written there — Angular's wrapper root IS `<aparte-chat>`, the
3644
3764
  * other three render a `[data-aparte-chat]` div.
3645
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
+ }
3646
3777
  _ownTargetId() {
3647
3778
  const attr = this.targetId;
3648
3779
  if (attr) return attr;
3649
- let el = this.parentElement;
3650
- while (el) {
3651
- const tag = el.tagName?.toLowerCase();
3652
- const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
3653
- if (isHost && el.id) return el.id;
3654
- 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;
3655
3786
  }
3656
3787
  return void 0;
3657
3788
  }
@@ -3739,6 +3870,14 @@ class AparteComposerInput extends HTMLElement {
3739
3870
  this._adjustHeight();
3740
3871
  this._getRoot()?.setValue("");
3741
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
+ }
3742
3881
  /** Focus the inner contenteditable rather than the host element. */
3743
3882
  focus() {
3744
3883
  this._editor?.focus();
@@ -3776,7 +3915,7 @@ class AparteComposerInput extends HTMLElement {
3776
3915
  role="textbox"
3777
3916
  aria-multiline="true"
3778
3917
  aria-label="${placeholder}"
3779
- tabindex="0"
3918
+ tabindex="${escapeAttr(String(disabled ? -1 : 0))}"
3780
3919
  aria-disabled="${disabled}"
3781
3920
  data-placeholder="${placeholder}"
3782
3921
  ></div>`;
@@ -3801,11 +3940,6 @@ class AparteComposerInput extends HTMLElement {
3801
3940
  else this.setValue(value);
3802
3941
  })
3803
3942
  );
3804
- this._unsubscribes.push(
3805
- root._on("streaming-change", ({ streaming }) => {
3806
- this._updateDisabled(streaming || root.disabled);
3807
- })
3808
- );
3809
3943
  }
3810
3944
  _handleInput() {
3811
3945
  this._adjustHeight();
@@ -3825,6 +3959,7 @@ class AparteComposerInput extends HTMLElement {
3825
3959
  e2.preventDefault();
3826
3960
  const root = this._getRoot();
3827
3961
  if (root) {
3962
+ if (root.streaming) return;
3828
3963
  root.submit();
3829
3964
  } else {
3830
3965
  this.dispatchEvent(new CustomEvent("aparte-composer-submit", { bubbles: true }));
@@ -3882,9 +4017,9 @@ class AparteComposerInput extends HTMLElement {
3882
4017
  }
3883
4018
  _updatePlaceholder() {
3884
4019
  if (this._editor) {
3885
- const p = this._getPlaceholder();
3886
- this._editor.setAttribute("data-placeholder", p);
3887
- 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);
3888
4023
  }
3889
4024
  }
3890
4025
  _updatePlaceholderVisibility() {
@@ -3893,6 +4028,8 @@ class AparteComposerInput extends HTMLElement {
3893
4028
  if (!this._editor) return;
3894
4029
  this._editor.setAttribute("contenteditable", String(!disabled));
3895
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();
3896
4033
  }
3897
4034
  /** Escape a value before it lands in a double-quoted HTML attribute. */
3898
4035
  }
@@ -4247,9 +4384,8 @@ class AparteComposerAddAttachment extends HTMLElement {
4247
4384
  * Re-read the label and the icon on the button that already exists.
4248
4385
  *
4249
4386
  * Deliberately not a re-render: `_render()` returns early once the button is
4250
- * there, and its `disabled` computation ignores `root.streaming` while the
4251
- * streaming listener sets `disabled` directly so rebuilding would silently
4252
- * 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.
4253
4389
  * The native file input and the drag listeners live outside this element
4254
4390
  * (on `document.body` and on the composer root), so they are untouched either way.
4255
4391
  */
@@ -4286,7 +4422,7 @@ class AparteComposerAddAttachment extends HTMLElement {
4286
4422
  );
4287
4423
  this._unsubscribes.push(
4288
4424
  root._on("streaming-change", ({ streaming }) => {
4289
- if (this._button) this._button.disabled = streaming || root.disabled;
4425
+ if (this._button) this._button.disabled = root.disabled;
4290
4426
  })
4291
4427
  );
4292
4428
  }
@@ -4595,10 +4731,10 @@ class AparteConversationList extends HTMLElement {
4595
4731
  /** Update active class without full re-render (perf optimisation). */
4596
4732
  _updateActiveState() {
4597
4733
  const items = this.querySelectorAll("[data-conv-id]");
4598
- items.forEach((el) => {
4599
- const isActive = el.dataset["convId"] === this._activeId;
4600
- el.classList.toggle("aparte-conv-item--active", isActive);
4601
- 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");
4602
4738
  });
4603
4739
  }
4604
4740
  // ─── Helpers ──────────────────────────────────────────────────────────
@@ -4607,6 +4743,279 @@ class AparteConversationList extends HTMLElement {
4607
4743
  }
4608
4744
  }
4609
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
+ }
4610
5019
  function registerAllComponents() {
4611
5020
  const _chat = customElements.get("aparte-chat");
4612
5021
  const _viewport = customElements.get("aparte-chat-viewport");
@@ -4623,16 +5032,15 @@ export {
4623
5032
  c as APARTE_DEFAULT_HOST_HANDLERS,
4624
5033
  d as APARTE_DEFAULT_ICON_FALLBACKS,
4625
5034
  e as APARTE_DEFAULT_LOCALE,
4626
- f as APARTE_DEFAULT_SKELETON_FALLBACKS,
4627
- g as APARTE_DEFAULT_UI_EVENTS,
4628
- h as APARTE_HOST_ATTR,
4629
- i as AparteBackendTransport,
5035
+ f as APARTE_DEFAULT_UI_EVENTS,
5036
+ g as APARTE_HOST_ATTR,
5037
+ h as AparteBackendTransport,
4630
5038
  AparteChat,
4631
5039
  AparteChatBubble,
4632
- j as AparteChatHost,
5040
+ i as AparteChatHost,
4633
5041
  AparteChatStatus,
4634
5042
  AparteChatViewport,
4635
- k as AparteClient,
5043
+ j as AparteClient,
4636
5044
  AparteComposer,
4637
5045
  AparteComposerAction,
4638
5046
  AparteComposerAddAttachment,
@@ -4641,62 +5049,64 @@ export {
4641
5049
  AparteComposerInput,
4642
5050
  AparteComposerSend,
4643
5051
  AparteComposerToolbar,
4644
- l as AparteConfig,
4645
- m as AparteConversationController,
5052
+ k as AparteConfig,
5053
+ AparteContext,
5054
+ l as AparteConversationController,
4646
5055
  AparteConversationList,
4647
- n as AparteConversationManager,
4648
- o as AparteDirectTransport,
5056
+ m as AparteConversationManager,
5057
+ n as AparteDirectTransport,
4649
5058
  AparteElicitation,
4650
5059
  AparteElicitationAbortError,
4651
- q as AparteError,
4652
- r as AparteErrorCode,
5060
+ p as AparteError,
5061
+ q as AparteErrorCode,
4653
5062
  AparteIcon,
4654
5063
  AparteMessageRepository,
4655
5064
  AparteOptgroup,
4656
5065
  AparteOption,
4657
5066
  AparteProgressSpinner,
4658
5067
  AparteSelect,
4659
- t as AparteStreamParser,
4660
- u as aparteGlobalConfig,
4661
- v as applyElementProps,
4662
- w as attachConfig,
5068
+ s as AparteStreamParser,
5069
+ AparteSuggestions,
5070
+ t as aparteGlobalConfig,
5071
+ u as applyElementProps,
5072
+ v as attachConfig,
4663
5073
  buildApprovalPanel,
4664
5074
  buildElicitationPanel,
4665
- z as collectRendererStyles,
4666
- B as contentToText,
4667
- C as contextConfig,
4668
- D as createAparteChatHandler,
4669
- E as createStreamAdapter,
5075
+ y as collectRendererStyles,
5076
+ z as contentToText,
5077
+ B as contextConfig,
5078
+ C as createAparteChatHandler,
5079
+ D as createStreamAdapter,
4670
5080
  cssEscape,
4671
- G as declineDefaultRenderers,
4672
- H as defaultSanitizer,
4673
- I as deriveArtifactKind,
4674
- J as detachConfig,
5081
+ F as declineDefaultRenderers,
5082
+ G as defaultSanitizer,
5083
+ deriveArtifactKind,
5084
+ H as detachConfig,
4675
5085
  escapeAttr,
4676
5086
  escapeHtml,
4677
- M as filesToAttachments,
4678
- N as getAllRenderers,
5087
+ K as filesToAttachments,
5088
+ L as getAllRenderers,
4679
5089
  getSegmentRenderer,
4680
5090
  installDefaultRenderersOnce,
4681
5091
  isAwaitingReply,
4682
- R as isFormatAdapter,
4683
- S as isSafeUrl,
4684
- T as isSegmentSettled,
4685
- U as parseAparteEventStream,
4686
- V as parseMarkdownToSegments,
5092
+ P as isFormatAdapter,
5093
+ Q as isSafeUrl,
5094
+ R as isSegmentSettled,
5095
+ S as parseAparteEventStream,
5096
+ T as parseMarkdownToSegments,
4687
5097
  populateBubbleFromMessage,
4688
- X as readableToAsyncIterable,
5098
+ V as readableToAsyncIterable,
4689
5099
  registerAllComponents,
4690
- Y as registerDefaultRenderers,
4691
- Z as registerSegmentRenderer,
4692
- _ as requestUserInput,
5100
+ W as registerDefaultRenderers,
5101
+ X as registerSegmentRenderer,
5102
+ Y as requestUserInput,
4693
5103
  resolveConfig,
4694
5104
  revokeAttachmentUrls,
4695
5105
  runWithConfig,
4696
- a2 as segmentDuration,
4697
- a3 as segmentTiming,
5106
+ a0 as segmentDuration,
5107
+ a1 as segmentTiming,
4698
5108
  subscribeConfigChange,
4699
- a5 as unregisterSegmentRenderer,
5109
+ a3 as unregisterSegmentRenderer,
4700
5110
  uuid
4701
5111
  };
4702
5112
  //# sourceMappingURL=index.js.map