@aparte/core 0.24.0 → 0.25.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 (63) hide show
  1. package/dist/client/aparte-client.d.ts +3 -0
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/components/bubble/aparte-chat-bubble.d.ts +64 -0
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  5. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  6. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  7. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  9. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  11. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  14. package/dist/components/context/aparte-context.d.ts.map +1 -1
  15. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  16. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  17. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  18. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
  19. package/dist/components/split/aparte-split.d.ts.map +1 -1
  20. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  21. package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
  22. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  23. package/dist/config/aparte-config.d.ts +30 -1
  24. package/dist/config/aparte-config.d.ts.map +1 -1
  25. package/dist/custom-elements.json +8808 -7934
  26. package/dist/{dialog-triggers-BqXRdHWZ.js → defer-hydration-BwMqeRHq.js} +548 -176
  27. package/dist/defer-hydration-BwMqeRHq.js.map +1 -0
  28. package/dist/{glyphs-C8z2oJUg.js → glyphs-2bym5jl5.js} +1 -1
  29. package/dist/{glyphs-C8z2oJUg.js.map → glyphs-2bym5jl5.js.map} +1 -1
  30. package/dist/host/aparte-chat-host.d.ts +22 -0
  31. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  32. package/dist/icons.js +2 -2
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +282 -56
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.node.d.ts +3 -2
  38. package/dist/index.node.d.ts.map +1 -1
  39. package/dist/index.node.js +35 -29
  40. package/dist/index.node.js.map +1 -1
  41. package/dist/interop/defer-hydration.d.ts +34 -0
  42. package/dist/interop/defer-hydration.d.ts.map +1 -0
  43. package/dist/primitives/icon/aparte-icon.d.ts.map +1 -1
  44. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  45. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  46. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  47. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  48. package/dist/renderers/index.d.ts +2 -1
  49. package/dist/renderers/index.d.ts.map +1 -1
  50. package/dist/renderers/mounted-renderer.d.ts +34 -0
  51. package/dist/renderers/mounted-renderer.d.ts.map +1 -0
  52. package/dist/renderers/segment-renderers.d.ts +14 -1
  53. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  54. package/dist/renderers/segments/tool-call.d.ts +2 -1
  55. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  56. package/dist/types/index.d.ts +2 -2
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/types/segments.d.ts +21 -3
  59. package/dist/types/segments.d.ts.map +1 -1
  60. package/dist/types/tools.d.ts +56 -0
  61. package/dist/types/tools.d.ts.map +1 -1
  62. package/package.json +2 -2
  63. package/dist/dialog-triggers-BqXRdHWZ.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a1 as resolveConfig, aa as eventOrigin, ab as activeElementIn, a6 as subscribeConfigChange, a3 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, ac as gateGlide, L as escapeAttr, ad as renderingVersionOf, H as cssEscape, ae as mergeSegmentUpdate, M as escapeHtml, V as isSegmentSettled, af as writeStreamedMarkdown, U as isSafeUrl, C as copyText, P as getSegmentRenderer, Q as installDefaultRenderersOnce, r as AparteMessageRepository, ag as stampSegmentActivity, ah as stampSegmentOnInsert, ai as stampSegmentOnUpdate, aj as renumberSegments, ak as openSegmentIds, al as isTerminalStatus, am as adoptMessageSegments, S as isAwaitingReply, Y as populateBubbleFromMessage, a9 as uuid, a2 as revokeAttachmentUrls, X as parseMarkdownToSegments, an as APARTE_DERIVED_SEGMENTS, ao as rememberConversationStatus, ap as tellConversationStatus, aq as conversationStatusOf, e as APARTE_DEFAULT_LOCALE, ar as chatBoundaryOf, as as rootOf, R as installDialogTriggersOnce } from "./dialog-triggers-BqXRdHWZ.js";
2
- import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, t, u, v, y, z, B, D, E, F, G, I, J, K, N, O, T, W, Z, _, $, a0, a4, a5, a7, a8 } from "./dialog-triggers-BqXRdHWZ.js";
1
+ import { K as deferHydrationOf, a6 as resolveConfig, ag as eventOrigin, ah as activeElementIn, ab as subscribeConfigChange, a8 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, ai as gateGlide, O as escapeAttr, aj as toolCallRenderer, ak as toolRendererThatDrew, t as aparteGlobalConfig, al as isMountedRenderer, am as renderingVersionOf, H as cssEscape, an as onRenderersChange, ao as mergeSegmentUpdate, P as escapeHtml, Y as isSegmentSettled, ap as writeStreamedMarkdown, X as isSafeUrl, C as copyText, S as getSegmentRenderer, T as installDefaultRenderersOnce, r as AparteMessageRepository, aq as stampSegmentActivity, ar as stampSegmentOnInsert, as as stampSegmentOnUpdate, at as renumberSegments, au as openSegmentIds, av as isTerminalStatus, aw as adoptMessageSegments, V as isAwaitingReply, a0 as populateBubbleFromMessage, af as uuid, a7 as revokeAttachmentUrls, $ as parseMarkdownToSegments, ax as APARTE_DERIVED_SEGMENTS, ay as rememberConversationStatus, az as tellConversationStatus, aA as conversationStatusOf, e as APARTE_DEFAULT_LOCALE, aB as chatBoundaryOf, aC as rootOf, U as installDialogTriggersOnce } from "./defer-hydration-BwMqeRHq.js";
2
+ import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, u, v, y, z, B, D, E, F, G, I, J, L, M, N, Q, R, W, Z, _, a1, a2, a3, a4, a5, a9, aa, ac, ad, ae } from "./defer-hydration-BwMqeRHq.js";
3
3
  function presenceOn(value) {
4
4
  return value === "" ? true : Boolean(value);
5
5
  }
