@runtypelabs/persona 3.21.2 → 3.21.3

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.
@@ -10562,12 +10562,7 @@ var createSendButton = (config) => {
10562
10562
  if (useIcon) {
10563
10563
  if (sendIcon && stopIcon) {
10564
10564
  const next = mode === "stop" ? stopIcon : sendIcon;
10565
- const prev = mode === "stop" ? sendIcon : stopIcon;
10566
- if (prev.parentNode === button) {
10567
- button.replaceChild(next, prev);
10568
- } else {
10569
- button.appendChild(next);
10570
- }
10565
+ button.replaceChildren(next);
10571
10566
  }
10572
10567
  } else {
10573
10568
  button.textContent = mode === "stop" ? stopLabel : sendLabel;
@@ -10565,12 +10565,7 @@ var createSendButton = (config) => {
10565
10565
  if (useIcon) {
10566
10566
  if (sendIcon && stopIcon) {
10567
10567
  const next = mode === "stop" ? stopIcon : sendIcon;
10568
- const prev = mode === "stop" ? sendIcon : stopIcon;
10569
- if (prev.parentNode === button) {
10570
- button.replaceChild(next, prev);
10571
- } else {
10572
- button.appendChild(next);
10573
- }
10568
+ button.replaceChildren(next);
10574
10569
  }
10575
10570
  } else {
10576
10571
  button.textContent = mode === "stop" ? stopLabel : sendLabel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.21.2",
3
+ "version": "3.21.3",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -69,6 +69,40 @@ describe("createSendButton", () => {
69
69
  send.setMode("send");
70
70
  expect(send.button.textContent).toBe("Send");
71
71
  });
72
+
73
+ describe("icon mode", () => {
74
+ const iconConfig: AgentWidgetConfig = {
75
+ ...baseConfig,
76
+ sendButton: { useIcon: true, iconName: "send", stopIconName: "square" },
77
+ };
78
+ const iconCount = (btn: HTMLElement) => btn.querySelectorAll("svg").length;
79
+
80
+ it("keeps exactly one icon across a send→stop→send cycle", () => {
81
+ const send = createSendButton(iconConfig);
82
+ expect(iconCount(send.button)).toBe(1);
83
+ send.setMode("stop");
84
+ expect(iconCount(send.button)).toBe(1);
85
+ send.setMode("send");
86
+ expect(iconCount(send.button)).toBe(1);
87
+ });
88
+
89
+ it("does not stack a stale icon when an external re-render swapped the live icon node", () => {
90
+ const send = createSendButton(iconConfig);
91
+ // Simulate a DOM morph/re-render (e.g. a host calling controller.update())
92
+ // that replaces the live icon child with a clone. This detaches the
93
+ // captured `sendIcon` reference, so `sendIcon.parentNode !== button`.
94
+ // The old replaceChild/appendChild fallback then left BOTH icons mounted,
95
+ // producing the doubled send-arrow after the first send→stop→send cycle.
96
+ const live = send.button.firstElementChild as SVGElement;
97
+ send.button.replaceChildren(live.cloneNode(true));
98
+ expect(iconCount(send.button)).toBe(1);
99
+
100
+ send.setMode("stop");
101
+ expect(iconCount(send.button)).toBe(1);
102
+ send.setMode("send");
103
+ expect(iconCount(send.button)).toBe(1);
104
+ });
105
+ });
72
106
  });
73
107
 
74
108
  describe("createMicButton", () => {
@@ -218,12 +218,15 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
218
218
  if (useIcon) {
219
219
  if (sendIcon && stopIcon) {
220
220
  const next = mode === "stop" ? stopIcon : sendIcon;
221
- const prev = mode === "stop" ? sendIcon : stopIcon;
222
- if (prev.parentNode === button) {
223
- button.replaceChild(next, prev);
224
- } else {
225
- button.appendChild(next);
226
- }
221
+ // Replace whatever icon is currently mounted the button only ever
222
+ // holds the single active icon. We use replaceChildren(next) rather
223
+ // than replaceChild(next, prev) against a captured `prev` reference:
224
+ // an external re-render/morph can swap the live icon child out from
225
+ // under us, detaching our captured node so `prev.parentNode !== button`.
226
+ // The old appendChild fallback then left BOTH icons mounted, which is
227
+ // how the send button ended up showing two stacked arrows after the
228
+ // first send→stop→send cycle.
229
+ button.replaceChildren(next);
227
230
  }
228
231
  } else {
229
232
  button.textContent = mode === "stop" ? stopLabel : sendLabel;