@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
@@ -1,5 +1,5 @@
1
1
  import { toolRefusal as toolRefusal$1, unansweredTail, runStreamAgent } from "@aparte/engine";
2
- import { A as APARTE_ICON_GLYPHS } from "./glyphs-C8z2oJUg.js";
2
+ import { A as APARTE_ICON_GLYPHS } from "./glyphs-2bym5jl5.js";
3
3
  function contentToText(content) {
4
4
  if (typeof content === "string") return content;
5
5
  return content.filter((p) => p.type === "text").map((p) => p.text).join("");
@@ -950,13 +950,13 @@ function stampSegmentOnInsert(segments, segment, messageId, defaults) {
950
950
  ...timingPatch(base, { startedAt: segmentTiming(segment)?.startedAt ?? Date.now() })
951
951
  };
952
952
  }
953
- function adoptSegment(segments, segment, messageId, live = false) {
953
+ function adoptSegment(segments, segment, messageId, live2 = false) {
954
954
  return {
955
955
  ...segment,
956
956
  messageId,
957
957
  index: segments.length,
958
958
  isStreaming: false,
959
- ...segment.type === "tool_call" && (segment.status === "awaiting-approval" || segment.status === "pending" && !live) && !segment.deferred?.ticket ? { status: "aborted" } : {}
959
+ ...segment.type === "tool_call" && (segment.status === "awaiting-approval" || segment.status === "pending" && !live2) && !segment.deferred?.ticket ? { status: "aborted" } : {}
960
960
  };
961
961
  }
962
962
  const RESERVED = /* @__PURE__ */ new Set(["id", "type", "messageId", "index"]);
@@ -1030,11 +1030,11 @@ function openSegmentIds(segments) {
1030
1030
  }
1031
1031
  function adoptMessageSegments(message) {
1032
1032
  if (!message.segments?.length) return { ...message };
1033
- const live = message.status === "streaming" || message.status === "pending";
1033
+ const live2 = message.status === "streaming" || message.status === "pending";
1034
1034
  return {
1035
1035
  ...message,
1036
1036
  segments: message.segments.reduce((acc, segment) => {
1037
- acc.push(adoptSegment(acc, segment, message.id, live));
1037
+ acc.push(adoptSegment(acc, segment, message.id, live2));
1038
1038
  return acc;
1039
1039
  }, [])
1040
1040
  };
@@ -1070,8 +1070,8 @@ function detachConfig(el2) {
1070
1070
  if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
1071
1071
  }
1072
1072
  function resolveConfig(el2) {
1073
- const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
1074
- return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
1073
+ const host2 = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
1074
+ return host2?.[CONFIG_SLOT] ?? aparteGlobalConfig;
1075
1075
  }
1076
1076
  let _renderConfig = null;
1077
1077
  function runWithConfig(config, fn) {
@@ -1928,6 +1928,33 @@ function renderingVersionOf(cfg) {
1928
1928
  function bumpRenderingVersion(cfg) {
1929
1929
  renderingVersions.set(cfg, renderingVersionOf(cfg) + 1);
1930
1930
  }
1931
+ const rendererListeners = /* @__PURE__ */ new Set();
1932
+ const pendingRendererChanges = /* @__PURE__ */ new Set();
1933
+ function onRenderersChange(listener) {
1934
+ rendererListeners.add(listener);
1935
+ return () => {
1936
+ rendererListeners.delete(listener);
1937
+ };
1938
+ }
1939
+ function notifyRenderersChanged(cfg) {
1940
+ if (pendingRendererChanges.size === 0) {
1941
+ queueMicrotask(() => {
1942
+ const changed = [...pendingRendererChanges];
1943
+ pendingRendererChanges.clear();
1944
+ for (const config of changed) {
1945
+ for (const listener of [...rendererListeners]) {
1946
+ try {
1947
+ listener(config);
1948
+ } catch (err) {
1949
+ console.error("[aparte] a renderer-change listener threw", err);
1950
+ }
1951
+ }
1952
+ }
1953
+ });
1954
+ }
1955
+ pendingRendererChanges.add(cfg);
1956
+ }
1957
+ const matcherKey = (match) => match instanceof RegExp ? `re:${String(match)}` : `name:${match}`;
1931
1958
  const ELEMENT_SHAPING = ["hidden", "open", "details", "quiet"];
1932
1959
  function drawingOf(provider, name) {
1933
1960
  if (!provider || !(name in provider) || name in Object.prototype) return void 0;
@@ -2039,6 +2066,11 @@ class AparteConfig {
2039
2066
  _tools = /* @__PURE__ */ new Map();
2040
2067
  // By exact name, pattern or '*' — see `tool-table.ts` for the one resolution rule.
2041
2068
  _toolRenderers = new ToolTable();
2069
+ /** {@link overrideToolRenderer}'s stacks: what each matcher held below every override. */
2070
+ /** Per matcher: what it held below every override, then one entry per override — an object of its own, so two overrides of the same renderer are told apart. */
2071
+ _toolRendererOverrides = /* @__PURE__ */ new Map();
2072
+ /** Bumped by `reset()`: a restore function from before it must not undo what came after. */
2073
+ _overrideGeneration = 0;
2042
2074
  _toolDisplays = new ToolTable();
2043
2075
  /** Tagged blocks the stream parser recognises in the prose — see {@link registerStreamBlock}. */
2044
2076
  _streamBlocks = /* @__PURE__ */ new Map();
@@ -2880,12 +2912,64 @@ class AparteConfig {
2880
2912
  * // Custom pill for a web-search tool
2881
2913
  * aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="aparte-tool-label">Searching...</div>` });
2882
2914
  *
2915
+ * Calls already on screen follow: those this registration now draws are drawn again
2916
+ * by it (the renderer that drew them before gets its `cleanup`), and nothing else on
2917
+ * the page is touched.
2918
+ *
2883
2919
  * @example
2884
2920
  * // One look for every tool an MCP server brings
2885
2921
  * aparteGlobalConfig.registerToolRenderer(/^mcp__/, { render: (seg) => myMcpCard(seg) });
2886
2922
  */
2887
2923
  registerToolRenderer(match, renderer) {
2924
+ if (this._toolRenderers.get(match) === renderer) return;
2888
2925
  this._toolRenderers.set(match, renderer);
2926
+ notifyRenderersChanged(this);
2927
+ }
2928
+ /**
2929
+ * Put `renderer` over whatever this matcher holds, and get back the function that takes
2930
+ * it off again — the registration a component makes for as long as it is mounted.
2931
+ *
2932
+ * Overrides stack per matcher. The function restores what was there before THIS
2933
+ * override, and taking off an override that is no longer on top changes nothing on
2934
+ * screen: two components declaring the same tool, the newer wins, and unmounting the
2935
+ * older does not take the newer's away. When the last one goes, the matcher holds what
2936
+ * it held before the first — another registration, or nothing. Calls on screen follow
2937
+ * each change, as with {@link registerToolRenderer}. The returned function is safe to
2938
+ * call twice.
2939
+ *
2940
+ * @example
2941
+ * const restore = aparteGlobalConfig.overrideToolRenderer('get_weather', weatherCard);
2942
+ * // …later, when the component that declared it unmounts:
2943
+ * restore();
2944
+ */
2945
+ overrideToolRenderer(match, renderer) {
2946
+ const key = matcherKey(match);
2947
+ const current = this._toolRenderers.get(match);
2948
+ let stack = this._toolRendererOverrides.get(key);
2949
+ if (!stack) {
2950
+ stack = [{ renderer: current }];
2951
+ this._toolRendererOverrides.set(key, stack);
2952
+ } else if (stack[stack.length - 1].renderer !== current) {
2953
+ stack.splice(0, stack.length, { renderer: current });
2954
+ }
2955
+ const entry = { renderer };
2956
+ stack.push(entry);
2957
+ this.registerToolRenderer(match, renderer);
2958
+ const generation = this._overrideGeneration;
2959
+ let restored = false;
2960
+ return () => {
2961
+ if (restored || generation !== this._overrideGeneration) return;
2962
+ restored = true;
2963
+ const i = stack.indexOf(entry);
2964
+ if (i <= 0) return;
2965
+ const wasTop = i === stack.length - 1;
2966
+ stack.splice(i, 1);
2967
+ if (stack.length === 1 && this._toolRendererOverrides.get(key) === stack) this._toolRendererOverrides.delete(key);
2968
+ if (!wasTop || this._toolRenderers.get(match) !== renderer) return;
2969
+ const top = stack[stack.length - 1].renderer;
2970
+ if (top) this.registerToolRenderer(match, top);
2971
+ else this.unregisterToolRenderer(match);
2972
+ };
2889
2973
  }
2890
2974
  /**
2891
2975
  * Field defaults for a segment TYPE, filled in when a segment enters a message.
@@ -2925,9 +3009,11 @@ class AparteConfig {
2925
3009
  clearSegmentDefaults(type) {
2926
3010
  this._segmentDefaults.delete(type);
2927
3011
  }
2928
- /** Unregister the renderer registered under this matcher. */
3012
+ /** Unregister the renderer registered under this matcher. Calls on screen it drew go back to core's row. */
2929
3013
  unregisterToolRenderer(match) {
3014
+ if (this._toolRenderers.get(match) === void 0) return;
2930
3015
  this._toolRenderers.delete(match);
3016
+ notifyRenderersChanged(this);
2931
3017
  }
2932
3018
  /** The renderer that draws a tool named `toolName` — exact, pattern or `'*'` — or undefined. */
2933
3019
  getToolRenderer(toolName) {
@@ -3278,6 +3364,9 @@ class AparteConfig {
3278
3364
  this._fetchedModels.clear();
3279
3365
  this._tools.clear();
3280
3366
  this._toolRenderers.clear();
3367
+ this._toolRendererOverrides.clear();
3368
+ this._overrideGeneration++;
3369
+ notifyRenderersChanged(this);
3281
3370
  this._toolDisplays.clear();
3282
3371
  this._streamBlocks.clear();
3283
3372
  this._modelConfig = {};
@@ -3329,19 +3418,19 @@ const ONE_SHOT_PROBE = "<i>a</i>";
3329
3418
  function hasOneShotMarkdownProvider() {
3330
3419
  return contextConfig().renderMarkdown(ONE_SHOT_PROBE) !== escapeHtml(ONE_SHOT_PROBE);
3331
3420
  }
3332
- function writeStreamedMarkdown(host, contentEl, content, streaming) {
3421
+ function writeStreamedMarkdown(host2, contentEl, content, streaming) {
3333
3422
  if (streaming) {
3334
- if (host._aparteSmd === void 0) {
3423
+ if (host2._aparteSmd === void 0) {
3335
3424
  settledFromParser.delete(contentEl);
3336
3425
  const renderer = contextConfig().createStreamingMarkdownRenderer(contentEl);
3337
3426
  if (renderer) {
3338
3427
  contentEl.textContent = "";
3339
- host._aparteSmd = { renderer, written: 0 };
3428
+ host2._aparteSmd = { renderer, written: 0 };
3340
3429
  } else {
3341
- host._aparteSmd = null;
3430
+ host2._aparteSmd = null;
3342
3431
  }
3343
3432
  }
3344
- const smd = host._aparteSmd;
3433
+ const smd = host2._aparteSmd;
3345
3434
  if (smd) {
3346
3435
  const delta = content.slice(smd.written);
3347
3436
  if (delta) {
@@ -3350,15 +3439,15 @@ function writeStreamedMarkdown(host, contentEl, content, streaming) {
3350
3439
  }
3351
3440
  return;
3352
3441
  }
3353
- } else if (host._aparteSmd) {
3354
- const smd = host._aparteSmd;
3442
+ } else if (host2._aparteSmd) {
3443
+ const smd = host2._aparteSmd;
3355
3444
  const tail = content.slice(smd.written);
3356
3445
  if (tail) {
3357
3446
  smd.renderer.write(tail);
3358
3447
  smd.written = content.length;
3359
3448
  }
3360
3449
  smd.renderer.end();
3361
- host._aparteSmd = void 0;
3450
+ host2._aparteSmd = void 0;
3362
3451
  if (!hasOneShotMarkdownProvider()) {
3363
3452
  contentEl.innerHTML = contextConfig().sanitizeHtml(contentEl.innerHTML);
3364
3453
  settledFromParser.set(contentEl, content);
@@ -3587,16 +3676,16 @@ function streamHighlight(element, paneSelector, content, lang, segId) {
3587
3676
  if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;
3588
3677
  markThrottle(_lastHighlightAt, segId, now);
3589
3678
  void contextConfig(element).highlightCode(content.slice(0, cut), lang).then((html) => {
3590
- const live = element.querySelector(paneSelector);
3591
- if (!live) return;
3592
- if (cut <= highlightedLen(live)) return;
3593
- live.innerHTML = html;
3594
- const codeEl = live.querySelector("code") ?? live;
3679
+ const live2 = element.querySelector(paneSelector);
3680
+ if (!live2) return;
3681
+ if (cut <= highlightedLen(live2)) return;
3682
+ live2.innerHTML = html;
3683
+ const codeEl = live2.querySelector("code") ?? live2;
3595
3684
  const span = document.createElement("span");
3596
3685
  span.dataset.aparteTail = "";
3597
3686
  span.textContent = content.slice(cut);
3598
3687
  codeEl.appendChild(span);
3599
- live.dataset.aparteHlLen = String(cut);
3688
+ live2.dataset.aparteHlLen = String(cut);
3600
3689
  }).catch(() => {
3601
3690
  });
3602
3691
  }
@@ -3820,9 +3909,9 @@ function sameShape(a, b) {
3820
3909
  }
3821
3910
  return true;
3822
3911
  }
3823
- function patchChanges(live, prev, next) {
3824
- for (let i = 0; i < live.childNodes.length; i++) {
3825
- const l = live.childNodes[i];
3912
+ function patchChanges(live2, prev, next) {
3913
+ for (let i = 0; i < live2.childNodes.length; i++) {
3914
+ const l = live2.childNodes[i];
3826
3915
  const p = prev.childNodes[i];
3827
3916
  const n = next.childNodes[i];
3828
3917
  if (l instanceof Element && p instanceof Element && n instanceof Element) {
@@ -3989,16 +4078,26 @@ function placeholderMarkup(segment) {
3989
4078
  return `<div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(segment.status ?? "pending")}" data-tool-call-id="${escapeAttr(segment.toolCall?.id ?? "")}" data-tool-name="${escapeAttr(segment.toolCall?.name ?? "tool")}" hidden></div>`;
3990
4079
  }
3991
4080
  const isPlaceholder = (element) => element.hidden && element.classList.contains("aparte-segment-tool-call") && element.childElementCount === 0;
3992
- function drawDefault(segment, behind) {
4081
+ const drawnBy = /* @__PURE__ */ new WeakMap();
4082
+ const behind = /* @__PURE__ */ new WeakMap();
4083
+ function toolRendererThatDrew(element) {
4084
+ return behind.get(element) ?? null;
4085
+ }
4086
+ function drawerOf(element, current) {
4087
+ const recorded = drawnBy.get(element);
4088
+ if (recorded) return recorded;
4089
+ return element.classList.contains("aparte-segment-tool-call") ? null : current;
4090
+ }
4091
+ function drawDefault(segment, behind2) {
3993
4092
  const name = segment.toolCall?.name ?? "tool";
3994
4093
  const status = segment.status ?? "pending";
3995
4094
  const toolCallId = segment.toolCall?.id ?? "";
3996
4095
  const pill = pillMarkup(segment);
3997
4096
  const quiet = isQuiet(segment) ? " data-quiet" : "";
3998
4097
  const toggle = contextConfig().getIcon("expand");
3999
- const input = behind.kind === "parts" ? describeInput(segment) : "";
4000
- const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
4001
- if (behind.kind === "none" || behind.kind === "parts" && !input && !outcome) {
4098
+ const input = behind2.kind === "parts" ? describeInput(segment) : "";
4099
+ const outcome = behind2.kind === "parts" ? outcomeOf(segment) : null;
4100
+ if (behind2.kind === "none" || behind2.kind === "parts" && !input && !outcome) {
4002
4101
  return `
4003
4102
  <div class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}><div class="aparte-tool-line">${pill}</div>
4004
4103
  </div>
@@ -4007,7 +4106,7 @@ function drawDefault(segment, behind) {
4007
4106
  const cfg = contextConfig();
4008
4107
  const copy = copyButtonMarkup("aparte-tool-copy");
4009
4108
  const open = segment.collapsed !== void 0 ? !segment.collapsed : cfg.getToolDisplay(name)?.open === true;
4010
- if (behind.kind === "body") {
4109
+ if (behind2.kind === "body") {
4011
4110
  const tpl = document.createElement("template");
4012
4111
  tpl.innerHTML = `
4013
4112
  <details class="aparte-segment aparte-segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}" data-tool-call-id="${escapeAttr(toolCallId)}" data-tool-name="${escapeAttr(name)}"${quiet}${open ? " open" : ""}>
@@ -4015,7 +4114,7 @@ function drawDefault(segment, behind) {
4015
4114
  <div class="aparte-tool-detail" data-details="app"></div>
4016
4115
  </details>`.trim();
4017
4116
  const row = tpl.content.firstElementChild;
4018
- setBody(row, behind.body);
4117
+ setBody(row, behind2.body);
4019
4118
  return row;
4020
4119
  }
4021
4120
  return `
@@ -4044,7 +4143,12 @@ const toolCallRenderer = {
4044
4143
  const customRenderer = contextConfig().getToolRenderer(name);
4045
4144
  if (customRenderer) {
4046
4145
  injectToolRendererStyles(name, customRenderer);
4047
- return customRenderer.render(segment) || placeholderMarkup(segment);
4146
+ const out = customRenderer.render(segment);
4147
+ if (out instanceof HTMLElement) {
4148
+ drawnBy.set(out, customRenderer);
4149
+ behind.set(out, customRenderer);
4150
+ }
4151
+ return out || placeholderMarkup(segment);
4048
4152
  }
4049
4153
  return drawDefault(segment, behindOf(segment));
4050
4154
  },
@@ -4061,7 +4165,7 @@ const toolCallRenderer = {
4061
4165
  relabel: (element, segment) => {
4062
4166
  if (isPlaceholder(element)) return;
4063
4167
  const cfg = contextConfig();
4064
- const custom = cfg.getToolRenderer(segment.toolCall?.name);
4168
+ const custom = drawerOf(element, cfg.getToolRenderer(segment.toolCall?.name) ?? null);
4065
4169
  if (custom) {
4066
4170
  custom.relabel?.(element, segment);
4067
4171
  return;
@@ -4081,8 +4185,8 @@ const toolCallRenderer = {
4081
4185
  for (const button of element.querySelectorAll(".aparte-tool-copy")) relabelCopyButton(button);
4082
4186
  const appDetail = appDetailOf(element);
4083
4187
  if (appDetail) {
4084
- const behind = behindOf(segment);
4085
- if (behind.kind === "body") relabelBody(appDetail, behind.body);
4188
+ const behind2 = behindOf(segment);
4189
+ if (behind2.kind === "body") relabelBody(appDetail, behind2.body);
4086
4190
  }
4087
4191
  const statusEl = element.querySelector(".aparte-tool-state");
4088
4192
  if (statusEl) statusEl.innerHTML = stateBadge(segment);
@@ -4108,9 +4212,10 @@ const toolCallRenderer = {
4108
4212
  element.setAttribute("data-status", status);
4109
4213
  return;
4110
4214
  }
4111
- const custom = contextConfig().getToolRenderer(segment.toolCall?.name);
4112
- if (custom?.update && !hidden && !isPlaceholder(element)) {
4113
- custom.update(element, segment);
4215
+ const custom = hidden ? null : contextConfig().getToolRenderer(segment.toolCall?.name) ?? null;
4216
+ const drawn = isPlaceholder(element) ? null : drawerOf(element, custom);
4217
+ if (drawn && drawn === custom && drawn.update && !isPlaceholder(element)) {
4218
+ drawn.update(element, segment);
4114
4219
  return;
4115
4220
  }
4116
4221
  const rebuild = (out) => {
@@ -4119,21 +4224,22 @@ const toolCallRenderer = {
4119
4224
  const rebuilt = typeof out === "string" ? tpl.content.firstElementChild : out;
4120
4225
  if (rebuilt) {
4121
4226
  if (!rebuilt.hasAttribute("data-segment-id")) rebuilt.setAttribute("data-segment-id", segment.id);
4227
+ toolCallRenderer.cleanup?.(element, segment);
4122
4228
  element.replaceWith(rebuilt);
4123
4229
  toolCallRenderer.setup?.(rebuilt, segment);
4124
4230
  }
4125
4231
  };
4126
- if (custom || hidden || isPlaceholder(element)) {
4232
+ if (custom || drawn || hidden || isPlaceholder(element)) {
4127
4233
  rebuild(toolCallRenderer.render(segment));
4128
4234
  return;
4129
4235
  }
4130
- const behind = behindOf(segment);
4131
- const input = behind.kind === "parts" ? describeInput(segment) : "";
4132
- const outcome = behind.kind === "parts" ? outcomeOf(segment) : null;
4133
- const wantsDetail = behind.kind === "body" || !!input || !!outcome;
4236
+ const behind2 = behindOf(segment);
4237
+ const input = behind2.kind === "parts" ? describeInput(segment) : "";
4238
+ const outcome = behind2.kind === "parts" ? outcomeOf(segment) : null;
4239
+ const wantsDetail = behind2.kind === "body" || !!input || !!outcome;
4134
4240
  const holdsAppDetail = !!appDetailOf(element);
4135
- if (wantsDetail !== (element.tagName === "DETAILS") || wantsDetail && behind.kind === "body" !== holdsAppDetail) {
4136
- rebuild(drawDefault(segment, behind));
4241
+ if (wantsDetail !== (element.tagName === "DETAILS") || wantsDetail && behind2.kind === "body" !== holdsAppDetail) {
4242
+ rebuild(drawDefault(segment, behind2));
4137
4243
  return;
4138
4244
  }
4139
4245
  element.setAttribute("data-status", status);
@@ -4143,8 +4249,8 @@ const toolCallRenderer = {
4143
4249
  setName(element, segment);
4144
4250
  setTarget(element, segment);
4145
4251
  setNote(element, segment);
4146
- if (behind.kind === "body") {
4147
- setBody(element, behind.body);
4252
+ if (behind2.kind === "body") {
4253
+ setBody(element, behind2.body);
4148
4254
  return;
4149
4255
  }
4150
4256
  setPart(element, "input", input, "toolInput");
@@ -4152,12 +4258,31 @@ const toolCallRenderer = {
4152
4258
  highlightParts(element);
4153
4259
  },
4154
4260
  setup: (element, segment) => {
4261
+ const hidden = contextConfig().isToolHidden(segment.toolCall?.name ?? "tool");
4262
+ const customRenderer = hidden ? void 0 : contextConfig().getToolRenderer(segment.toolCall?.name);
4263
+ if (customRenderer) behind.set(element, customRenderer);
4155
4264
  if (isPlaceholder(element)) return;
4156
4265
  wireParts(element);
4157
4266
  highlightParts(element);
4158
- const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
4267
+ if (customRenderer) drawnBy.set(element, customRenderer);
4159
4268
  customRenderer?.setup?.(element, segment);
4160
4269
  },
4270
+ /**
4271
+ * The row leaves for good: the registered renderer that drew it, if any, cleans up. A
4272
+ * cleanup that throws is reported and goes no further: the row is replaced or removed
4273
+ * all the same, and the stream that caused it is not interrupted.
4274
+ */
4275
+ cleanup: (element, segment) => {
4276
+ behind.delete(element);
4277
+ const custom = drawnBy.get(element);
4278
+ if (!custom) return;
4279
+ drawnBy.delete(element);
4280
+ try {
4281
+ custom.cleanup?.(element, segment);
4282
+ } catch (err) {
4283
+ console.error("[aparte] a tool renderer's cleanup threw", err);
4284
+ }
4285
+ },
4161
4286
  // CSS lives in styles/segment/ — see that folder for why a
4162
4287
  // built-in's rules belong there. `getStyles` stays for a CONSUMER's renderer, which
4163
4288
  // has no other way onto the page.
@@ -4181,8 +4306,51 @@ function effectiveRenderers(config) {
4181
4306
  }
4182
4307
  let styleElement = null;
4183
4308
  function registerSegmentRenderer(renderer, config = contextConfig()) {
4184
- registryFor(config).renderers.set(renderer.type, renderer);
4309
+ const reg = registryFor(config);
4310
+ if (reg.renderers.get(renderer.type) === renderer) return;
4311
+ reg.renderers.set(renderer.type, renderer);
4185
4312
  injectRendererStyles(config);
4313
+ notifyRenderersChanged(config);
4314
+ }
4315
+ const overrideStacks = /* @__PURE__ */ new WeakMap();
4316
+ function overrideSegmentRenderer(renderer, config = contextConfig()) {
4317
+ const type = renderer.type;
4318
+ const reg = registryFor(config);
4319
+ let byType = overrideStacks.get(config);
4320
+ if (!byType) {
4321
+ byType = /* @__PURE__ */ new Map();
4322
+ overrideStacks.set(config, byType);
4323
+ }
4324
+ const current = reg.renderers.get(type);
4325
+ let stack = byType.get(type);
4326
+ if (!stack) {
4327
+ stack = [{ renderer: current }];
4328
+ byType.set(type, stack);
4329
+ } else if (stack[stack.length - 1].renderer !== current) stack.splice(0, stack.length, { renderer: current });
4330
+ const entry = { renderer };
4331
+ stack.push(entry);
4332
+ registerSegmentRenderer(renderer, config);
4333
+ let restored = false;
4334
+ return () => {
4335
+ if (restored) return;
4336
+ restored = true;
4337
+ const i = stack.indexOf(entry);
4338
+ if (i <= 0) return;
4339
+ const wasTop = i === stack.length - 1;
4340
+ stack.splice(i, 1);
4341
+ if (stack.length === 1 && byType.get(type) === stack) byType.delete(type);
4342
+ if (!wasTop || reg.renderers.get(type) !== renderer) return;
4343
+ const top = stack[stack.length - 1].renderer;
4344
+ if (top) {
4345
+ registerSegmentRenderer(top, config);
4346
+ return;
4347
+ }
4348
+ unregisterSegmentRenderer(type, config);
4349
+ if (!effectiveRenderers(config).has(type) && reg.defaultsInstalled && !reg.defaultsDeclined) {
4350
+ const builtIn = DEFAULT_RENDERERS.find((r) => r.type === type);
4351
+ if (builtIn) registerSegmentRenderer(builtIn, config);
4352
+ }
4353
+ };
4186
4354
  }
4187
4355
  function declineDefaultRenderers(config = contextConfig()) {
4188
4356
  registryFor(config).defaultsDeclined = true;
@@ -4197,7 +4365,10 @@ function installDefaultRenderersOnce(config = contextConfig()) {
4197
4365
  injectRendererStyles(config);
4198
4366
  }
4199
4367
  function unregisterSegmentRenderer(type, config = contextConfig()) {
4200
- registryFor(config).renderers.delete(type);
4368
+ const reg = registryFor(config);
4369
+ if (!reg.renderers.has(type)) return;
4370
+ reg.renderers.delete(type);
4371
+ notifyRenderersChanged(config);
4201
4372
  }
4202
4373
  function getSegmentRenderer(type, config = contextConfig()) {
4203
4374
  return effectiveRenderers(config).get(type);
@@ -4257,6 +4428,71 @@ const DEFAULT_RENDERERS = [
4257
4428
  errorRenderer,
4258
4429
  toolCallRenderer
4259
4430
  ];
4431
+ const mountedRenderers = /* @__PURE__ */ new WeakSet();
4432
+ function isMountedRenderer(renderer) {
4433
+ return !!renderer && mountedRenderers.has(renderer);
4434
+ }
4435
+ function toToolCallView(segment) {
4436
+ return {
4437
+ id: segment.toolCall?.id ?? segment.id,
4438
+ name: segment.toolCall?.name ?? "tool",
4439
+ input: segment.toolCall?.input,
4440
+ status: segment.status ?? "pending",
4441
+ result: segment.result,
4442
+ structuredResult: segment.structuredResult,
4443
+ deferred: segment.deferred,
4444
+ refusal: segment.refusal,
4445
+ abortCause: segment.abortCause,
4446
+ segment
4447
+ };
4448
+ }
4449
+ function host(className, segmentId) {
4450
+ const el2 = document.createElement("div");
4451
+ el2.className = className;
4452
+ el2.setAttribute("data-segment-id", segmentId);
4453
+ return el2;
4454
+ }
4455
+ function defineMountedToolRenderer(mount) {
4456
+ const mounted = /* @__PURE__ */ new WeakMap();
4457
+ const renderer = {
4458
+ render: (segment) => host("aparte-tool-ui", segment.id),
4459
+ setup: (el2, segment) => {
4460
+ if (!mounted.has(el2)) mounted.set(el2, mount(el2, toToolCallView(segment)));
4461
+ },
4462
+ update: (el2, segment) => {
4463
+ mounted.get(el2)?.update(toToolCallView(segment));
4464
+ },
4465
+ cleanup: (el2) => {
4466
+ const view = mounted.get(el2);
4467
+ if (!view) return;
4468
+ mounted.delete(el2);
4469
+ view.destroy();
4470
+ }
4471
+ };
4472
+ mountedRenderers.add(renderer);
4473
+ return renderer;
4474
+ }
4475
+ function defineMountedSegmentRenderer(type, mount) {
4476
+ const mounted = /* @__PURE__ */ new WeakMap();
4477
+ const renderer = {
4478
+ type,
4479
+ render: (segment) => host("aparte-segment-ui", segment.id),
4480
+ setup: (el2, segment) => {
4481
+ if (!mounted.has(el2)) mounted.set(el2, mount(el2, segment));
4482
+ },
4483
+ update: (el2, segment) => {
4484
+ mounted.get(el2)?.update(segment);
4485
+ },
4486
+ cleanup: (el2) => {
4487
+ const view = mounted.get(el2);
4488
+ if (!view) return;
4489
+ mounted.delete(el2);
4490
+ view.destroy();
4491
+ }
4492
+ };
4493
+ mountedRenderers.add(renderer);
4494
+ return renderer;
4495
+ }
4260
4496
  function populateBubbleFromMessage(bubble, message, siblingInfo) {
4261
4497
  if (message.segments && message.segments.length > 0) {
4262
4498
  const existing = bubble.getSegments?.() ?? [];
@@ -4902,7 +5138,7 @@ class AparteConversationController {
4902
5138
  * Attach event listeners to the binding host. Returns an unbind function.
4903
5139
  */
4904
5140
  bind() {
4905
- const host = this._binding.host;
5141
+ const host2 = this._binding.host;
4906
5142
  this._onSendCapture = (e) => {
4907
5143
  const evt = e;
4908
5144
  const targetId = evt.detail?.targetId;
@@ -4921,13 +5157,13 @@ class AparteConversationController {
4921
5157
  evt.detail.echoed = true;
4922
5158
  void this._ensureConversationAndPersist(userMsg);
4923
5159
  };
4924
- host.addEventListener("aparte-send", this._onSendCapture, { capture: true });
5160
+ host2.addEventListener("aparte-send", this._onSendCapture, { capture: true });
4925
5161
  this._onPathChanged = () => {
4926
5162
  if (!this._isLoadingConversation) {
4927
5163
  this._persistActive();
4928
5164
  }
4929
5165
  };
4930
- host.addEventListener("aparte-path-changed", this._onPathChanged);
5166
+ host2.addEventListener("aparte-path-changed", this._onPathChanged);
4931
5167
  this._onMessageStart = (e) => {
4932
5168
  this._isStreaming = true;
4933
5169
  this._turnMessage = e.detail?.messageId ?? null;
@@ -4958,12 +5194,12 @@ class AparteConversationController {
4958
5194
  this._onMessageResumed = (e) => {
4959
5195
  if (this._ofThisTurn(e)) this._setTurn("running");
4960
5196
  };
4961
- host.addEventListener("aparte-message-start", this._onMessageStart);
4962
- host.addEventListener("aparte-message-done", this._onMessageDone);
4963
- host.addEventListener("aparte-message-error", this._onMessageError);
4964
- host.addEventListener("aparte-message-aborted", this._onMessageAborted);
4965
- host.addEventListener("aparte-message-paused", this._onMessagePaused);
4966
- host.addEventListener("aparte-message-resumed", this._onMessageResumed);
5197
+ host2.addEventListener("aparte-message-start", this._onMessageStart);
5198
+ host2.addEventListener("aparte-message-done", this._onMessageDone);
5199
+ host2.addEventListener("aparte-message-error", this._onMessageError);
5200
+ host2.addEventListener("aparte-message-aborted", this._onMessageAborted);
5201
+ host2.addEventListener("aparte-message-paused", this._onMessagePaused);
5202
+ host2.addEventListener("aparte-message-resumed", this._onMessageResumed);
4967
5203
  this._onSelectConversation = (e) => {
4968
5204
  const evt = e;
4969
5205
  const targetId = evt.detail?.targetId;
@@ -4995,37 +5231,37 @@ class AparteConversationController {
4995
5231
  this._turnMessage = null;
4996
5232
  this._stopHydrationRetry?.();
4997
5233
  this._stopHydrationRetry = null;
4998
- const host = this._binding.host;
5234
+ const host2 = this._binding.host;
4999
5235
  if (this._onSendCapture) {
5000
- host.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
5236
+ host2.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
5001
5237
  this._onSendCapture = null;
5002
5238
  }
5003
5239
  if (this._onPathChanged) {
5004
- host.removeEventListener("aparte-path-changed", this._onPathChanged);
5240
+ host2.removeEventListener("aparte-path-changed", this._onPathChanged);
5005
5241
  this._onPathChanged = null;
5006
5242
  }
5007
5243
  if (this._onMessageStart) {
5008
- host.removeEventListener("aparte-message-start", this._onMessageStart);
5244
+ host2.removeEventListener("aparte-message-start", this._onMessageStart);
5009
5245
  this._onMessageStart = null;
5010
5246
  }
5011
5247
  if (this._onMessageDone) {
5012
- host.removeEventListener("aparte-message-done", this._onMessageDone);
5248
+ host2.removeEventListener("aparte-message-done", this._onMessageDone);
5013
5249
  this._onMessageDone = null;
5014
5250
  }
5015
5251
  if (this._onMessageError) {
5016
- host.removeEventListener("aparte-message-error", this._onMessageError);
5252
+ host2.removeEventListener("aparte-message-error", this._onMessageError);
5017
5253
  this._onMessageError = null;
5018
5254
  }
5019
5255
  if (this._onMessageAborted) {
5020
- host.removeEventListener("aparte-message-aborted", this._onMessageAborted);
5256
+ host2.removeEventListener("aparte-message-aborted", this._onMessageAborted);
5021
5257
  this._onMessageAborted = null;
5022
5258
  }
5023
5259
  if (this._onMessagePaused) {
5024
- host.removeEventListener("aparte-message-paused", this._onMessagePaused);
5260
+ host2.removeEventListener("aparte-message-paused", this._onMessagePaused);
5025
5261
  this._onMessagePaused = null;
5026
5262
  }
5027
5263
  if (this._onMessageResumed) {
5028
- host.removeEventListener("aparte-message-resumed", this._onMessageResumed);
5264
+ host2.removeEventListener("aparte-message-resumed", this._onMessageResumed);
5029
5265
  this._onMessageResumed = null;
5030
5266
  }
5031
5267
  if (this._onCompactDone && typeof window !== "undefined") {
@@ -5302,6 +5538,12 @@ class AparteChatHost {
5302
5538
  _tokenBuffer = null;
5303
5539
  /** Handle of the scheduled coalesced sync, or null when none is pending. */
5304
5540
  _flushHandle = null;
5541
+ /**
5542
+ * Lists reported through `onMessagesChange` that the parent has not handed back yet, oldest
5543
+ * first — what {@link takeEcho} recognises. Bounded: a parent that never hands a list back
5544
+ * (an uncontrolled chat) must not make it grow for the life of the page.
5545
+ */
5546
+ _pendingEchoes = [];
5305
5547
  _conversationId = null;
5306
5548
  _controller;
5307
5549
  _teardown = [];
@@ -5311,6 +5553,34 @@ class AparteChatHost {
5311
5553
  this.options = options;
5312
5554
  this._conversationId = options.conversationId ?? null;
5313
5555
  }
5556
+ /** Every report goes through here, so {@link takeEcho} knows what the parent may hand back. */
5557
+ _report(next) {
5558
+ this._pendingEchoes.push(next);
5559
+ if (this._pendingEchoes.length > 64) this._pendingEchoes.shift();
5560
+ this.binding.onMessagesChange?.(next);
5561
+ }
5562
+ /**
5563
+ * Whether a list the parent hands back is this host's own report coming back — the echo of
5564
+ * `onMessagesChange` through a two-way binding (`bind:messages`, `v-model:messages`,
5565
+ * `[(messages)]`, a controlled React prop). A wrapper asks before applying the list it is
5566
+ * given, because applying an echo rolls back what the host did since without reporting (an
5567
+ * optimistic append).
5568
+ *
5569
+ * True for a list reported and not handed back yet, however late it lands — a parent whose
5570
+ * render comes after the host's next write hands back an EARLIER report — and that list and
5571
+ * every older one are taken. False for any other list, the parent's own word, which clears
5572
+ * them: a list the parent restores later (a reopened conversation, an undo) is applied,
5573
+ * where remembering every list ever reported swallowed it.
5574
+ */
5575
+ takeEcho(list) {
5576
+ const i = this._pendingEchoes.indexOf(list);
5577
+ if (i < 0) {
5578
+ this._pendingEchoes.length = 0;
5579
+ return false;
5580
+ }
5581
+ this._pendingEchoes.splice(0, i + 1);
5582
+ return true;
5583
+ }
5314
5584
  // ── lifecycle ──────────────────────────────────────────────────────────
5315
5585
  /**
5316
5586
  * Install the host-method surface + event listeners and create the
@@ -5320,19 +5590,19 @@ class AparteChatHost {
5320
5590
  bind() {
5321
5591
  if (this._bound) return () => this.unbind();
5322
5592
  this._bound = true;
5323
- const host = this.binding.host;
5324
- if (!host.id) host.id = this.binding.hostId;
5593
+ const host2 = this.binding.host;
5594
+ if (!host2.id) host2.id = this.binding.hostId;
5325
5595
  if (this.options.config) attachConfig(this.binding.host, this.options.config);
5326
5596
  const vp = this._vp();
5327
5597
  vp?.setFrameworkManagedDOM?.(true);
5328
5598
  if (this.options.layoutTransitionMs && this.options.layoutTransitionMs > 0) {
5329
5599
  vp?.configure?.({ layoutTransitionMs: this.options.layoutTransitionMs });
5330
5600
  }
5331
- this._installHostMethods(host);
5332
- this._installLifecycleListeners(host);
5333
- this._teardown.push(gateGlide(host));
5601
+ this._installHostMethods(host2);
5602
+ this._installLifecycleListeners(host2);
5603
+ this._teardown.push(gateGlide(host2));
5334
5604
  this._installViewportListeners();
5335
- this._createController(host);
5605
+ this._createController(host2);
5336
5606
  return () => this.unbind();
5337
5607
  }
5338
5608
  unbind() {
@@ -5378,10 +5648,10 @@ class AparteChatHost {
5378
5648
  return this._controller?.setConversationId(id) ?? Promise.resolve();
5379
5649
  }
5380
5650
  _unbindController;
5381
- _createController(host) {
5651
+ _createController(host2) {
5382
5652
  const convBinding = {
5383
5653
  hostId: this.binding.hostId,
5384
- host,
5654
+ host: host2,
5385
5655
  // Controller-driven flushes mirror AparteConversationManager state, not
5386
5656
  // user intent — deliberately NOT emitted as `messagesChange` to
5387
5657
  // avoid a feedback loop with hosts using a two-way `messages` input.
@@ -5488,7 +5758,7 @@ class AparteChatHost {
5488
5758
  const merged = { ...next[index], ...updates };
5489
5759
  next[index] = merged;
5490
5760
  this.binding.setMessages(next);
5491
- this.binding.onMessagesChange?.(next);
5761
+ this._report(next);
5492
5762
  if (updates.status) {
5493
5763
  this._bubbleById(messageId)?.updateMessage?.({ status: updates.status });
5494
5764
  }
@@ -5508,7 +5778,7 @@ class AparteChatHost {
5508
5778
  const newContent = options?.append ? (last.content || "") + content : content;
5509
5779
  const next = [...msgs.slice(0, -1), { ...last, content: newContent }];
5510
5780
  this.binding.setMessages(next);
5511
- this.binding.onMessagesChange?.(next);
5781
+ this._report(next);
5512
5782
  }
5513
5783
  /** Add a segment to the last message (immediate bubble + state sync). */
5514
5784
  addSegment(segment) {
@@ -5533,7 +5803,7 @@ class AparteChatHost {
5533
5803
  const segments = [...last.segments || [], stamped];
5534
5804
  const next = [...msgs.slice(0, -1), { ...last, segments }];
5535
5805
  this.binding.setMessages(next);
5536
- this.binding.onMessagesChange?.(next);
5806
+ this._report(next);
5537
5807
  }
5538
5808
  /** Update a segment in the last message in place. */
5539
5809
  updateSegment(segmentId, updates) {
@@ -5550,7 +5820,7 @@ class AparteChatHost {
5550
5820
  );
5551
5821
  const next = [...msgs.slice(0, -1), { ...last, segments }];
5552
5822
  this.binding.setMessages(next);
5553
- this.binding.onMessagesChange?.(next);
5823
+ this._report(next);
5554
5824
  }
5555
5825
  /** Remove a transient segment (a status row the host added itself). */
5556
5826
  removeSegment(segmentId) {
@@ -5564,7 +5834,7 @@ class AparteChatHost {
5564
5834
  renumberSegments(segments);
5565
5835
  const next = [...msgs.slice(0, -1), { ...last, segments }];
5566
5836
  this.binding.setMessages(next);
5567
- this.binding.onMessagesChange?.(next);
5837
+ this._report(next);
5568
5838
  }
5569
5839
  /**
5570
5840
  * Append text to a segment's content in the last message — the streaming path
@@ -5620,8 +5890,8 @@ class AparteChatHost {
5620
5890
  this._flushHandle = null;
5621
5891
  }
5622
5892
  const tokens = this._tokenBuffer;
5623
- const pending = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
5624
- if (!tokens && !pending) return;
5893
+ const pending2 = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
5894
+ if (!tokens && !pending2) return;
5625
5895
  this._tokenBuffer = null;
5626
5896
  this._segmentBuffer.clear();
5627
5897
  let msgs = this.binding.getMessages();
@@ -5635,11 +5905,11 @@ class AparteChatHost {
5635
5905
  changed = true;
5636
5906
  }
5637
5907
  }
5638
- if (pending) {
5908
+ if (pending2) {
5639
5909
  const last = msgs[msgs.length - 1];
5640
5910
  if (last?.segments) {
5641
5911
  const segments = last.segments.map((s) => {
5642
- const buffered = pending.get(s.id);
5912
+ const buffered = pending2.get(s.id);
5643
5913
  if (buffered === void 0 || !("content" in s)) return s;
5644
5914
  return {
5645
5915
  ...s,
@@ -5653,7 +5923,7 @@ class AparteChatHost {
5653
5923
  }
5654
5924
  if (!changed) return;
5655
5925
  this.binding.setMessages(msgs);
5656
- this.binding.onMessagesChange?.(msgs);
5926
+ this._report(msgs);
5657
5927
  }
5658
5928
  /** Read the current message list. */
5659
5929
  getMessages() {
@@ -5686,7 +5956,7 @@ class AparteChatHost {
5686
5956
  const idx = msgs.findIndex((m) => m.id === messageId);
5687
5957
  const next = idx >= 0 ? msgs.slice(0, idx) : msgs;
5688
5958
  this.binding.setMessages(next);
5689
- this.binding.onMessagesChange?.(next);
5959
+ this._report(next);
5690
5960
  }
5691
5961
  /** Keep up to and including a user message, drop later responses (retry). */
5692
5962
  truncateResponsesAfter(userMessageId) {
@@ -5695,7 +5965,7 @@ class AparteChatHost {
5695
5965
  const idx = msgs.findIndex((m) => m.id === userMessageId);
5696
5966
  const next = idx >= 0 ? msgs.slice(0, idx + 1) : msgs;
5697
5967
  this.binding.setMessages(next);
5698
- this.binding.onMessagesChange?.(next);
5968
+ this._report(next);
5699
5969
  }
5700
5970
  // ── manual token streaming (agnostic core of injectTokenStream) ─────────
5701
5971
  /**
@@ -5723,14 +5993,14 @@ class AparteChatHost {
5723
5993
  }
5724
5994
  ac.signal.addEventListener("abort", () => resolve("aborted"), { once: true });
5725
5995
  });
5726
- const { host, hostId } = this.binding;
5996
+ const { host: host2, hostId } = this.binding;
5727
5997
  const onAbort = (e) => {
5728
5998
  const target = e.detail?.targetId;
5729
5999
  if ((!target || target === hostId) && this._streamAbort === ac) this.stopTokenStream();
5730
6000
  };
5731
6001
  window.addEventListener("aparte-abort", onAbort);
5732
6002
  const replaced = () => this._streamAbort !== void 0 && this._streamAbort !== ac;
5733
- dispatchLifecycleEvent(host, "aparte-message-start", { messageId, role: "assistant" }, hostId);
6003
+ dispatchLifecycleEvent(host2, "aparte-message-start", { messageId, role: "assistant" }, hostId);
5734
6004
  try {
5735
6005
  for (; ; ) {
5736
6006
  const res = await Promise.race([iterator.next(), aborted]);
@@ -5743,7 +6013,7 @@ class AparteChatHost {
5743
6013
  if (!replaced()) {
5744
6014
  this._setStreamingId(null);
5745
6015
  dispatchLifecycleEvent(
5746
- host,
6016
+ host2,
5747
6017
  ac.signal.aborted ? "aparte-message-aborted" : "aparte-message-done",
5748
6018
  ac.signal.aborted ? { messageId } : { messageId, role: "assistant" },
5749
6019
  hostId
@@ -5753,7 +6023,7 @@ class AparteChatHost {
5753
6023
  this._flushStreamState();
5754
6024
  if (!replaced()) {
5755
6025
  this._setStreamingId(null);
5756
- dispatchLifecycleEvent(host, "aparte-message-error", { messageId, error: err }, hostId);
6026
+ dispatchLifecycleEvent(host2, "aparte-message-error", { messageId, error: err }, hostId);
5757
6027
  }
5758
6028
  throw err;
5759
6029
  } finally {
@@ -5911,22 +6181,22 @@ class AparteChatHost {
5911
6181
  const all = vp.querySelectorAll("aparte-chat-bubble");
5912
6182
  return all.length ? all[all.length - 1] : null;
5913
6183
  }
5914
- _installHostMethods(host) {
5915
- host["appendMessage"] = (m) => this.appendMessage(m);
5916
- host["updateMessage"] = (id, u) => this.updateMessage(id, u);
5917
- host["updateLastMessage"] = (c, o) => this.updateLastMessage(c, o);
5918
- host["addSegment"] = (s) => this.addSegment(s);
5919
- host["updateSegment"] = (id, u) => this.updateSegment(id, u);
5920
- host["removeSegment"] = (id) => this.removeSegment(id);
5921
- host["appendToSegment"] = (id, c) => this.appendToSegment(id, c);
5922
- host["getMessages"] = () => this.getMessages();
5923
- host["addBranch"] = (id) => this.addBranch(id);
5924
- host["addSiblingOf"] = (id, m) => this.addSiblingOf(id, m);
5925
- host["truncateFrom"] = (id) => this.truncateFrom(id);
5926
- host["truncateResponsesAfter"] = (id) => this.truncateResponsesAfter(id);
5927
- host["clearAll"] = (options) => this.clearMessages(options);
5928
- }
5929
- _installLifecycleListeners(host) {
6184
+ _installHostMethods(host2) {
6185
+ host2["appendMessage"] = (m) => this.appendMessage(m);
6186
+ host2["updateMessage"] = (id, u) => this.updateMessage(id, u);
6187
+ host2["updateLastMessage"] = (c, o) => this.updateLastMessage(c, o);
6188
+ host2["addSegment"] = (s) => this.addSegment(s);
6189
+ host2["updateSegment"] = (id, u) => this.updateSegment(id, u);
6190
+ host2["removeSegment"] = (id) => this.removeSegment(id);
6191
+ host2["appendToSegment"] = (id, c) => this.appendToSegment(id, c);
6192
+ host2["getMessages"] = () => this.getMessages();
6193
+ host2["addBranch"] = (id) => this.addBranch(id);
6194
+ host2["addSiblingOf"] = (id, m) => this.addSiblingOf(id, m);
6195
+ host2["truncateFrom"] = (id) => this.truncateFrom(id);
6196
+ host2["truncateResponsesAfter"] = (id) => this.truncateResponsesAfter(id);
6197
+ host2["clearAll"] = (options) => this.clearMessages(options);
6198
+ }
6199
+ _installLifecycleListeners(host2) {
5930
6200
  const onStart = (e) => {
5931
6201
  const d = e?.detail;
5932
6202
  if (d?.messageId) this._setStreamingId(d.messageId);
@@ -5946,7 +6216,7 @@ class AparteChatHost {
5946
6216
  (m) => m.id === d.messageId ? { ...m, usage: d.usage } : m
5947
6217
  );
5948
6218
  this.binding.setMessages(next);
5949
- this.binding.onMessagesChange?.(next);
6219
+ this._report(next);
5950
6220
  this._bubbleById(d.messageId)?.setUsage?.(d.usage);
5951
6221
  };
5952
6222
  const onEnd = (e) => {
@@ -5954,15 +6224,15 @@ class AparteChatHost {
5954
6224
  this.binding.onTypingChange?.(false);
5955
6225
  this._setStreamingId(null);
5956
6226
  };
5957
- host.addEventListener("aparte-message-start", onStart);
5958
- host.addEventListener("aparte-message-done", onDone);
5959
- host.addEventListener("aparte-message-error", onEnd);
5960
- host.addEventListener("aparte-message-aborted", onEnd);
6227
+ host2.addEventListener("aparte-message-start", onStart);
6228
+ host2.addEventListener("aparte-message-done", onDone);
6229
+ host2.addEventListener("aparte-message-error", onEnd);
6230
+ host2.addEventListener("aparte-message-aborted", onEnd);
5961
6231
  this._teardown.push(() => {
5962
- host.removeEventListener("aparte-message-start", onStart);
5963
- host.removeEventListener("aparte-message-done", onDone);
5964
- host.removeEventListener("aparte-message-error", onEnd);
5965
- host.removeEventListener("aparte-message-aborted", onEnd);
6232
+ host2.removeEventListener("aparte-message-start", onStart);
6233
+ host2.removeEventListener("aparte-message-done", onDone);
6234
+ host2.removeEventListener("aparte-message-error", onEnd);
6235
+ host2.removeEventListener("aparte-message-aborted", onEnd);
5966
6236
  });
5967
6237
  }
5968
6238
  _installViewportListeners() {
@@ -5979,7 +6249,7 @@ class AparteChatHost {
5979
6249
  return old?.usage !== void 0 ? { ...m, usage: old.usage } : m;
5980
6250
  });
5981
6251
  this.binding.setMessages(merged);
5982
- this.binding.onMessagesChange?.(merged);
6252
+ this._report(merged);
5983
6253
  if (evt.detail.siblings.some((s) => s.count > 1)) {
5984
6254
  this._pendingSiblings = evt.detail.siblings;
5985
6255
  this.binding.afterRender(() => this._applyPendingSiblings());
@@ -6600,6 +6870,9 @@ function remoteProvider(id) {
6600
6870
  }
6601
6871
  class AparteClient {
6602
6872
  _boundHandler = null;
6873
+ /** Between `start()` and `stop()`: the listeners are on `window`. The send handler alone is
6874
+ * not the sign — the constructor prepares it without listening. */
6875
+ _listening = false;
6603
6876
  _boundAbortHandler = null;
6604
6877
  _boundLoadedHandler = null;
6605
6878
  _boundRetryHandler = null;
@@ -6734,11 +7007,12 @@ class AparteClient {
6734
7007
  if (!this._boundLoadedHandler) {
6735
7008
  this._boundLoadedHandler = (e) => {
6736
7009
  if (!this._isForThisInstance(e)) return;
6737
- const host = e.target instanceof HTMLElement ? e.target : void 0;
6738
- void this.resumeDeferred(host ?? e.detail?.targetId);
7010
+ const host2 = e.target instanceof HTMLElement ? e.target : void 0;
7011
+ void this.resumeDeferred(host2 ?? e.detail?.targetId);
6739
7012
  };
6740
7013
  }
6741
7014
  window.addEventListener("aparte-conversation-loaded", this._boundLoadedHandler);
7015
+ this._listening = true;
6742
7016
  }
6743
7017
  /**
6744
7018
  * Stop answering: abort the reply in flight, take this client's `keyResolver` off the
@@ -6748,7 +7022,8 @@ class AparteClient {
6748
7022
  this.abort();
6749
7023
  this._releaseKeyResolver?.();
6750
7024
  this._releaseKeyResolver = null;
6751
- if (!this._boundHandler) return;
7025
+ if (!this._listening || !this._boundHandler) return;
7026
+ this._listening = false;
6752
7027
  window.removeEventListener("aparte-send", this._boundHandler);
6753
7028
  this._boundHandler = null;
6754
7029
  if (this._boundAbortHandler) {
@@ -7681,15 +7956,15 @@ ${content}
7681
7956
  * nobody answered — the turn was stopped, or the channel gave up — and there is no
7682
7957
  * resume. A channel that CRASHES fails the turn, as it does for a run in the page.
7683
7958
  */
7684
- async _answerDeferred(targetElement, pending, signal, rowEvents, approvedBefore = /* @__PURE__ */ new Set(), approvedNow = /* @__PURE__ */ new Set()) {
7959
+ async _answerDeferred(targetElement, pending2, signal, rowEvents, approvedBefore = /* @__PURE__ */ new Set(), approvedNow = /* @__PURE__ */ new Set()) {
7685
7960
  const outcomes = {};
7686
7961
  const resolver = this._approvalResolverFor(targetElement, true);
7687
7962
  const refuseTheRest = (after) => {
7688
- for (const rest of pending.slice(after + 1)) {
7963
+ for (const rest of pending2.slice(after + 1)) {
7689
7964
  outcomes[rest.toolCallId] = { kind: "decision", approved: false, reason: NOT_RUN_AFTER_A_REFUSAL, by: "earlier-refusal" };
7690
7965
  }
7691
7966
  };
7692
- for (const [index, p] of pending.entries()) {
7967
+ for (const [index, p] of pending2.entries()) {
7693
7968
  if (signal.aborted) break;
7694
7969
  const call = { id: p.toolCallId, name: p.name, input: p.input ?? {} };
7695
7970
  if (p.reason === "approval") {
@@ -7849,8 +8124,8 @@ ${content}
7849
8124
  * @param target The chat — an element, or its id. Defaults to the one `targetResolver` names, else the first on the page.
7850
8125
  */
7851
8126
  async resumeDeferred(target) {
7852
- const host = typeof target === "string" ? this._byId(target) : target ?? this.options.targetResolver?.() ?? null;
7853
- const targetElement = host ? this._asRenderTarget(host) : this._scanForTarget();
8127
+ const host2 = typeof target === "string" ? this._byId(target) : target ?? this.options.targetResolver?.() ?? null;
8128
+ const targetElement = host2 ? this._asRenderTarget(host2) : this._scanForTarget();
7854
8129
  if (!targetElement) return false;
7855
8130
  const messages = targetElement.getMessages?.() ?? [];
7856
8131
  const last = [...messages].reverse().find((m) => m.role === "assistant");
@@ -7869,7 +8144,7 @@ ${content}
7869
8144
  this._resuming.add(last.id);
7870
8145
  try {
7871
8146
  const authConfig = provider.id ? await this._resolveAuth(provider.id) : void 0;
7872
- const pending = open.map((s) => ({
8147
+ const pending2 = open.map((s) => ({
7873
8148
  toolCallId: s.toolCall.id,
7874
8149
  name: s.toolCall.name,
7875
8150
  input: s.toolCall.input,
@@ -7877,7 +8152,7 @@ ${content}
7877
8152
  }));
7878
8153
  const base = this._resumeBase(messages, messages.indexOf(last));
7879
8154
  await this._streamTurn(targetElement, last.id, provider, base, recorded?.modelId ?? config.defaultModel ?? "", authConfig, {
7880
- resume: { pending, ticket },
8155
+ resume: { pending: pending2, ticket },
7881
8156
  ...recorded?.temperature !== void 0 ? { temperature: recorded.temperature } : {}
7882
8157
  });
7883
8158
  } finally {
@@ -8364,8 +8639,95 @@ function installDialogTriggersOnce(doc = document) {
8364
8639
  doc.addEventListener("pointerup", onPointerUp, true);
8365
8640
  doc.addEventListener("click", onClick);
8366
8641
  }
8642
+ const ATTR = "defer-hydration";
8643
+ const waiting = /* @__PURE__ */ new WeakMap();
8644
+ const holderOf = /* @__PURE__ */ new WeakMap();
8645
+ const pending = /* @__PURE__ */ new WeakSet();
8646
+ const live = /* @__PURE__ */ new WeakSet();
8647
+ const observers = /* @__PURE__ */ new WeakMap();
8648
+ const patched = /* @__PURE__ */ new WeakSet();
8649
+ function isHeld(el2) {
8650
+ if (live.has(el2)) return false;
8651
+ if (holderOf.has(el2)) return true;
8652
+ const holder = el2.closest(`[${ATTR}]`);
8653
+ if (!holder) return false;
8654
+ holderOf.set(el2, holder);
8655
+ let list = waiting.get(holder);
8656
+ if (!list) {
8657
+ list = [];
8658
+ waiting.set(holder, list);
8659
+ const observer = new MutationObserver(() => {
8660
+ if (!holder.hasAttribute(ATTR)) release(holder);
8661
+ });
8662
+ observer.observe(holder, { attributes: true, attributeFilter: [ATTR] });
8663
+ observers.set(holder, observer);
8664
+ }
8665
+ list.push(el2);
8666
+ return true;
8667
+ }
8668
+ function release(holder) {
8669
+ observers.get(holder)?.disconnect();
8670
+ observers.delete(holder);
8671
+ const list = waiting.get(holder) ?? [];
8672
+ waiting.delete(holder);
8673
+ for (const el2 of list) {
8674
+ holderOf.delete(el2);
8675
+ if (el2.isConnected && !live.has(el2)) el2.connectedCallback?.();
8676
+ }
8677
+ }
8678
+ function releaseDeferredHydration(root) {
8679
+ if (!root) return;
8680
+ if (root.hasAttribute(ATTR)) root.removeAttribute(ATTR);
8681
+ else if (!waiting.has(root)) return;
8682
+ release(root);
8683
+ }
8684
+ function deferHydrationOf(ctor) {
8685
+ if (patched.has(ctor)) return;
8686
+ patched.add(ctor);
8687
+ const proto = ctor.prototype;
8688
+ const connected = proto.connectedCallback;
8689
+ const changed = proto.attributeChangedCallback;
8690
+ const disconnected = proto.disconnectedCallback;
8691
+ const observed = ctor.observedAttributes ?? [];
8692
+ proto.connectedCallback = function() {
8693
+ if (isHeld(this)) {
8694
+ pending.add(this);
8695
+ return;
8696
+ }
8697
+ if (pending.has(this)) {
8698
+ pending.delete(this);
8699
+ for (const name of observed) {
8700
+ const value = this.getAttribute(name);
8701
+ if (value !== null) changed?.call(this, name, null, value);
8702
+ }
8703
+ }
8704
+ live.add(this);
8705
+ connected?.call(this);
8706
+ };
8707
+ if (changed) {
8708
+ proto.attributeChangedCallback = function(name, oldValue, newValue) {
8709
+ if (isHeld(this)) {
8710
+ pending.add(this);
8711
+ return;
8712
+ }
8713
+ changed.call(this, name, oldValue, newValue);
8714
+ };
8715
+ }
8716
+ proto.disconnectedCallback = function() {
8717
+ const holder = holderOf.get(this);
8718
+ if (holder) {
8719
+ holderOf.delete(this);
8720
+ const list = waiting.get(holder);
8721
+ if (list) list.splice(list.indexOf(this), 1);
8722
+ return;
8723
+ }
8724
+ if (pending.has(this)) return;
8725
+ live.delete(this);
8726
+ disconnected?.call(this);
8727
+ };
8728
+ }
8367
8729
  export {
8368
- registerSegmentRenderer as $,
8730
+ parseMarkdownToSegments as $,
8369
8731
  APARTE_CONFIG_CHANGE as A,
8370
8732
  contextConfig as B,
8371
8733
  copyText as C,
@@ -8376,53 +8738,63 @@ export {
8376
8738
  cssEscape as H,
8377
8739
  declineDefaultRenderers as I,
8378
8740
  defaultSanitizer as J,
8379
- detachConfig as K,
8380
- escapeAttr as L,
8381
- escapeHtml as M,
8382
- filesToAttachments as N,
8383
- getAllRenderers as O,
8384
- getSegmentRenderer as P,
8385
- installDefaultRenderersOnce as Q,
8386
- installDialogTriggersOnce as R,
8387
- isAwaitingReply as S,
8388
- isFormatAdapter as T,
8389
- isSafeUrl as U,
8390
- isSegmentSettled as V,
8391
- parseAparteEventStream as W,
8392
- parseMarkdownToSegments as X,
8393
- populateBubbleFromMessage as Y,
8394
- readableToAsyncIterable as Z,
8395
- registerDefaultRenderers as _,
8741
+ deferHydrationOf as K,
8742
+ defineMountedSegmentRenderer as L,
8743
+ defineMountedToolRenderer as M,
8744
+ detachConfig as N,
8745
+ escapeAttr as O,
8746
+ escapeHtml as P,
8747
+ filesToAttachments as Q,
8748
+ getAllRenderers as R,
8749
+ getSegmentRenderer as S,
8750
+ installDefaultRenderersOnce as T,
8751
+ installDialogTriggersOnce as U,
8752
+ isAwaitingReply as V,
8753
+ isFormatAdapter as W,
8754
+ isSafeUrl as X,
8755
+ isSegmentSettled as Y,
8756
+ overrideSegmentRenderer as Z,
8757
+ parseAparteEventStream as _,
8396
8758
  APARTE_CONVERSATION_SCHEMA_VERSION as a,
8397
- requestUserInput as a0,
8398
- resolveConfig as a1,
8399
- revokeAttachmentUrls as a2,
8400
- runWithConfig as a3,
8401
- segmentDuration as a4,
8402
- segmentTiming as a5,
8403
- subscribeConfigChange as a6,
8404
- toolRefusal as a7,
8405
- unregisterSegmentRenderer as a8,
8406
- uuid as a9,
8407
- eventOrigin as aa,
8408
- activeElementIn as ab,
8409
- gateGlide as ac,
8410
- renderingVersionOf as ad,
8411
- mergeSegmentUpdate as ae,
8412
- writeStreamedMarkdown as af,
8413
- stampSegmentActivity as ag,
8414
- stampSegmentOnInsert as ah,
8415
- stampSegmentOnUpdate as ai,
8416
- renumberSegments as aj,
8417
- openSegmentIds as ak,
8418
- isTerminalStatus as al,
8419
- adoptMessageSegments as am,
8420
- APARTE_DERIVED_SEGMENTS as an,
8421
- rememberConversationStatus as ao,
8422
- tellConversationStatus as ap,
8423
- conversationStatusOf as aq,
8424
- chatBoundaryOf as ar,
8425
- rootOf as as,
8759
+ populateBubbleFromMessage as a0,
8760
+ readableToAsyncIterable as a1,
8761
+ registerDefaultRenderers as a2,
8762
+ registerSegmentRenderer as a3,
8763
+ releaseDeferredHydration as a4,
8764
+ requestUserInput as a5,
8765
+ resolveConfig as a6,
8766
+ revokeAttachmentUrls as a7,
8767
+ runWithConfig as a8,
8768
+ segmentDuration as a9,
8769
+ conversationStatusOf as aA,
8770
+ chatBoundaryOf as aB,
8771
+ rootOf as aC,
8772
+ segmentTiming as aa,
8773
+ subscribeConfigChange as ab,
8774
+ toToolCallView as ac,
8775
+ toolRefusal as ad,
8776
+ unregisterSegmentRenderer as ae,
8777
+ uuid as af,
8778
+ eventOrigin as ag,
8779
+ activeElementIn as ah,
8780
+ gateGlide as ai,
8781
+ toolCallRenderer as aj,
8782
+ toolRendererThatDrew as ak,
8783
+ isMountedRenderer as al,
8784
+ renderingVersionOf as am,
8785
+ onRenderersChange as an,
8786
+ mergeSegmentUpdate as ao,
8787
+ writeStreamedMarkdown as ap,
8788
+ stampSegmentActivity as aq,
8789
+ stampSegmentOnInsert as ar,
8790
+ stampSegmentOnUpdate as as,
8791
+ renumberSegments as at,
8792
+ openSegmentIds as au,
8793
+ isTerminalStatus as av,
8794
+ adoptMessageSegments as aw,
8795
+ APARTE_DERIVED_SEGMENTS as ax,
8796
+ rememberConversationStatus as ay,
8797
+ tellConversationStatus as az,
8426
8798
  APARTE_DEFAULT_BUBBLE_ACTIONS as b,
8427
8799
  APARTE_DEFAULT_HOST_HANDLERS as c,
8428
8800
  APARTE_DEFAULT_ICON_FALLBACKS as d,
@@ -8449,4 +8821,4 @@ export {
8449
8821
  collectRendererStyles as y,
8450
8822
  contentToText as z
8451
8823
  };
8452
- //# sourceMappingURL=dialog-triggers-BqXRdHWZ.js.map
8824
+ //# sourceMappingURL=defer-hydration-BwMqeRHq.js.map