@@ -64,6 +64,7 @@ class AparteOption extends HTMLElement {
64
64
  dot.setAttribute("data-status", status);
65
65
  }
66
66
  }
67
+ deferHydrationOf(AparteOption);
67
68
  if (!customElements.get("aparte-option")) {
68
69
  customElements.define("aparte-option", AparteOption);
69
70
  }
@@ -173,6 +174,7 @@ class AparteOptgroup extends HTMLElement {
173
174
  });
174
175
  }
175
176
  }
177
+ deferHydrationOf(AparteOptgroup);
176
178
  if (!customElements.get("aparte-optgroup")) {
177
179
  customElements.define("aparte-optgroup", AparteOptgroup);
178
180
  }
@@ -726,6 +728,7 @@ function optionLabelText(option) {
726
728
  }
727
729
  return text.trim();
728
730
  }
731
+ deferHydrationOf(AparteSelect);
729
732
  if (!customElements.get("aparte-select")) {
730
733
  customElements.define("aparte-select", AparteSelect);
731
734
  }
@@ -760,6 +763,7 @@ class AparteProgressSpinner extends HTMLElement {
760
763
  this.innerHTML = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><circle class="aparte-spinner-track" cx="12" cy="12" r="${this._r}"/><circle class="aparte-spinner-fill" cx="12" cy="12" r="${this._r}" stroke-dasharray="${dasharray}" stroke-dashoffset="${dashoffset.toFixed(2)}"/></svg>`;
761
764
  }
762
765
  }
766
+ deferHydrationOf(AparteProgressSpinner);
763
767
  if (!customElements.get("aparte-progress-spinner")) {
764
768
  customElements.define("aparte-progress-spinner", AparteProgressSpinner);
765
769
  }
@@ -786,6 +790,7 @@ class AparteIcon extends HTMLElement {
786
790
  this.firstElementChild?.setAttribute("aria-hidden", "true");
787
791
  }
788
792
  }
793
+ deferHydrationOf(AparteIcon);
789
794
  if (!customElements.get("aparte-icon")) {
790
795
  customElements.define("aparte-icon", AparteIcon);
791
796
  }
@@ -1100,6 +1105,7 @@ class AparteElicitation extends HTMLElement {
1100
1105
  }
1101
1106
  }
1102
1107
  if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
1108
+ deferHydrationOf(AparteElicitation);
1103
1109
  customElements.define("aparte-elicitation", AparteElicitation);
1104
1110
  }
1105
1111
  function isTranscriptEmpty(viewport) {
@@ -1297,6 +1303,7 @@ class AparteChat extends HTMLElement {
1297
1303
  this.toggleAttribute("data-empty", !viewport || isTranscriptEmpty(viewport));
1298
1304
  }
1299
1305
  }
1306
+ deferHydrationOf(AparteChat);
1300
1307
  if (!customElements.get("aparte-chat")) {
1301
1308
  customElements.define("aparte-chat", AparteChat);
1302
1309
  }
@@ -1400,6 +1407,7 @@ class AparteChatBubble extends HTMLElement {
1400
1407
  this._updateLocalizedLabels();
1401
1408
  this._updateWaiting();
1402
1409
  this._renderAvatar();
1410
+ this._redrawSegmentsWhoseRendererChanged();
1403
1411
  this._relabelSegments();
1404
1412
  this._updateTimestamp(this.getAttribute("timestamp"));
1405
1413
  if (!this._redrawQueued) {
@@ -1414,16 +1422,151 @@ class AparteChatBubble extends HTMLElement {
1414
1422
  _redrawQueued = false;
1415
1423
  /** The {@link renderingVersionOf} count the content was last drawn with. */
1416
1424
  _drawnWithVersion = 0;
1425
+ /** Every segment on screen, by id: see {@link DrawnSegment}. */
1426
+ _drawn = /* @__PURE__ */ new Map();
1427
+ /** The segments were released because the bubble was still out of the page at the next microtask. */
1428
+ _releasedWhileDetached = false;
1429
+ /** A {@link _scheduleReleaseCheck} is queued. */
1430
+ _releaseCheckQueued = false;
1431
+ /** Unsubscribes from the renderer registries' channel; set while connected. */
1432
+ _offRenderers = null;
1433
+ /** The tool renderer in effect for a call: none for a hidden call or another segment. */
1434
+ _toolRendererFor(segment) {
1435
+ if (segment.type !== "tool_call") return null;
1436
+ const cfg = this._cfg;
1437
+ if (resolveSegmentRenderer("tool_call", cfg) !== toolCallRenderer) return null;
1438
+ const name = segment.toolCall?.name;
1439
+ if (!name || cfg.isToolHidden(name)) return null;
1440
+ return cfg.getToolRenderer(name) ?? null;
1441
+ }
1442
+ /**
1443
+ * Record a segment as drawn, set it up, then read back which tool renderer is behind the
1444
+ * row (tool-call.ts records it as the row is set up — one record, not a second lookup).
1445
+ * Recorded BEFORE `setup`: a setup that throws (a framework mount) leaves an element that
1446
+ * will still be cleaned up. The same id drawn twice releases the first element.
1447
+ */
1448
+ _install(segment, el2, renderer) {
1449
+ const previous = this._drawn.get(segment.id);
1450
+ if (previous && previous.el !== el2) this._release(segment.id);
1451
+ const drawn = { el: el2, renderer, tool: null, segment };
1452
+ this._drawn.set(segment.id, drawn);
1453
+ if (!this.isConnected) this._scheduleReleaseCheck();
1454
+ if (renderer?.setup) runWithConfig(this._cfg, () => renderer.setup(el2, segment));
1455
+ if (segment.type === "tool_call") drawn.tool = toolRendererThatDrew(el2);
1456
+ }
1457
+ /**
1458
+ * The segment's element leaves for good: its renderer's `cleanup`, once. A cleanup that
1459
+ * throws is reported and costs nothing else — the element still goes, and the others
1460
+ * are still released.
1461
+ */
1462
+ _release(id) {
1463
+ const drawn = this._drawn.get(id);
1464
+ if (!drawn) return;
1465
+ this._drawn.delete(id);
1466
+ const cleanup = drawn.renderer?.cleanup;
1467
+ if (!cleanup) return;
1468
+ try {
1469
+ runWithConfig(this._cfg, () => cleanup.call(drawn.renderer, drawn.el, drawn.segment));
1470
+ } catch (err) {
1471
+ console.error("[aparte] a segment renderer's cleanup threw", err);
1472
+ }
1473
+ }
1474
+ _releaseAll() {
1475
+ for (const id of [...this._drawn.keys()]) this._release(id);
1476
+ }
1477
+ /**
1478
+ * Gone for good, or only moved? A keyed list moving a row takes it out and puts it back
1479
+ * before the next microtask, and must not unmount what its segments hold: decide then.
1480
+ */
1481
+ _scheduleReleaseCheck() {
1482
+ if (this._releaseCheckQueued) return;
1483
+ this._releaseCheckQueued = true;
1484
+ queueMicrotask(() => {
1485
+ this._releaseCheckQueued = false;
1486
+ if (this.isConnected || this._drawn.size === 0) return;
1487
+ this._releaseAll();
1488
+ this._releasedWhileDetached = true;
1489
+ });
1490
+ }
1491
+ /** One segment drawn again in place: its renderer cleans up, the current one draws. False when it was never drawn. */
1492
+ _redrawSegment(segment) {
1493
+ const drawn = this._drawn.get(segment.id);
1494
+ if (!drawn) return false;
1495
+ const cfg = this._cfg;
1496
+ const renderer = resolveSegmentRenderer(segment.type, cfg);
1497
+ const old = drawn.el;
1498
+ this._release(segment.id);
1499
+ const el2 = renderer ? segmentRenderResultToElement(runWithConfig(cfg, () => renderer.render(segment)), segment) : unrenderedSegment(segment);
1500
+ if (!el2) {
1501
+ old.remove();
1502
+ return true;
1503
+ }
1504
+ old.replaceWith(el2);
1505
+ this._install(segment, el2, renderer);
1506
+ return true;
1507
+ }
1508
+ /** The renderer that drew this segment is still the one in effect (for a tool call, the tool renderer too). */
1509
+ _stillDrawnByCurrent(segment, drawn) {
1510
+ return resolveSegmentRenderer(segment.type, this._cfg) === drawn.renderer && this._toolRendererFor(segment) === drawn.tool;
1511
+ }
1512
+ /**
1513
+ * A renderer registered, unregistered or overridden since these segments were drawn: draw
1514
+ * again only the segments whose renderer is no longer the one that drew them (for a tool
1515
+ * call, the tool renderer counts), in place. The others keep their elements — and with
1516
+ * them anything mounted in them, which a redraw of the whole message would unmount.
1517
+ */
1518
+ _redrawSegmentsWhoseRendererChanged() {
1519
+ if (!this.isConnected || !this._segmentsEl || this._drawn.size === 0) return;
1520
+ for (const segment of this._segments) {
1521
+ const drawn = this._drawn.get(segment.id);
1522
+ if (!drawn || this._stillDrawnByCurrent(segment, drawn)) continue;
1523
+ this._redrawSegment(segment);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * The registries' channel: a renderer changed on our config — or on the global one, whose
1528
+ * segment renderers a chat with its own config inherits.
1529
+ */
1530
+ _onRenderersChange = (cfg) => {
1531
+ if (cfg !== this._cfg && cfg !== aparteGlobalConfig) return;
1532
+ this._redrawSegmentsWhoseRendererChanged();
1533
+ };
1534
+ /**
1535
+ * A card a mounted renderer keeps (a framework component): its drawing reads none of the
1536
+ * providers a rendering-version bump announces, so it stays mounted through one — as long
1537
+ * as that renderer is still the one in effect.
1538
+ */
1539
+ _keepsItsDrawing(segment, drawn) {
1540
+ const mounted = isMountedRenderer(drawn.renderer) || segment.type === "tool_call" && isMountedRenderer(drawn.tool);
1541
+ return mounted && this._stillDrawnByCurrent(segment, drawn);
1542
+ }
1417
1543
  /**
1418
1544
  * A Markdown or highlight provider set after this message was drawn: draw it again.
1419
1545
  * Not mid-stream — the stream reads the providers as it goes, and its end redraws — and
1420
1546
  * not mid-edit, where the drawn message is hidden behind the editor and drawing it would
1421
1547
  * show it again beside the editor; leaving the edit calls this.
1548
+ *
1549
+ * Segment by segment, keeping the cards a mounted renderer holds: a highlighter that loads
1550
+ * after the first answer must not remount every card on the page and lose its state.
1422
1551
  */
1423
1552
  _redrawIfStale() {
1424
1553
  if (this._streaming || this._editing || renderingVersionOf(this._cfg) === this._drawnWithVersion) return;
1425
- if (this._segments.length > 0) this._renderSegments();
1426
- else this._updateContent();
1554
+ if (this._segments.length === 0) {
1555
+ this._updateContent();
1556
+ return;
1557
+ }
1558
+ if (this._segments.some((s2) => !this._drawn.has(s2.id))) {
1559
+ this._renderSegments();
1560
+ return;
1561
+ }
1562
+ this._drawnWithVersion = renderingVersionOf(this._cfg);
1563
+ for (const segment of this._segments) {
1564
+ const drawn = this._drawn.get(segment.id);
1565
+ if (this._keepsItsDrawing(segment, drawn)) continue;
1566
+ this._redrawSegment(segment);
1567
+ }
1568
+ this._reflectError();
1569
+ this._syncCopyAction();
1427
1570
  }
1428
1571
  /**
1429
1572
  * Ask every rendered segment to re-read its config-derived text.
@@ -1482,7 +1625,12 @@ class AparteChatBubble extends HTMLElement {
1482
1625
  this._render();
1483
1626
  this._updateContent();
1484
1627
  this._updateTimestamp(this.getAttribute("timestamp"));
1628
+ if (this._releasedWhileDetached) {
1629
+ this._releasedWhileDetached = false;
1630
+ if (this._segments.length > 0) this._renderSegments();
1631
+ }
1485
1632
  window.addEventListener("aparte-config-change", this._onConfigChange);
1633
+ this._offRenderers ??= onRenderersChange(this._onRenderersChange);
1486
1634
  this.addEventListener("click", this._onBranchPickerClick);
1487
1635
  this.addEventListener("click", this._onLinkClick);
1488
1636
  }
@@ -1499,7 +1647,10 @@ class AparteChatBubble extends HTMLElement {
1499
1647
  * into one.
1500
1648
  */
1501
1649
  aparteConfigChanged() {
1502
- queueMicrotask(() => this._redrawFrameIfChanged());
1650
+ queueMicrotask(() => {
1651
+ this._redrawFrameIfChanged();
1652
+ this._redrawSegmentsWhoseRendererChanged();
1653
+ });
1503
1654
  }
1504
1655
  _redrawFrameIfChanged() {
1505
1656
  if (!this.isConnected || this._editing) return;
@@ -1512,6 +1663,7 @@ class AparteChatBubble extends HTMLElement {
1512
1663
  this._avatarCleanup = null;
1513
1664
  }
1514
1665
  this._resetMarkdownStream();
1666
+ this._releaseAll();
1515
1667
  this.replaceChildren();
1516
1668
  this._render();
1517
1669
  this._updateContent();
@@ -1522,6 +1674,8 @@ class AparteChatBubble extends HTMLElement {
1522
1674
  }
1523
1675
  disconnectedCallback() {
1524
1676
  window.removeEventListener("aparte-config-change", this._onConfigChange);
1677
+ this._offRenderers?.();
1678
+ this._offRenderers = null;
1525
1679
  this.removeEventListener("click", this._onBranchPickerClick);
1526
1680
  this.removeEventListener("click", this._onLinkClick);
1527
1681
  if (this._avatarCleanup) {
@@ -1531,6 +1685,7 @@ class AparteChatBubble extends HTMLElement {
1531
1685
  }
1532
1686
  this._avatarCleanup = null;
1533
1687
  }
1688
+ this._scheduleReleaseCheck();
1534
1689
  }
1535
1690
  attributeChangedCallback(name, oldValue, newValue) {
1536
1691
  if (oldValue === newValue) return;
@@ -1591,10 +1746,38 @@ class AparteChatBubble extends HTMLElement {
1591
1746
  }
1592
1747
  /** Set segments for rich content */
1593
1748
  setSegments(segments) {
1594
- this._segments = [...segments];
1595
- this._renderSegments();
1749
+ this._replaceSegments(segments);
1596
1750
  this._updateWaiting();
1597
1751
  }
1752
+ /** A whole new list, from `setSegments` or `updateMessage({ segments })`: copied in, then drawn. */
1753
+ _replaceSegments(segments) {
1754
+ const next = [...segments];
1755
+ if (this._resyncKeepingCards(next)) return;
1756
+ this._segments = next;
1757
+ this._renderSegments();
1758
+ }
1759
+ /**
1760
+ * The same segments again, in the same order — the host re-syncing a message when its turn
1761
+ * ends — while a mounted renderer holds one of them: the cards it holds are handed the new
1762
+ * state, and only the other segments are drawn again, in place. Drawing everything again
1763
+ * unmounted every framework card at the end of every turn and lost its state. False, and
1764
+ * nothing touched, in every other case: those keep the full redraw.
1765
+ */
1766
+ _resyncKeepingCards(next) {
1767
+ if (!this._segmentsEl || next.length !== this._segments.length) return false;
1768
+ if (next.some((s2, i2) => s2.id !== this._segments[i2].id || !this._drawn.has(s2.id))) return false;
1769
+ const kept = new Set(next.filter((s2) => this._keepsItsDrawing(s2, this._drawn.get(s2.id))).map((s2) => s2.id));
1770
+ if (kept.size === 0) return false;
1771
+ this._segments = next;
1772
+ this._drawnWithVersion = renderingVersionOf(this._cfg);
1773
+ for (const segment of next) {
1774
+ if (kept.has(segment.id)) this._applySegmentUpdate(segment.id, segment, segment);
1775
+ else this._redrawSegment(segment);
1776
+ }
1777
+ this._reflectError();
1778
+ this._syncCopyAction();
1779
+ return true;
1780
+ }
1598
1781
  /** Add a segment */
1599
1782
  addSegment(segment) {
1600
1783
  this._segments.push(segment);
@@ -1645,6 +1828,7 @@ class AparteChatBubble extends HTMLElement {
1645
1828
  if (index !== -1) {
1646
1829
  this._segments.splice(index, 1);
1647
1830
  }
1831
+ this._release(segmentId);
1648
1832
  const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1649
1833
  el2?.remove();
1650
1834
  this._updateWaiting();
@@ -1693,8 +1877,7 @@ class AparteChatBubble extends HTMLElement {
1693
1877
  this._updateContent();
1694
1878
  }
1695
1879
  if ("segments" in updates) {
1696
- this._segments = [...updates.segments];
1697
- this._renderSegments();
1880
+ this._replaceSegments(updates.segments);
1698
1881
  }
1699
1882
  if ("timestamp" in updates) {
1700
1883
  this._updateTimestamp(updates.timestamp);
@@ -1723,10 +1906,12 @@ class AparteChatBubble extends HTMLElement {
1723
1906
  const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)), segment);
1724
1907
  if (el2) {
1725
1908
  this._segmentsEl.appendChild(el2);
1726
- runWithConfig(this._cfg, () => renderer.setup?.(el2, segment));
1909
+ this._install(segment, el2, renderer);
1727
1910
  }
1728
1911
  } else {
1729
- this._segmentsEl.appendChild(unrenderedSegment(segment));
1912
+ const el2 = unrenderedSegment(segment);
1913
+ this._segmentsEl.appendChild(el2);
1914
+ this._install(segment, el2, void 0);
1730
1915
  }
1731
1916
  if (this._contentEl) this._contentEl.style.display = "none";
1732
1917
  this._reflectError();
@@ -1737,15 +1922,32 @@ class AparteChatBubble extends HTMLElement {
1737
1922
  this._renderSegments();
1738
1923
  return;
1739
1924
  }
1925
+ if (this._releasedWhileDetached) return;
1740
1926
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
1741
1927
  if (!renderer) return;
1928
+ const drawnBefore = this._drawn.get(segmentId);
1929
+ if (drawnBefore && drawnBefore.renderer !== renderer) {
1930
+ drawnBefore.segment = segment;
1931
+ this._redrawSegment(segment);
1932
+ return;
1933
+ }
1742
1934
  if (renderer.update) {
1743
1935
  runWithConfig(this._cfg, () => renderer.update(el2, segment));
1936
+ const drawn = this._drawn.get(segmentId);
1937
+ if (drawn) {
1938
+ drawn.segment = segment;
1939
+ const now = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1940
+ if (now && now !== drawn.el) {
1941
+ drawn.el = now;
1942
+ drawn.tool = segment.type === "tool_call" ? toolRendererThatDrew(now) : null;
1943
+ }
1944
+ }
1744
1945
  } else {
1745
1946
  const newEl = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)), segment);
1746
1947
  if (newEl) {
1948
+ this._release(segmentId);
1747
1949
  el2.replaceWith(newEl);
1748
- runWithConfig(this._cfg, () => renderer.setup?.(newEl, segment));
1950
+ this._install(segment, newEl, renderer);
1749
1951
  }
1750
1952
  }
1751
1953
  if ("collapsed" in updates) {
@@ -2025,6 +2227,7 @@ class AparteChatBubble extends HTMLElement {
2025
2227
  _renderSegments() {
2026
2228
  if (!this._segmentsEl) return;
2027
2229
  this._drawnWithVersion = renderingVersionOf(this._cfg);
2230
+ this._releaseAll();
2028
2231
  this._segmentsEl.innerHTML = "";
2029
2232
  for (const segment of this._segments) {
2030
2233
  const renderer = resolveSegmentRenderer(segment.type, this._cfg);
@@ -2032,10 +2235,12 @@ class AparteChatBubble extends HTMLElement {
2032
2235
  const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)), segment);
2033
2236
  if (el2) {
2034
2237
  this._segmentsEl.appendChild(el2);
2035
- runWithConfig(this._cfg, () => renderer.setup?.(el2, segment));
2238
+ this._install(segment, el2, renderer);
2036
2239
  }
2037
2240
  } else {
2038
- this._segmentsEl.appendChild(unrenderedSegment(segment));
2241
+ const el2 = unrenderedSegment(segment);
2242
+ this._segmentsEl.appendChild(el2);
2243
+ this._install(segment, el2, void 0);
2039
2244
  }
2040
2245
  }
2041
2246
  if (this._contentEl) {
@@ -2097,18 +2302,18 @@ class AparteChatBubble extends HTMLElement {
2097
2302
  const customAttachment = this._cfg.getAttachmentRenderer?.();
2098
2303
  if (customAttachment) {
2099
2304
  this._attachmentsEl.replaceChildren();
2100
- for (const a2 of this._attachments) {
2101
- const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a2)));
2305
+ for (const a6 of this._attachments) {
2306
+ const el2 = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a6)));
2102
2307
  if (el2) this._attachmentsEl.appendChild(el2);
2103
2308
  }
2104
2309
  return;
2105
2310
  }
2106
- this._attachmentsEl.innerHTML = this._attachments.map((a2) => {
2107
- const name = escapeHtml(a2.name);
2108
- if (a2.type.startsWith("image/") && this._isShowableUrl(a2.url)) {
2109
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeHtml(a2.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
2311
+ this._attachmentsEl.innerHTML = this._attachments.map((a6) => {
2312
+ const name = escapeHtml(a6.name);
2313
+ if (a6.type.startsWith("image/") && this._isShowableUrl(a6.url)) {
2314
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeHtml(a6.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
2110
2315
  }
2111
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumbnail__label">${escapeHtml(this._fileExt(a2.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
2316
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumbnail__label">${escapeHtml(this._fileExt(a6.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
2112
2317
  }).join("");
2113
2318
  if (!this._cfg.getHostHandlers().attachmentPreview) return;
2114
2319
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
@@ -2294,14 +2499,14 @@ class AparteChatBubble extends HTMLElement {
2294
2499
  const buttons = [];
2295
2500
  if (this._role === "user") {
2296
2501
  if (config.user) {
2297
- for (const a2 of config.user) buttons.push(this._actionButtonHtml(a2, icons, locale));
2502
+ for (const a6 of config.user) buttons.push(this._actionButtonHtml(a6, icons, locale));
2298
2503
  } else {
2299
2504
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
2300
2505
  if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
2301
2506
  }
2302
2507
  } else if (this._role === "assistant") {
2303
2508
  if (config.assistant) {
2304
- for (const a2 of config.assistant) buttons.push(this._actionButtonHtml(a2, icons, locale));
2509
+ for (const a6 of config.assistant) buttons.push(this._actionButtonHtml(a6, icons, locale));
2305
2510
  } else {
2306
2511
  if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
2307
2512
  if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
@@ -2397,16 +2602,16 @@ class AparteChatBubble extends HTMLElement {
2397
2602
  /** Append the registered custom action buttons for this bubble's role. */
2398
2603
  _appendCustomActions() {
2399
2604
  if (!this._actionBarEl) return;
2400
- for (const a2 of this._cfg.getActions("bubble")) {
2401
- const roles = a2.bubble?.roles ?? ["user", "assistant"];
2605
+ for (const a6 of this._cfg.getActions("bubble")) {
2606
+ const roles = a6.bubble?.roles ?? ["user", "assistant"];
2402
2607
  if (!roles.includes(this._role)) continue;
2403
2608
  const btn = document.createElement("button");
2404
2609
  btn.type = "button";
2405
2610
  btn.className = "aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-custom";
2406
- btn.dataset["action"] = `custom:${a2.id}`;
2407
- btn.setAttribute("aria-label", a2.label);
2408
- btn.setAttribute("title", a2.label);
2409
- btn.innerHTML = a2.icon.startsWith("<") ? a2.icon : this._cfg.getIcon(a2.icon) ?? a2.iconFallback ?? "";
2611
+ btn.dataset["action"] = `custom:${a6.id}`;
2612
+ btn.setAttribute("aria-label", a6.label);
2613
+ btn.setAttribute("title", a6.label);
2614
+ btn.innerHTML = a6.icon.startsWith("<") ? a6.icon : this._cfg.getIcon(a6.icon) ?? a6.iconFallback ?? "";
2410
2615
  this._actionBarEl.appendChild(btn);
2411
2616
  }
2412
2617
  }
@@ -2653,11 +2858,10 @@ class AparteChatBubble extends HTMLElement {
2653
2858
  }
2654
2859
  this._updateWaiting();
2655
2860
  if (wasStreaming && !streaming) this._updateContent();
2656
- if (wasStreaming && !streaming && this._segments.length > 0 && renderingVersionOf(this._cfg) !== this._drawnWithVersion) {
2657
- this._renderSegments();
2658
- }
2861
+ if (wasStreaming && !streaming && this._segments.length > 0) this._redrawIfStale();
2659
2862
  }
2660
2863
  }
2864
+ deferHydrationOf(AparteChatBubble);
2661
2865
  if (!customElements.get("aparte-chat-bubble")) {
2662
2866
  customElements.define("aparte-chat-bubble", AparteChatBubble);
2663
2867
  }
@@ -2831,6 +3035,7 @@ class AparteChatStatus extends HTMLElement {
2831
3035
  container.setAttribute("aria-label", text || this._word());
2832
3036
  }
2833
3037
  }
3038
+ deferHydrationOf(AparteChatStatus);
2834
3039
  if (!customElements.get("aparte-chat-status")) {
2835
3040
  customElements.define("aparte-chat-status", AparteChatStatus);
2836
3041
  }
@@ -3243,10 +3448,10 @@ class AparteChatViewport extends HTMLElement {
3243
3448
  this._notifyBubble(messageId, "addSegment", stamped);
3244
3449
  this._autoScroll();
3245
3450
  }
3246
- updateSegment(a2, b2, c2) {
3451
+ updateSegment(a6, b2, c2) {
3247
3452
  const clientForm = c2 === void 0 && typeof b2 === "object";
3248
- const messageId = clientForm ? this._activeMessageId() : a2;
3249
- const segmentId = clientForm ? a2 : b2;
3453
+ const messageId = clientForm ? this._activeMessageId() : a6;
3454
+ const segmentId = clientForm ? a6 : b2;
3250
3455
  const updates = clientForm ? b2 : c2;
3251
3456
  if (!messageId) return;
3252
3457
  const message = this._repo.getMessageById(messageId);
@@ -3259,10 +3464,10 @@ class AparteChatViewport extends HTMLElement {
3259
3464
  this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
3260
3465
  }
3261
3466
  }
3262
- removeSegment(a2, b2) {
3467
+ removeSegment(a6, b2) {
3263
3468
  const clientForm = b2 === void 0;
3264
- const messageId = clientForm ? this._activeMessageId() : a2;
3265
- const segmentId = clientForm ? a2 : b2;
3469
+ const messageId = clientForm ? this._activeMessageId() : a6;
3470
+ const segmentId = clientForm ? a6 : b2;
3266
3471
  if (!messageId || !segmentId) return;
3267
3472
  const message = this._repo.getMessageById(messageId);
3268
3473
  if (message?.segments) {
@@ -4399,6 +4604,7 @@ class AparteChatViewport extends HTMLElement {
4399
4604
  this._settleUntil = 0;
4400
4605
  }
4401
4606
  }
4607
+ deferHydrationOf(AparteChatViewport);
4402
4608
  if (!customElements.get("aparte-chat-viewport")) {
4403
4609
  customElements.define("aparte-chat-viewport", AparteChatViewport);
4404
4610
  }
@@ -4905,6 +5111,7 @@ class AparteComposer extends HTMLElement {
4905
5111
  if (this._panelActive) this._evictPanel();
4906
5112
  }
4907
5113
  }
5114
+ deferHydrationOf(AparteComposer);
4908
5115
  if (!customElements.get("aparte-composer")) {
4909
5116
  customElements.define("aparte-composer", AparteComposer);
4910
5117
  }
@@ -5152,6 +5359,7 @@ class AparteComposerInput extends HTMLElement {
5152
5359
  }
5153
5360
  /** Escape a value before it lands in a double-quoted HTML attribute. */
5154
5361
  }
5362
+ deferHydrationOf(AparteComposerInput);
5155
5363
  if (!customElements.get("aparte-composer-input")) {
5156
5364
  customElements.define("aparte-composer-input", AparteComposerInput);
5157
5365
  }
@@ -5313,6 +5521,7 @@ class AparteComposerSend extends HTMLElement {
5313
5521
  return resolveConfig(this).getIcon("stop");
5314
5522
  }
5315
5523
  }
5524
+ deferHydrationOf(AparteComposerSend);
5316
5525
  if (!customElements.get("aparte-composer-send")) {
5317
5526
  customElements.define("aparte-composer-send", AparteComposerSend);
5318
5527
  }
@@ -5381,6 +5590,7 @@ class AparteComposerCancel extends HTMLElement {
5381
5590
  return resolveConfig(this).getIcon("stop");
5382
5591
  }
5383
5592
  }
5593
+ deferHydrationOf(AparteComposerCancel);
5384
5594
  if (!customElements.get("aparte-composer-cancel")) {
5385
5595
  customElements.define("aparte-composer-cancel", AparteComposerCancel);
5386
5596
  }
@@ -5466,6 +5676,7 @@ class AparteComposerAttachments extends HTMLElement {
5466
5676
  return dot > 0 ? filename.slice(dot + 1).toUpperCase().slice(0, 4) : "FILE";
5467
5677
  }
5468
5678
  }
5679
+ deferHydrationOf(AparteComposerAttachments);
5469
5680
  if (!customElements.get("aparte-composer-attachments")) {
5470
5681
  customElements.define("aparte-composer-attachments", AparteComposerAttachments);
5471
5682
  }
@@ -5592,6 +5803,7 @@ class AparteComposerAddAttachment extends HTMLElement {
5592
5803
  };
5593
5804
  }
5594
5805
  }
5806
+ deferHydrationOf(AparteComposerAddAttachment);
5595
5807
  if (!customElements.get("aparte-composer-add-attachment")) {
5596
5808
  customElements.define("aparte-composer-add-attachment", AparteComposerAddAttachment);
5597
5809
  }
@@ -5674,6 +5886,7 @@ class AparteComposerAction extends HTMLElement {
5674
5886
  return resolveConfig(this).getIcon(icon) ?? icon;
5675
5887
  }
5676
5888
  }
5889
+ deferHydrationOf(AparteComposerAction);
5677
5890
  if (!customElements.get("aparte-composer-action")) {
5678
5891
  customElements.define("aparte-composer-action", AparteComposerAction);
5679
5892
  }
@@ -5712,6 +5925,7 @@ class AparteComposerToolbar extends HTMLElement {
5712
5925
  else this.setAttribute("data-empty", "");
5713
5926
  }
5714
5927
  }
5928
+ deferHydrationOf(AparteComposerToolbar);
5715
5929
  if (!customElements.get("aparte-composer-toolbar")) {
5716
5930
  customElements.define("aparte-composer-toolbar", AparteComposerToolbar);
5717
5931
  }
@@ -6053,7 +6267,7 @@ class AparteConversationList extends HTMLElement {
6053
6267
  }
6054
6268
  }
6055
6269
  const fixed = ["pinned", "today", "yesterday", "week", "month"].map((k2) => buckets.get(k2)).filter((g2) => !!g2);
6056
- const months = [...buckets.keys()].filter((k2) => k2.startsWith("m-")).sort((a2, b2) => b2.localeCompare(a2)).map((k2) => buckets.get(k2));
6270
+ const months = [...buckets.keys()].filter((k2) => k2.startsWith("m-")).sort((a6, b2) => b2.localeCompare(a6)).map((k2) => buckets.get(k2));
6057
6271
  const undated = buckets.get("undated");
6058
6272
  return [...fixed, ...months, ...undated ? [undated] : []];
6059
6273
  }
@@ -6472,6 +6686,7 @@ class AparteConversationList extends HTMLElement {
6472
6686
  });
6473
6687
  }
6474
6688
  }
6689
+ deferHydrationOf(AparteConversationList);
6475
6690
  if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
6476
6691
  const isSuggestion = (value) => typeof value === "string" || typeof value === "object" && value !== null && typeof value.label === "string";
6477
6692
  const parseSuggestions = (raw) => {
@@ -6699,6 +6914,7 @@ class AparteSuggestions extends HTMLElement {
6699
6914
  }
6700
6915
  }
6701
6916
  }
6917
+ deferHydrationOf(AparteSuggestions);
6702
6918
  if (!customElements.get("aparte-suggestions")) {
6703
6919
  customElements.define("aparte-suggestions", AparteSuggestions);
6704
6920
  }
@@ -6901,6 +7117,7 @@ class AparteContext extends HTMLElement {
6901
7117
  }
6902
7118
  }
6903
7119
  }
7120
+ deferHydrationOf(AparteContext);
6904
7121
  if (!customElements.get("aparte-context")) {
6905
7122
  customElements.define("aparte-context", AparteContext);
6906
7123
  }
@@ -7363,11 +7580,12 @@ class AparteScrollRail extends HTMLElement {
7363
7580
  this._held = null;
7364
7581
  }
7365
7582
  }
7366
- function sameElements(a2, b2) {
7367
- if (a2.length !== b2.length) return false;
7368
- for (let i2 = 0; i2 < a2.length; i2++) if (a2[i2] !== b2[i2]) return false;
7583
+ function sameElements(a6, b2) {
7584
+ if (a6.length !== b2.length) return false;
7585
+ for (let i2 = 0; i2 < a6.length; i2++) if (a6[i2] !== b2[i2]) return false;
7369
7586
  return true;
7370
7587
  }
7588
+ deferHydrationOf(AparteScrollRail);
7371
7589
  if (!customElements.get("aparte-scroll-rail")) {
7372
7590
  customElements.define("aparte-scroll-rail", AparteScrollRail);
7373
7591
  }
@@ -7718,6 +7936,7 @@ function visibleFocusable(root) {
7718
7936
  function nearestSidebar(control) {
7719
7937
  return control.closest("aparte-sidebar") ?? rootOf(control).querySelector("aparte-sidebar") ?? document.querySelector("aparte-sidebar");
7720
7938
  }
7939
+ deferHydrationOf(AparteSidebar);
7721
7940
  if (!customElements.get("aparte-sidebar")) {
7722
7941
  customElements.define("aparte-sidebar", AparteSidebar);
7723
7942
  }
@@ -8496,6 +8715,7 @@ class AparteSplit extends HTMLElement {
8496
8715
  handle.dataset["ownLabel"] = "locale";
8497
8716
  }
8498
8717
  }
8718
+ deferHydrationOf(AparteSplit);
8499
8719
  if (!customElements.get("aparte-split")) {
8500
8720
  customElements.define("aparte-split", AparteSplit);
8501
8721
  }
@@ -8583,7 +8803,7 @@ export {
8583
8803
  AparteSplit,
8584
8804
  s as AparteStreamParser,
8585
8805
  AparteSuggestions,
8586
- t as aparteGlobalConfig,
8806
+ aparteGlobalConfig,
8587
8807
  u as applyElementProps,
8588
8808
  v as attachConfig,
8589
8809
  buildApprovalPanel,
@@ -8599,34 +8819,40 @@ export {
8599
8819
  cssEscape,
8600
8820
  I as declineDefaultRenderers,
8601
8821
  J as defaultSanitizer,
8602
- K as detachConfig,
8822
+ deferHydrationOf,
8823
+ L as defineMountedSegmentRenderer,
8824
+ M as defineMountedToolRenderer,
8825
+ N as detachConfig,
8603
8826
  escapeAttr,
8604
8827
  escapeHtml,
8605
- N as filesToAttachments,
8606
- O as getAllRenderers,
8828
+ Q as filesToAttachments,
8829
+ R as getAllRenderers,
8607
8830
  getSegmentRenderer,
8608
8831
  installDefaultRenderersOnce,
8609
8832
  installDialogTriggersOnce,
8610
8833
  isAwaitingReply,
8611
- T as isFormatAdapter,
8834
+ W as isFormatAdapter,
8612
8835
  isSafeUrl,
8613
8836
  isSegmentSettled,
8614
- W as parseAparteEventStream,
8837
+ Z as overrideSegmentRenderer,
8838
+ _ as parseAparteEventStream,
8615
8839
  parseMarkdownToSegments,
8616
8840
  populateBubbleFromMessage,
8617
- Z as readableToAsyncIterable,
8841
+ a1 as readableToAsyncIterable,
8618
8842
  registerAllComponents,
8619
- _ as registerDefaultRenderers,
8620
- $ as registerSegmentRenderer,
8621
- a0 as requestUserInput,
8843
+ a2 as registerDefaultRenderers,
8844
+ a3 as registerSegmentRenderer,
8845
+ a4 as releaseDeferredHydration,
8846
+ a5 as requestUserInput,
8622
8847
  resolveConfig,
8623
8848
  revokeAttachmentUrls,
8624
8849
  runWithConfig,
8625
- a4 as segmentDuration,
8626
- a5 as segmentTiming,
8850
+ a9 as segmentDuration,
8851
+ aa as segmentTiming,
8627
8852
  subscribeConfigChange,
8628
- a7 as toolRefusal,
8629
- a8 as unregisterSegmentRenderer,
8853
+ ac as toToolCallView,
8854
+ ad as toolRefusal,
8855
+ ae as unregisterSegmentRenderer,
8630
8856
  uuid
8631
8857
  };
8632
8858
  //# sourceMappingURL=index.js.map