@runtypelabs/persona 3.34.0 → 3.35.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 (186) hide show
  1. package/README.md +11 -11
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
  5. package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +11 -11
  9. package/dist/codegen.js +10 -10
  10. package/dist/index.cjs +46 -44
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +514 -158
  13. package/dist/index.d.ts +514 -158
  14. package/dist/index.global.js +41 -89
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +46 -44
  17. package/dist/index.js.map +1 -1
  18. package/dist/install.global.js +1 -1
  19. package/dist/install.global.js.map +1 -1
  20. package/dist/launcher.global.js +2 -2
  21. package/dist/launcher.global.js.map +1 -1
  22. package/dist/markdown-parsers.js +53 -0
  23. package/dist/plugin-kit.d.cts +4 -4
  24. package/dist/plugin-kit.d.ts +4 -4
  25. package/dist/runtype-tts.js +1 -0
  26. package/dist/smart-dom-reader.cjs +1 -1
  27. package/dist/smart-dom-reader.d.cts +352 -103
  28. package/dist/smart-dom-reader.d.ts +352 -103
  29. package/dist/smart-dom-reader.js +1 -1
  30. package/dist/theme-editor-preview.cjs +203 -0
  31. package/dist/theme-editor-preview.d.cts +5748 -0
  32. package/dist/theme-editor-preview.d.ts +5748 -0
  33. package/dist/theme-editor-preview.js +203 -0
  34. package/dist/theme-editor.cjs +14 -131
  35. package/dist/theme-editor.d.cts +362 -574
  36. package/dist/theme-editor.d.ts +362 -574
  37. package/dist/theme-editor.js +14 -131
  38. package/dist/theme-reference.cjs +1 -1
  39. package/dist/theme-reference.d.cts +1 -1
  40. package/dist/theme-reference.d.ts +1 -1
  41. package/dist/theme-reference.js +1 -1
  42. package/dist/voice-worklet-player.cjs +80 -0
  43. package/dist/voice-worklet-player.d.cts +215 -0
  44. package/dist/voice-worklet-player.d.ts +215 -0
  45. package/dist/voice-worklet-player.js +80 -0
  46. package/dist/widget.css +54 -36
  47. package/package.json +16 -2
  48. package/src/animations/glyph-cycle.ts +11 -14
  49. package/src/animations/wipe.ts +1 -1
  50. package/src/ask-user-question-tool.test.ts +1 -1
  51. package/src/ask-user-question-tool.ts +6 -8
  52. package/src/client.test.ts +67 -12
  53. package/src/client.ts +40 -32
  54. package/src/codegen.test.ts +1 -1
  55. package/src/codegen.ts +1 -1
  56. package/src/components/approval-bubble.test.ts +1 -1
  57. package/src/components/approval-bubble.ts +3 -3
  58. package/src/components/artifact-pane.ts +3 -3
  59. package/src/components/ask-user-question-bubble.test.ts +14 -14
  60. package/src/components/ask-user-question-bubble.ts +10 -10
  61. package/src/components/composer-builder.ts +17 -16
  62. package/src/components/composer-parts.test.ts +54 -0
  63. package/src/components/composer-parts.ts +95 -126
  64. package/src/components/demo-carousel.ts +1 -1
  65. package/src/components/event-stream-view.test.ts +7 -7
  66. package/src/components/event-stream-view.ts +5 -5
  67. package/src/components/header-builder.ts +33 -27
  68. package/src/components/header-layouts.ts +1 -1
  69. package/src/components/header-parts.test.ts +90 -0
  70. package/src/components/header-parts.ts +62 -93
  71. package/src/components/launcher.test.ts +11 -0
  72. package/src/components/launcher.ts +1 -1
  73. package/src/components/message-bubble.test.ts +74 -2
  74. package/src/components/message-bubble.ts +25 -11
  75. package/src/components/messages.ts +2 -2
  76. package/src/components/panel.test.ts +1 -1
  77. package/src/components/panel.ts +92 -97
  78. package/src/components/pill-composer-builder.ts +28 -24
  79. package/src/components/reasoning-bubble.ts +2 -2
  80. package/src/components/suggestions.ts +1 -1
  81. package/src/components/tool-bubble.ts +1 -1
  82. package/src/components/widget-view.test.ts +181 -0
  83. package/src/components/widget-view.ts +234 -0
  84. package/src/defaults.ts +54 -51
  85. package/src/generated/runtype-openapi-contract.ts +5 -0
  86. package/src/index-core.ts +30 -4
  87. package/src/index-global.ts +73 -6
  88. package/src/index.ts +7 -1
  89. package/src/install.test.ts +11 -11
  90. package/src/install.ts +28 -18
  91. package/src/launcher-global.ts +25 -14
  92. package/src/markdown-parsers-eager.ts +20 -0
  93. package/src/markdown-parsers-entry.ts +4 -0
  94. package/src/markdown-parsers-loader.ts +49 -0
  95. package/src/plugin-kit.test.ts +1 -1
  96. package/src/plugin-kit.ts +7 -7
  97. package/src/plugins/types.ts +5 -5
  98. package/src/postprocessors.ts +27 -14
  99. package/src/runtime/host-layout.test.ts +1 -1
  100. package/src/runtime/host-layout.ts +4 -5
  101. package/src/runtime/init.ts +2 -2
  102. package/src/session.test.ts +5 -6
  103. package/src/session.ts +333 -227
  104. package/src/session.voice.test.ts +146 -0
  105. package/src/session.webmcp.test.ts +24 -25
  106. package/src/smart-dom-reader.ts +3 -4
  107. package/src/styles/widget.css +54 -36
  108. package/src/suggest-replies-tool.test.ts +3 -3
  109. package/src/suggest-replies-tool.ts +5 -5
  110. package/src/testing/index.ts +1 -1
  111. package/src/theme-editor/color-utils.ts +2 -2
  112. package/src/theme-editor/index.ts +4 -13
  113. package/src/theme-editor/presets.ts +1 -1
  114. package/src/theme-editor/preview-utils.ts +22 -20
  115. package/src/theme-editor/preview.ts +4 -1
  116. package/src/theme-editor/role-mappings.ts +3 -3
  117. package/src/theme-editor/sections.ts +21 -20
  118. package/src/theme-editor/types.ts +1 -1
  119. package/src/theme-editor/webmcp/index.ts +1 -1
  120. package/src/theme-editor/webmcp/summary.ts +1 -2
  121. package/src/theme-editor/webmcp/tools.ts +30 -30
  122. package/src/theme-editor/webmcp/types.ts +2 -2
  123. package/src/theme-editor-preview.ts +10 -0
  124. package/src/theme-reference.ts +26 -26
  125. package/src/types.ts +368 -99
  126. package/src/ui.approval-plugin.test.ts +3 -3
  127. package/src/ui.ask-user-question-plugin.test.ts +9 -9
  128. package/src/ui.component-directive.test.ts +4 -4
  129. package/src/ui.composer-bar.test.ts +10 -11
  130. package/src/ui.composer-keyboard.test.ts +5 -5
  131. package/src/ui.header-icon-color.test.ts +59 -0
  132. package/src/ui.scroll.test.ts +5 -6
  133. package/src/ui.stop-button.test.ts +3 -4
  134. package/src/ui.suggest-replies.test.ts +2 -2
  135. package/src/ui.ts +292 -131
  136. package/src/utils/buttons.ts +1 -1
  137. package/src/utils/code-generators.ts +1 -1
  138. package/src/utils/composer-history.ts +3 -3
  139. package/src/utils/dom-context.test.ts +2 -2
  140. package/src/utils/dom-context.ts +7 -7
  141. package/src/utils/dom.test.ts +67 -0
  142. package/src/utils/dom.ts +75 -0
  143. package/src/utils/dropdown.ts +2 -2
  144. package/src/utils/event-stream-buffer.test.ts +2 -2
  145. package/src/utils/message-fingerprint.test.ts +9 -0
  146. package/src/utils/message-fingerprint.ts +5 -0
  147. package/src/utils/morph.ts +1 -1
  148. package/src/utils/sanitize.ts +32 -16
  149. package/src/utils/sequence-buffer.test.ts +8 -8
  150. package/src/utils/sequence-buffer.ts +4 -4
  151. package/src/utils/smart-dom-adapter.test.ts +1 -1
  152. package/src/utils/smart-dom-adapter.ts +4 -5
  153. package/src/utils/speech-text.test.ts +122 -0
  154. package/src/utils/speech-text.ts +101 -0
  155. package/src/utils/stream-animation.test.ts +3 -3
  156. package/src/utils/stream-animation.ts +6 -6
  157. package/src/utils/theme.test.ts +1 -1
  158. package/src/utils/throughput-tracker.test.ts +11 -11
  159. package/src/utils/throughput-tracker.ts +9 -9
  160. package/src/utils/tokens.ts +13 -13
  161. package/src/utils/virtual-scroller.ts +1 -1
  162. package/src/vendor/smart-dom-reader/README.md +4 -4
  163. package/src/vendor/smart-dom-reader/index.d.ts +1 -1
  164. package/src/vendor/smart-dom-reader/index.js +2 -2
  165. package/src/version.ts +1 -1
  166. package/src/voice/audio-playback-manager.test.ts +159 -0
  167. package/src/voice/audio-playback-manager.ts +145 -30
  168. package/src/voice/browser-speech-engine.ts +130 -0
  169. package/src/voice/fallback-speech-engine.ts +89 -0
  170. package/src/voice/index.ts +21 -0
  171. package/src/voice/read-aloud-controller.test.ts +143 -0
  172. package/src/voice/read-aloud-controller.ts +136 -0
  173. package/src/voice/runtype-speech-engine.test.ts +280 -0
  174. package/src/voice/runtype-speech-engine.ts +211 -0
  175. package/src/voice/runtype-tts-entry.ts +10 -0
  176. package/src/voice/runtype-tts-loader.test.ts +47 -0
  177. package/src/voice/runtype-tts-loader.ts +42 -0
  178. package/src/voice/runtype-voice-provider.ts +332 -537
  179. package/src/voice/voice-factory.ts +26 -4
  180. package/src/voice/voice.test.ts +321 -7
  181. package/src/voice/worklet-playback-engine.ts +272 -0
  182. package/src/voice-worklet-player.ts +39 -0
  183. package/src/webmcp-bridge.test.ts +6 -6
  184. package/src/webmcp-bridge.ts +24 -25
  185. package/src/webmcp-polyfill.ts +1 -1
  186. package/src/voice/voice-activity-detector.ts +0 -90
@@ -70,6 +70,60 @@ describe("createSendButton", () => {
70
70
  expect(send.button.textContent).toBe("Send");
71
71
  });
72
72
 
73
+ it("applies configured icon-button styles and skips the primary-bg fallback class", () => {
74
+ const send = createSendButton({
75
+ ...baseConfig,
76
+ sendButton: {
77
+ useIcon: true,
78
+ iconName: "send",
79
+ size: "48px",
80
+ backgroundColor: "rgb(1, 2, 3)",
81
+ textColor: "rgb(4, 5, 6)",
82
+ borderWidth: "2px",
83
+ borderColor: "rgb(7, 8, 9)",
84
+ paddingX: "6px",
85
+ paddingY: "4px",
86
+ },
87
+ });
88
+ const s = send.button.style;
89
+ expect(s.width).toBe("48px");
90
+ expect(s.height).toBe("48px");
91
+ expect(s.minWidth).toBe("48px");
92
+ expect(s.minHeight).toBe("48px");
93
+ expect(s.color).toBe("rgb(4, 5, 6)");
94
+ expect(s.backgroundColor).toBe("rgb(1, 2, 3)");
95
+ expect(s.borderWidth).toBe("2px");
96
+ expect(s.borderStyle).toBe("solid");
97
+ expect(s.borderColor).toBe("rgb(7, 8, 9)");
98
+ expect(s.paddingLeft).toBe("6px");
99
+ expect(s.paddingRight).toBe("6px");
100
+ expect(s.paddingTop).toBe("4px");
101
+ expect(s.paddingBottom).toBe("4px");
102
+ // An explicit backgroundColor means the fallback class is not added.
103
+ expect(send.button.classList.contains("persona-bg-persona-primary")).toBe(false);
104
+ });
105
+
106
+ it("toggles the white-text fallback class based on textColor (text mode)", () => {
107
+ const plain = createSendButton(baseConfig);
108
+ expect(plain.button.classList.contains("persona-text-white")).toBe(true);
109
+ expect(plain.button.style.color).toBe("");
110
+
111
+ const colored = createSendButton({
112
+ ...baseConfig,
113
+ sendButton: { textColor: "rgb(4, 5, 6)" },
114
+ });
115
+ expect(colored.button.classList.contains("persona-text-white")).toBe(false);
116
+ expect(colored.button.style.color).toBe("rgb(4, 5, 6)");
117
+ });
118
+
119
+ it("adds the primary-bg fallback class in icon mode without an explicit background", () => {
120
+ const send = createSendButton({
121
+ ...baseConfig,
122
+ sendButton: { useIcon: true, iconName: "send" },
123
+ });
124
+ expect(send.button.classList.contains("persona-bg-persona-primary")).toBe(true);
125
+ });
126
+
73
127
  describe("icon mode", () => {
74
128
  const iconConfig: AgentWidgetConfig = {
75
129
  ...baseConfig,
@@ -1,4 +1,4 @@
1
- import { createElement } from "../utils/dom";
1
+ import { createElement, createNode, cx } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
3
  import { AgentWidgetConfig } from "../types";
4
4
  import { ALL_SUPPORTED_MIME_TYPES } from "../utils/content";
@@ -6,7 +6,7 @@ import { ALL_SUPPORTED_MIME_TYPES } from "../utils/content";
6
6
  /**
7
7
  * Low-level composer control factories. Both `buildComposer` (full,
8
8
  * column-stacked card) and `buildPillComposer` (single-row pill) consume
9
- * these the only meaningful difference between the two composers is the
9
+ * these: the only meaningful difference between the two composers is the
10
10
  * layout shell + className. No DOM assembly here; each factory returns the
11
11
  * element plus any handles the caller needs.
12
12
  *
@@ -61,7 +61,7 @@ export const createComposerTextarea = (config?: AgentWidgetConfig): ComposerText
61
61
  });
62
62
  };
63
63
 
64
- // Strip browser default focus rings the composer wraps the textarea in
64
+ // Strip browser default focus rings: the composer wraps the textarea in
65
65
  // its own surface, so the textarea itself must be visually transparent.
66
66
  textarea.style.border = "none";
67
67
  textarea.style.outline = "none";
@@ -112,36 +112,47 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
112
112
 
113
113
  const wrapper = createElement("div", "persona-send-button-wrapper");
114
114
 
115
- const button = createElement(
116
- "button",
117
- useIcon
118
- ? "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer"
119
- : "persona-rounded-button persona-bg-persona-accent persona-px-4 persona-py-2 persona-text-sm persona-font-semibold disabled:persona-opacity-50 persona-cursor-pointer"
120
- ) as HTMLButtonElement;
121
-
122
- button.type = "submit";
123
- button.setAttribute("data-persona-composer-submit", "");
115
+ const button = createNode("button", {
116
+ className: cx(
117
+ "persona-rounded-button disabled:persona-opacity-50 persona-cursor-pointer",
118
+ useIcon
119
+ ? "persona-flex persona-items-center persona-justify-center"
120
+ : "persona-bg-persona-accent persona-px-4 persona-py-2 persona-text-sm persona-font-semibold",
121
+ // Icon mode without an explicit background falls back to the primary bg
122
+ // class; text mode without an explicit color falls back to white text.
123
+ useIcon && !backgroundColor && "persona-bg-persona-primary",
124
+ !useIcon && !textColor && "persona-text-white"
125
+ ),
126
+ attrs: { type: "submit", "data-persona-composer-submit": "" },
127
+ style: {
128
+ // Sizing is icon-mode-only (text mode is sized by its padding classes).
129
+ width: useIcon ? buttonSize : undefined,
130
+ height: useIcon ? buttonSize : undefined,
131
+ minWidth: useIcon ? buttonSize : undefined,
132
+ minHeight: useIcon ? buttonSize : undefined,
133
+ fontSize: useIcon ? "18px" : undefined,
134
+ lineHeight: useIcon ? "1" : undefined,
135
+ // Icon mode always sets a color; text mode only when textColor is given.
136
+ color: useIcon
137
+ ? textColor || "var(--persona-button-primary-fg, #ffffff)"
138
+ : textColor || undefined,
139
+ // backgroundColor is honored in icon mode only.
140
+ backgroundColor: useIcon ? backgroundColor || undefined : undefined,
141
+ borderWidth: sendButtonConfig.borderWidth || undefined,
142
+ borderStyle: sendButtonConfig.borderWidth ? "solid" : undefined,
143
+ borderColor: sendButtonConfig.borderColor || undefined,
144
+ paddingLeft: sendButtonConfig.paddingX || undefined,
145
+ paddingRight: sendButtonConfig.paddingX || undefined,
146
+ paddingTop: sendButtonConfig.paddingY || undefined,
147
+ paddingBottom: sendButtonConfig.paddingY || undefined,
148
+ },
149
+ });
124
150
 
125
151
  // Both icons are pre-rendered so setMode can swap cheaply.
126
152
  let sendIcon: SVGElement | null = null;
127
153
  let stopIcon: SVGElement | null = null;
128
154
 
129
155
  if (useIcon) {
130
- button.style.width = buttonSize;
131
- button.style.height = buttonSize;
132
- button.style.minWidth = buttonSize;
133
- button.style.minHeight = buttonSize;
134
- button.style.fontSize = "18px";
135
- button.style.lineHeight = "1";
136
-
137
- button.innerHTML = "";
138
-
139
- if (textColor) {
140
- button.style.color = textColor;
141
- } else {
142
- button.style.color = "var(--persona-button-primary-fg, #ffffff)";
143
- }
144
-
145
156
  const iconSize = parseFloat(buttonSize) || 24;
146
157
  const iconColor = textColor?.trim() || "currentColor";
147
158
 
@@ -157,42 +168,8 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
157
168
  }
158
169
 
159
170
  stopIcon = renderLucideIcon(stopIconName, iconSize, iconColor, 2);
160
-
161
- if (backgroundColor) {
162
- button.style.backgroundColor = backgroundColor;
163
- } else {
164
- button.classList.add("persona-bg-persona-primary");
165
- }
166
171
  } else {
167
172
  button.textContent = sendLabel;
168
- if (textColor) {
169
- button.style.color = textColor;
170
- } else {
171
- button.classList.add("persona-text-white");
172
- }
173
- }
174
-
175
- if (sendButtonConfig.borderWidth) {
176
- button.style.borderWidth = sendButtonConfig.borderWidth;
177
- button.style.borderStyle = "solid";
178
- }
179
- if (sendButtonConfig.borderColor) {
180
- button.style.borderColor = sendButtonConfig.borderColor;
181
- }
182
-
183
- if (sendButtonConfig.paddingX) {
184
- button.style.paddingLeft = sendButtonConfig.paddingX;
185
- button.style.paddingRight = sendButtonConfig.paddingX;
186
- } else {
187
- button.style.paddingLeft = "";
188
- button.style.paddingRight = "";
189
- }
190
- if (sendButtonConfig.paddingY) {
191
- button.style.paddingTop = sendButtonConfig.paddingY;
192
- button.style.paddingBottom = sendButtonConfig.paddingY;
193
- } else {
194
- button.style.paddingTop = "";
195
- button.style.paddingBottom = "";
196
173
  }
197
174
 
198
175
  let tooltip: HTMLElement | null = null;
@@ -218,7 +195,7 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
218
195
  if (useIcon) {
219
196
  if (sendIcon && stopIcon) {
220
197
  const next = mode === "stop" ? stopIcon : sendIcon;
221
- // Replace whatever icon is currently mounted the button only ever
198
+ // Replace whatever icon is currently mounted: the button only ever
222
199
  // holds the single active icon. We use replaceChildren(next) rather
223
200
  // than replaceChild(next, prev) against a captured `prev` reference:
224
201
  // an external re-render/morph can swap the live icon child out from
@@ -259,36 +236,40 @@ export const createMicButton = (config?: AgentWidgetConfig): MicButtonParts | nu
259
236
  if (!hasVoiceInput) return null;
260
237
 
261
238
  const buttonSize = config?.sendButton?.size ?? "40px";
262
- const wrapper = createElement("div", "persona-send-button-wrapper");
263
- const button = createElement(
264
- "button",
265
- "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer"
266
- ) as HTMLButtonElement;
267
-
268
- button.type = "button";
269
- button.setAttribute("data-persona-composer-mic", "");
270
- button.setAttribute("aria-label", "Start voice recognition");
271
-
272
239
  const micIconName = voiceRecognitionConfig.iconName ?? "mic";
273
240
  const micIconSize = voiceRecognitionConfig.iconSize ?? buttonSize;
274
241
  const micIconSizeNum = parseFloat(micIconSize) || 24;
275
-
276
242
  const micBackgroundColor =
277
243
  voiceRecognitionConfig.backgroundColor ?? config?.sendButton?.backgroundColor;
278
244
  const micIconColor = voiceRecognitionConfig.iconColor ?? config?.sendButton?.textColor;
279
245
 
280
- button.style.width = micIconSize;
281
- button.style.height = micIconSize;
282
- button.style.minWidth = micIconSize;
283
- button.style.minHeight = micIconSize;
284
- button.style.fontSize = "18px";
285
- button.style.lineHeight = "1";
286
-
287
- if (micIconColor) {
288
- button.style.color = micIconColor;
289
- } else {
290
- button.style.color = "var(--persona-text, #111827)";
291
- }
246
+ const wrapper = createElement("div", "persona-send-button-wrapper");
247
+ const button = createNode("button", {
248
+ className:
249
+ "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer",
250
+ attrs: {
251
+ type: "button",
252
+ "data-persona-composer-mic": "",
253
+ "aria-label": "Start voice recognition",
254
+ },
255
+ style: {
256
+ width: micIconSize,
257
+ height: micIconSize,
258
+ minWidth: micIconSize,
259
+ minHeight: micIconSize,
260
+ fontSize: "18px",
261
+ lineHeight: "1",
262
+ color: micIconColor || "var(--persona-text, #111827)",
263
+ backgroundColor: micBackgroundColor || undefined,
264
+ borderWidth: voiceRecognitionConfig.borderWidth || undefined,
265
+ borderStyle: voiceRecognitionConfig.borderWidth ? "solid" : undefined,
266
+ borderColor: voiceRecognitionConfig.borderColor || undefined,
267
+ paddingLeft: voiceRecognitionConfig.paddingX || undefined,
268
+ paddingRight: voiceRecognitionConfig.paddingX || undefined,
269
+ paddingTop: voiceRecognitionConfig.paddingY || undefined,
270
+ paddingBottom: voiceRecognitionConfig.paddingY || undefined,
271
+ },
272
+ });
292
273
 
293
274
  const iconColorValue = micIconColor || "currentColor";
294
275
  const micIconSvg = renderLucideIcon(micIconName, micIconSizeNum, iconColorValue, 1.5);
@@ -298,27 +279,6 @@ export const createMicButton = (config?: AgentWidgetConfig): MicButtonParts | nu
298
279
  button.textContent = "🎤";
299
280
  }
300
281
 
301
- if (micBackgroundColor) {
302
- button.style.backgroundColor = micBackgroundColor;
303
- }
304
-
305
- if (voiceRecognitionConfig.borderWidth) {
306
- button.style.borderWidth = voiceRecognitionConfig.borderWidth;
307
- button.style.borderStyle = "solid";
308
- }
309
- if (voiceRecognitionConfig.borderColor) {
310
- button.style.borderColor = voiceRecognitionConfig.borderColor;
311
- }
312
-
313
- if (voiceRecognitionConfig.paddingX) {
314
- button.style.paddingLeft = voiceRecognitionConfig.paddingX;
315
- button.style.paddingRight = voiceRecognitionConfig.paddingX;
316
- }
317
- if (voiceRecognitionConfig.paddingY) {
318
- button.style.paddingTop = voiceRecognitionConfig.paddingY;
319
- button.style.paddingBottom = voiceRecognitionConfig.paddingY;
320
- }
321
-
322
282
  wrapper.appendChild(button);
323
283
 
324
284
  const micTooltipText = voiceRecognitionConfig.tooltipText ?? "Start voice recognition";
@@ -355,39 +315,45 @@ export const createAttachmentControls = (config?: AgentWidgetConfig): Attachment
355
315
  "div",
356
316
  "persona-attachment-previews persona-flex persona-flex-wrap persona-gap-2 persona-mb-2"
357
317
  );
318
+ previewsContainer.setAttribute("data-persona-composer-attachment-previews", "");
358
319
  previewsContainer.style.display = "none";
359
320
 
360
321
  const input = createElement("input") as HTMLInputElement;
361
322
  input.type = "file";
323
+ input.setAttribute("data-persona-composer-attachment-input", "");
362
324
  input.accept = (attachmentsConfig.allowedTypes ?? ALL_SUPPORTED_MIME_TYPES).join(",");
363
325
  input.multiple = (attachmentsConfig.maxFiles ?? 4) > 1;
364
326
  input.style.display = "none";
365
327
  input.setAttribute("aria-label", "Attach files");
366
328
 
367
- const wrapper = createElement("div", "persona-send-button-wrapper");
368
- const button = createElement(
369
- "button",
370
- "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer persona-attachment-button"
371
- ) as HTMLButtonElement;
372
- button.type = "button";
373
- button.setAttribute("aria-label", attachmentsConfig.buttonTooltipText ?? "Attach file");
374
-
375
329
  const attachIconName = attachmentsConfig.buttonIconName ?? "paperclip";
376
330
  const attachIconSize = buttonSize;
377
331
  const buttonSizeNum = parseFloat(attachIconSize) || 40;
378
332
  const attachIconSizeNum = Math.round(buttonSizeNum * 0.6);
379
333
 
380
- button.style.width = attachIconSize;
381
- button.style.height = attachIconSize;
382
- button.style.minWidth = attachIconSize;
383
- button.style.minHeight = attachIconSize;
384
- button.style.fontSize = "18px";
385
- button.style.lineHeight = "1";
386
- button.style.backgroundColor = "transparent";
387
- button.style.color = "var(--persona-primary, #111827)";
388
- button.style.border = "none";
389
- button.style.borderRadius = "6px";
390
- button.style.transition = "background-color 0.15s ease";
334
+ const wrapper = createElement("div", "persona-send-button-wrapper");
335
+ const button = createNode("button", {
336
+ className:
337
+ "persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer persona-attachment-button",
338
+ attrs: {
339
+ type: "button",
340
+ "data-persona-composer-attachment-button": "",
341
+ "aria-label": attachmentsConfig.buttonTooltipText ?? "Attach file",
342
+ },
343
+ style: {
344
+ width: attachIconSize,
345
+ height: attachIconSize,
346
+ minWidth: attachIconSize,
347
+ minHeight: attachIconSize,
348
+ fontSize: "18px",
349
+ lineHeight: "1",
350
+ backgroundColor: "transparent",
351
+ color: "var(--persona-primary, #111827)",
352
+ border: "none",
353
+ borderRadius: "6px",
354
+ transition: "background-color 0.15s ease",
355
+ },
356
+ });
391
357
 
392
358
  button.addEventListener("mouseenter", () => {
393
359
  button.style.backgroundColor = "var(--persona-palette-colors-black-alpha-50, rgba(0, 0, 0, 0.05))";
@@ -452,4 +418,7 @@ export const createStatusText = (config?: AgentWidgetConfig): HTMLElement => {
452
418
  };
453
419
 
454
420
  export const createSuggestionsRow = (): HTMLElement =>
455
- createElement("div", "persona-mb-3 persona-flex persona-flex-wrap persona-gap-2");
421
+ createNode("div", {
422
+ className: "persona-mb-3 persona-flex persona-flex-wrap persona-gap-2",
423
+ attrs: { "data-persona-composer-suggestions": "" },
424
+ });
@@ -612,7 +612,7 @@ export function createDemoCarousel(
612
612
  body.classList.remove("theme-dark");
613
613
  }
614
614
  } catch {
615
- // Cross-origin iframe silently ignore
615
+ // Cross-origin iframe: silently ignore
616
616
  }
617
617
  }
618
618
 
@@ -815,7 +815,7 @@ describe("createEventStreamView", () => {
815
815
  const buffer = createMockBuffer(events);
816
816
  const { element, update } = createEventStreamView({ buffer: buffer as any });
817
817
 
818
- // Initial render (Path A first render)
818
+ // Initial render (Path A: first render)
819
819
  update();
820
820
 
821
821
  const eventsList = getEventsList(element);
@@ -825,7 +825,7 @@ describe("createEventStreamView", () => {
825
825
  const row1Ref = eventsList.children[0];
826
826
  const row2Ref = eventsList.children[1];
827
827
 
828
- // Add a new event and update (Path C incremental append)
828
+ // Add a new event and update (Path C: incremental append)
829
829
  vi.advanceTimersByTime(150);
830
830
  buffer.push(makeEvent("step_chunk", 3));
831
831
  update();
@@ -1173,7 +1173,7 @@ describe("createEventStreamView", () => {
1173
1173
  // Wait past the throttle interval
1174
1174
  vi.advanceTimersByTime(150);
1175
1175
 
1176
- // Add event and update should render immediately since 150ms > 100ms
1176
+ // Add event and update: should render immediately since 150ms > 100ms
1177
1177
  buffer.push(makeEvent("flow_complete", 2));
1178
1178
  update();
1179
1179
 
@@ -1223,7 +1223,7 @@ describe("createEventStreamView", () => {
1223
1223
  const filterSelect = getFilterSelect(element);
1224
1224
  const copyAllBtn = getCopyAllBtn(element);
1225
1225
 
1226
- // Immediately change filter this should bypass throttle (uses updateNow internally)
1226
+ // Immediately change filter: this should bypass throttle (uses updateNow internally)
1227
1227
  filterSelect.value = "step_chunk";
1228
1228
  filterSelect.__fireEvent("change");
1229
1229
 
@@ -1237,17 +1237,17 @@ describe("createEventStreamView", () => {
1237
1237
  const buffer = createMockBuffer([makeEvent("step_chunk", 1)]);
1238
1238
  const { update, destroy } = createEventStreamView({ buffer: buffer as any });
1239
1239
 
1240
- // First update immediate
1240
+ // First update: immediate
1241
1241
  update();
1242
1242
 
1243
1243
  // Schedule a throttled update
1244
1244
  buffer.push(makeEvent("step_chunk", 2));
1245
1245
  update();
1246
1246
 
1247
- // Destroy before rAF fires should not throw
1247
+ // Destroy before rAF fires: should not throw
1248
1248
  expect(() => destroy()).not.toThrow();
1249
1249
 
1250
- // Advancing timers to flush rAF should not throw even though view is destroyed
1250
+ // Advancing timers to flush rAF: should not throw even though view is destroyed
1251
1251
  vi.advanceTimersByTime(20);
1252
1252
  vi.useRealTimers();
1253
1253
  });
@@ -568,7 +568,7 @@ export function createEventStreamView(
568
568
  );
569
569
  throughputValueEl.textContent = "-- tok/s";
570
570
 
571
- // Custom hover tooltip appears instantly (no native title delay).
571
+ // Custom hover tooltip: appears instantly (no native title delay).
572
572
  // Appended to the (non-clipping) toolbar wrapper rather than the header
573
573
  // bar, which has overflow-hidden and would clip a dropdown. Position is
574
574
  // measured on hover so it sits just under the throughput group.
@@ -899,7 +899,7 @@ export function createEventStreamView(
899
899
  expandedSet.add(eventId);
900
900
  }
901
901
  dirtyExpandId = eventId;
902
- // Save scroll position user-initiated expand/collapse should not auto-scroll
902
+ // Save scroll position: user-initiated expand/collapse should not auto-scroll
903
903
  const savedScrollTop = eventsList.scrollTop;
904
904
  const wasAutoFollowing = autoFollow.isFollowing();
905
905
  suppressScrollHandler = true;
@@ -993,7 +993,7 @@ export function createEventStreamView(
993
993
  const isFirstRender = rowElements.size === 0 && filteredEvents.length > 0;
994
994
 
995
995
  if (filterChanged || isFirstRender || filteredEvents.length === 0) {
996
- // Path A Full rebuild (filter/search changed, first render, or empty list)
996
+ // Path A: Full rebuild (filter/search changed, first render, or empty list)
997
997
  eventsList.innerHTML = "";
998
998
  rowElements.clear();
999
999
  const fragment = document.createDocumentFragment();
@@ -1016,7 +1016,7 @@ export function createEventStreamView(
1016
1016
  lastRenderedSearch = searchTerm;
1017
1017
  dirtyExpandId = null;
1018
1018
  } else {
1019
- // Path B Single row replace (expand/collapse)
1019
+ // Path B: Single row replace (expand/collapse)
1020
1020
  if (dirtyExpandId !== null) {
1021
1021
  const oldRow = rowElements.get(dirtyExpandId);
1022
1022
  if (oldRow && oldRow.parentNode === eventsList) {
@@ -1043,7 +1043,7 @@ export function createEventStreamView(
1043
1043
  dirtyExpandId = null;
1044
1044
  }
1045
1045
 
1046
- // Path C Incremental append (default streaming path)
1046
+ // Path C: Incremental append (default streaming path)
1047
1047
  // Remove evicted rows
1048
1048
  const activeIds = new Set(filteredEvents.map((e) => e.id));
1049
1049
  for (const [id, el] of rowElements) {
@@ -1,4 +1,4 @@
1
- import { createElement } from "../utils/dom";
1
+ import { createElement, createNode } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
3
  import { AgentWidgetConfig } from "../types";
4
4
  import { createCloseButton, createClearChatButton } from "./header-parts";
@@ -38,15 +38,18 @@ export interface HeaderBuildContext {
38
38
  export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
39
39
  const { config, showClose = true } = context;
40
40
 
41
- const header = createElement(
42
- "div",
43
- "persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5"
44
- );
45
- header.setAttribute("data-persona-theme-zone", "header");
46
- header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
47
- header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
48
- header.style.boxShadow = 'var(--persona-header-shadow, none)';
49
- header.style.borderBottom = 'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
41
+ const header = createNode("div", {
42
+ className:
43
+ "persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5",
44
+ attrs: { "data-persona-theme-zone": "header" },
45
+ style: {
46
+ backgroundColor: "var(--persona-header-bg, var(--persona-surface, #ffffff))",
47
+ borderBottomColor: "var(--persona-header-border, var(--persona-divider, #f1f5f9))",
48
+ boxShadow: "var(--persona-header-shadow, none)",
49
+ borderBottom:
50
+ "var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))",
51
+ },
52
+ });
50
53
 
51
54
  const launcher = config?.launcher ?? {};
52
55
  const headerIconSize = launcher.headerIconSize ?? "48px";
@@ -54,16 +57,16 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
54
57
  const headerIconHidden = launcher.headerIconHidden ?? false;
55
58
  const headerIconName = launcher.headerIconName;
56
59
 
57
- const iconHolder = createElement(
58
- "div",
59
- "persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-text-xl"
60
- );
61
- iconHolder.style.height = headerIconSize;
62
- iconHolder.style.width = headerIconSize;
63
- iconHolder.style.backgroundColor =
64
- "var(--persona-header-icon-bg, var(--persona-primary, #0f0f0f))";
65
- iconHolder.style.color =
66
- "var(--persona-header-icon-fg, var(--persona-text-inverse, #ffffff))";
60
+ const iconHolder = createNode("div", {
61
+ className:
62
+ "persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-text-xl",
63
+ style: {
64
+ height: headerIconSize,
65
+ width: headerIconSize,
66
+ backgroundColor: "var(--persona-header-icon-bg, var(--persona-primary, #0f0f0f))",
67
+ color: "var(--persona-header-icon-fg, var(--persona-text-inverse, #ffffff))",
68
+ },
69
+ });
67
70
 
68
71
  // Render icon based on priority: Lucide icon > iconUrl > agentIconText
69
72
  if (!headerIconHidden) {
@@ -93,13 +96,16 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
93
96
  }
94
97
 
95
98
  const headerCopy = createElement("div", "persona-flex persona-flex-col persona-flex-1 persona-min-w-0");
96
- const title = createElement("span", "persona-text-base persona-font-semibold");
97
- title.style.color = HEADER_THEME_CSS.titleColor;
98
- title.textContent = config?.launcher?.title ?? "Chat Assistant";
99
- const subtitle = createElement("span", "persona-text-xs");
100
- subtitle.style.color = HEADER_THEME_CSS.subtitleColor;
101
- subtitle.textContent =
102
- config?.launcher?.subtitle ?? "Here to help you get answers fast";
99
+ const title = createNode("span", {
100
+ className: "persona-text-base persona-font-semibold",
101
+ text: config?.launcher?.title ?? "Chat Assistant",
102
+ style: { color: HEADER_THEME_CSS.titleColor },
103
+ });
104
+ const subtitle = createNode("span", {
105
+ className: "persona-text-xs",
106
+ text: config?.launcher?.subtitle ?? "Here to help you get answers fast",
107
+ style: { color: HEADER_THEME_CSS.subtitleColor },
108
+ });
103
109
 
104
110
  headerCopy.append(title, subtitle);
105
111
 
@@ -121,7 +121,7 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
121
121
  header.style.borderBottom =
122
122
  'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
123
123
 
124
- // Build the title area either a combo button (titleMenu) or standard title row
124
+ // Build the title area: either a combo button (titleMenu) or standard title row
125
125
  const titleMenuConfig = layoutHeaderConfig?.titleMenu;
126
126
  let titleRow: HTMLElement;
127
127
  let headerTitle: HTMLElement;