@runtypelabs/persona 3.34.1 → 3.36.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 +56 -12
  53. package/src/client.ts +35 -31
  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
@@ -104,7 +104,7 @@ describe("renderApproval plugin hook", () => {
104
104
  head.setAttribute("data-action", "toggle");
105
105
  const params = document.createElement("pre");
106
106
  params.setAttribute("data-test-id", "appr-params");
107
- // Single delegated listener at the root the recommended pattern.
107
+ // Single delegated listener at the root: the recommended pattern.
108
108
  root.addEventListener("click", (e) => {
109
109
  const target = (e.target as HTMLElement).closest("[data-action]");
110
110
  if (target?.getAttribute("data-action") === "toggle") {
@@ -125,7 +125,7 @@ describe("renderApproval plugin hook", () => {
125
125
  } as unknown as Parameters<typeof createAgentExperience>[1]);
126
126
 
127
127
  injectApproval(controller);
128
- // Force a re-render the scenario where innerHTML morph drops listeners
128
+ // Force a re-render: the scenario where innerHTML morph drops listeners
129
129
  // on a freshly-built plugin element unless it's hydrated post-morph.
130
130
  injectNoise(controller);
131
131
 
@@ -195,7 +195,7 @@ describe("renderApproval plugin hook", () => {
195
195
  plugins: [plugin],
196
196
  } as unknown as Parameters<typeof createAgentExperience>[1]);
197
197
 
198
- // Inject directly in the resolved state plugin opts out, built-in renders.
198
+ // Inject directly in the resolved state: plugin opts out, built-in renders.
199
199
  injectApproval(controller, { status: "approved" });
200
200
 
201
201
  expect(mount.querySelector('[data-test-id="custom-pending"]')).toBeNull();
@@ -127,7 +127,7 @@ describe("renderAskUserQuestion plugin hook", () => {
127
127
  injectAskUserQuestion(controller);
128
128
  expect(mount.querySelector("[data-persona-ask-sheet-for]")).toBeNull();
129
129
 
130
- // Force a re-render by injecting a second message the ask_user_question
130
+ // Force a re-render by injecting a second message: the ask_user_question
131
131
  // wrapper now goes through the render path again. The built-in overlay
132
132
  // sheet must NOT appear; the plugin still owns the UI.
133
133
  controller.injectTestMessage({
@@ -158,7 +158,7 @@ describe("renderAskUserQuestion plugin hook", () => {
158
158
  btn.type = "button";
159
159
  btn.setAttribute("data-test-id", "click-pill");
160
160
  btn.textContent = "Pick me";
161
- // Single delegated listener at root pattern recommended for plugins.
161
+ // Single delegated listener at root: pattern recommended for plugins.
162
162
  root.addEventListener("click", (e) => {
163
163
  if ((e.target as HTMLElement).getAttribute("data-test-id") === "click-pill") {
164
164
  resolve("Pick me");
@@ -190,7 +190,7 @@ describe("renderAskUserQuestion plugin hook", () => {
190
190
 
191
191
  injectAskUserQuestion(controller);
192
192
 
193
- // Force a re-render before the click this is exactly the scenario
193
+ // Force a re-render before the click: this is exactly the scenario
194
194
  // where innerHTML-based morph used to drop listeners on the freshly-built
195
195
  // plugin root.
196
196
  controller.injectTestMessage({
@@ -226,7 +226,7 @@ describe("renderAskUserQuestion plugin hook", () => {
226
226
 
227
227
  // Plugin renders an interactive card while the question is awaiting an
228
228
  // answer. Once answered, the widget suppresses the original tool message
229
- // entirely the plugin renderer is invoked but returns null and the
229
+ // entirely, the plugin renderer is invoked but returns null, and the
230
230
  // session injects Q→A pair messages (assistant question + user answer)
231
231
  // that render through the standard transcript pipeline.
232
232
  const plugin: AgentWidgetPlugin = {
@@ -350,7 +350,7 @@ describe("renderAskUserQuestion plugin hook", () => {
350
350
  (sheet.querySelector('[data-option-label="A"]') as HTMLElement).click();
351
351
  expect(sheet.getAttribute("data-ask-current-index")).toBe("1");
352
352
 
353
- // Final page: pick should NOT auto-submit Submit-all button still present.
353
+ // Final page: pick should NOT auto-submit: Submit-all button still present.
354
354
  (sheet.querySelector('[data-option-label="C"]') as HTMLElement).click();
355
355
  expect(sheet.getAttribute("data-ask-current-index")).toBe("2");
356
356
  (sheet.querySelector('[data-option-label="E"]') as HTMLElement).click();
@@ -519,12 +519,12 @@ describe("renderAskUserQuestion plugin hook", () => {
519
519
  expect(sheet.getAttribute("data-ask-layout")).toBe("rows");
520
520
  expect(sheet.getAttribute("data-ask-current-index")).toBe("0");
521
521
 
522
- // Focus on body no element inside the overlay subtree is focused.
522
+ // Focus on body: no element inside the overlay subtree is focused.
523
523
  if (document.activeElement && document.activeElement !== document.body) {
524
524
  (document.activeElement as HTMLElement).blur?.();
525
525
  }
526
526
 
527
- // Dispatch the digit keypress on `mount` the listener on `mount`
527
+ // Dispatch the digit keypress on `mount`: the listener on `mount`
528
528
  // catches it regardless of focus location. In grouped single-select mode,
529
529
  // a successful pick auto-advances to the next page (default behavior).
530
530
  mount.dispatchEvent(
@@ -564,7 +564,7 @@ describe("renderAskUserQuestion plugin hook", () => {
564
564
  new KeyboardEvent("keydown", { key: "2", bubbles: true })
565
565
  );
566
566
 
567
- // No row should have been picked input is the focused target so the
567
+ // No row should have been picked: input is the focused target so the
568
568
  // mount-level handler bails out.
569
569
  const beta = sheet.querySelector<HTMLElement>('[data-option-label="Beta"]')!;
570
570
  expect(beta.getAttribute("aria-pressed")).toBe("false");
@@ -572,7 +572,7 @@ describe("renderAskUserQuestion plugin hook", () => {
572
572
  controller.destroy();
573
573
  });
574
574
 
575
- it("respects groupedAutoAdvance: false pick stays on current page", () => {
575
+ it("respects groupedAutoAdvance: false: pick stays on current page", () => {
576
576
  const mount = createMount();
577
577
  const controller = createAgentExperience(mount, {
578
578
  apiUrl: "https://api.example.com/chat",
@@ -38,7 +38,7 @@ const directiveMessage = (
38
38
  });
39
39
  };
40
40
 
41
- describe("component directive bubble listener preservation across morphs", () => {
41
+ describe("component directive bubble: listener preservation across morphs", () => {
42
42
  beforeEach(() => {
43
43
  // The component registry is a singleton; clear it between tests so each
44
44
  // test starts from a clean slate.
@@ -89,7 +89,7 @@ describe("component directive bubble — listener preservation across morphs", (
89
89
  const formBefore = mount.querySelector<HTMLFormElement>('[data-test-id="test-form-el"]');
90
90
  expect(formBefore).not.toBeNull();
91
91
 
92
- // Force another render pass this is what triggered the bug pre-fix:
92
+ // Force another render pass: this is what triggered the bug pre-fix:
93
93
  // Idiomorph would replace the form via innerHTML serialization, dropping
94
94
  // the addEventListener-attached submit handler.
95
95
  controller.injectTestMessage({
@@ -103,7 +103,7 @@ describe("component directive bubble — listener preservation across morphs", (
103
103
  },
104
104
  });
105
105
 
106
- // The form node should be the SAME instance preserved by the live
106
+ // The form node should be the SAME instance: preserved by the live
107
107
  // wrapper's `data-preserve-runtime` and the post-morph hydrate skipping
108
108
  // when fingerprint is unchanged.
109
109
  const formAfter = mount.querySelector<HTMLFormElement>('[data-test-id="test-form-el"]');
@@ -145,7 +145,7 @@ describe("component directive bubble — listener preservation across morphs", (
145
145
  });
146
146
  expect(mount.querySelector('[data-test-id="test-badge"]')?.textContent).toBe("first");
147
147
 
148
- // Same id, new props fingerprint changes, so renderer should be invoked
148
+ // Same id, new props: fingerprint changes, so renderer should be invoked
149
149
  // again and the new element hydrated into the live wrapper.
150
150
  directiveMessage(controller, {
151
151
  id: "badge-1",
@@ -17,7 +17,7 @@ describe("createAgentExperience composer-bar mode", () => {
17
17
  }
18
18
  });
19
19
 
20
- it("starts collapsed with pill geometry bottom-centered, configured width on pillRoot", () => {
20
+ it("starts collapsed with pill geometry: bottom-centered, configured width on pillRoot", () => {
21
21
  const mount = document.createElement("div");
22
22
  document.body.appendChild(mount);
23
23
 
@@ -197,21 +197,20 @@ describe("createAgentExperience composer-bar mode", () => {
197
197
  expect(pillRoot).not.toBeNull();
198
198
  expect(wrapper!.dataset.state).toBe("collapsed");
199
199
  expect(pillRoot!.dataset.state).toBe("collapsed");
200
- // Modal's expanded geometry on the wrapper must be cleared on close
201
- // the wrapper has no visible chrome in collapsed state since the
200
+ // Modal's expanded geometry on the wrapper must be cleared on close: // the wrapper has no visible chrome in collapsed state since the
202
201
  // container is hidden via CSS and the pill lives in pillRoot.
203
202
  expect(wrapper!.style.transform).toBe("");
204
203
  expect(wrapper!.style.top).toBe("");
205
204
  expect(wrapper!.style.maxHeight).toBe("");
206
205
  expect(wrapper!.style.height).toBe("");
207
- // Pill geometry on pillRoot is reapplied per state bottom uses the
206
+ // Pill geometry on pillRoot is reapplied per state: bottom uses the
208
207
  // configured offset (default 16px).
209
208
  expect(pillRoot!.style.bottom).toBe("16px");
210
209
 
211
210
  controller.destroy();
212
211
  });
213
212
 
214
- it("does NOT cap the pill composer form with contentMaxWidth the pill matches the wrapper's responsive width", () => {
213
+ it("does NOT cap the pill composer form with contentMaxWidth: the pill matches the wrapper's responsive width", () => {
215
214
  const mount = document.createElement("div");
216
215
  document.body.appendChild(mount);
217
216
 
@@ -374,7 +373,7 @@ describe("createAgentExperience composer-bar mode", () => {
374
373
  controller.destroy();
375
374
  });
376
375
 
377
- it("places the pill (footer) inside pillRoot a viewport-fixed sibling of the wrapper, not inside the panel", () => {
376
+ it("places the pill (footer) inside pillRoot: a viewport-fixed sibling of the wrapper, not inside the panel", () => {
378
377
  const mount = document.createElement("div");
379
378
  document.body.appendChild(mount);
380
379
 
@@ -519,7 +518,7 @@ describe("createAgentExperience composer-bar mode", () => {
519
518
  expect(wrapper!.dataset.state).toBe("expanded");
520
519
 
521
520
  // Simulate a pointerdown anywhere outside the wrapper. Use document.body
522
- // as the target definitely outside the wrapper subtree.
521
+ // as the target: definitely outside the wrapper subtree.
523
522
  const outsideTarget = document.createElement("div");
524
523
  document.body.appendChild(outsideTarget);
525
524
  outsideTarget.dispatchEvent(
@@ -813,7 +812,7 @@ describe("createAgentExperience composer-bar mode", () => {
813
812
  panel!.dispatchEvent(new PointerEvent("pointerenter", { bubbles: true }));
814
813
 
815
814
  const textNode = getPeekText(mount)!;
816
- // letter-rise wins over typewriter peek-specific config beats inherit.
815
+ // letter-rise wins over typewriter: peek-specific config beats inherit.
817
816
  expect(textNode.classList.contains("persona-stream-letter-rise")).toBe(true);
818
817
  expect(textNode.classList.contains("persona-stream-typewriter")).toBe(false);
819
818
  expect(textNode.style.getPropertyValue("--persona-stream-step")).toBe("40ms");
@@ -945,7 +944,7 @@ describe("createAgentExperience composer-bar mode", () => {
945
944
 
946
945
  const textNode = getPeekText(mount)!;
947
946
  expect(textNode.querySelector(".persona-pill-peek__skeleton")).not.toBeNull();
948
- // No char spans yet skeleton stands alone.
947
+ // No char spans yet: skeleton stands alone.
949
948
  expect(textNode.querySelectorAll(".persona-stream-char").length).toBe(0);
950
949
 
951
950
  controller.destroy();
@@ -986,7 +985,7 @@ describe("createAgentExperience composer-bar mode", () => {
986
985
  const wrapper = mount.querySelector<HTMLElement>("[data-persona-composer-bar]");
987
986
  expect(wrapper!.dataset.state).toBe("expanded");
988
987
 
989
- // Click inside the pill textarea must keep the panel expanded.
988
+ // Click inside the pill textarea: must keep the panel expanded.
990
989
  const textarea = mount.querySelector<HTMLTextAreaElement>(
991
990
  "[data-persona-composer-input]"
992
991
  );
@@ -996,7 +995,7 @@ describe("createAgentExperience composer-bar mode", () => {
996
995
  );
997
996
  expect(wrapper!.dataset.state).toBe("expanded");
998
997
 
999
- // Click inside the chat container body must also keep the panel open.
998
+ // Click inside the chat container body: must also keep the panel open.
1000
999
  const body = mount.querySelector<HTMLElement>(".persona-widget-body");
1001
1000
  expect(body).not.toBeNull();
1002
1001
  body!.dispatchEvent(
@@ -25,7 +25,7 @@ const press = (el: Element, key: string) =>
25
25
  new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true })
26
26
  );
27
27
 
28
- describe("createAgentExperience composer keyboard Enter / Esc while streaming", () => {
28
+ describe("createAgentExperience composer keyboard: Enter / Esc while streaming", () => {
29
29
  const originalFetch = global.fetch;
30
30
  let capturedSignals: AbortSignal[] = [];
31
31
 
@@ -38,7 +38,7 @@ describe("createAgentExperience composer keyboard — Enter / Esc while streamin
38
38
  vi.stubGlobal("cancelAnimationFrame", () => {});
39
39
  window.scrollTo = vi.fn();
40
40
 
41
- // Fetch hangs until aborted models an in-flight SSE stream so the widget
41
+ // Fetch hangs until aborted: models an in-flight SSE stream so the widget
42
42
  // stays "streaming".
43
43
  global.fetch = vi.fn().mockImplementation((_url: string, options: any) => {
44
44
  const signal = options.signal as AbortSignal;
@@ -80,7 +80,7 @@ describe("createAgentExperience composer keyboard — Enter / Esc while streamin
80
80
  expect(controller.getState().streaming).toBe(true);
81
81
  expect(capturedSignals).toHaveLength(1);
82
82
 
83
- // Type something new, then hit Enter it must be inert mid-stream.
83
+ // Type something new, then hit Enter: it must be inert mid-stream.
84
84
  textarea.value = "queued text";
85
85
  press(textarea, "Enter");
86
86
  await flush();
@@ -130,7 +130,7 @@ describe("createAgentExperience composer keyboard — Enter / Esc while streamin
130
130
  });
131
131
  });
132
132
 
133
- describe("createAgentExperience composer keyboard Up/Down history", () => {
133
+ describe("createAgentExperience composer keyboard: Up/Down history", () => {
134
134
  beforeEach(() => {
135
135
  window.scrollTo = vi.fn();
136
136
  try {
@@ -207,7 +207,7 @@ describe("createAgentExperience composer keyboard — Up/Down history", () => {
207
207
  textarea.setSelectionRange(5, 5); // caret mid-text
208
208
 
209
209
  press(textarea, "ArrowUp");
210
- // Value unchanged Up moved the cursor instead of recalling history.
210
+ // Value unchanged: Up moved the cursor instead of recalling history.
211
211
  expect(textarea.value).toBe("line one\nline two");
212
212
 
213
213
  controller.destroy();
@@ -0,0 +1,59 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+
7
+ const createMount = () => {
8
+ const mount = document.createElement("div");
9
+ document.body.appendChild(mount);
10
+ return mount;
11
+ };
12
+
13
+ // The header icon lives in the icon holder (the only `.persona-rounded-xl`
14
+ // element in the header zone; the close/clear buttons are `.persona-rounded-full`).
15
+ const HEADER_ICON_SELECTOR =
16
+ '[data-persona-theme-zone="header"] .persona-rounded-xl svg';
17
+
18
+ describe("createAgentExperience header icon color", () => {
19
+ afterEach(() => {
20
+ document.body.innerHTML = "";
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ it("renders the header Lucide icon with currentColor so it inherits the themed icon color", () => {
25
+ window.scrollTo = vi.fn();
26
+ const mount = createMount();
27
+ createAgentExperience(mount, {
28
+ apiUrl: "https://api.example.com/chat",
29
+ launcher: { enabled: false, headerIconName: "bot" },
30
+ });
31
+
32
+ const icon = mount.querySelector<SVGElement>(HEADER_ICON_SELECTOR);
33
+ expect(icon).not.toBeNull();
34
+ expect(icon!.getAttribute("stroke")).toBe("currentColor");
35
+ });
36
+
37
+ it("keeps the header icon on currentColor after controller.update (regression: was hardcoded #ffffff)", () => {
38
+ window.scrollTo = vi.fn();
39
+ const mount = createMount();
40
+ const controller = createAgentExperience(mount, {
41
+ apiUrl: "https://api.example.com/chat",
42
+ launcher: { enabled: false, headerIconName: "bot", title: "Before" },
43
+ });
44
+
45
+ // A runtime update re-renders the header icon. It must preserve
46
+ // `currentColor` so the themed `--persona-header-icon-fg` keeps applying;
47
+ // previously this path hardcoded "#ffffff", so the icon color "wouldn't
48
+ // stick" whenever the theme editor (or any update) ran.
49
+ controller.update({
50
+ apiUrl: "https://api.example.com/chat",
51
+ launcher: { enabled: false, headerIconName: "bot", title: "After" },
52
+ });
53
+
54
+ const icon = mount.querySelector<SVGElement>(HEADER_ICON_SELECTOR);
55
+ expect(icon).not.toBeNull();
56
+ expect(icon!.getAttribute("stroke")).toBe("currentColor");
57
+ expect(icon!.getAttribute("stroke")).not.toBe("#ffffff");
58
+ });
59
+ });
@@ -808,7 +808,7 @@ describe("createAgentExperience streaming scroll", () => {
808
808
  expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
809
809
 
810
810
  // Content grows with no render event (e.g. an image finishing loading
811
- // mid-stream) only the ResizeObserver sees it.
811
+ // mid-stream): only the ResizeObserver sees it.
812
812
  metrics.setScrollHeight(1200);
813
813
  resize.trigger();
814
814
  raf.flush();
@@ -869,8 +869,7 @@ describe("createAgentExperience streaming scroll", () => {
869
869
 
870
870
  expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
871
871
 
872
- // A selection forms inside the transcript (mouse drag or keyboard
873
- // both surface as selectionchange).
872
+ // A selection forms inside the transcript (mouse drag or keyboard: // both surface as selectionchange).
874
873
  let currentSelection: Partial<Selection> | null = {
875
874
  isCollapsed: false,
876
875
  anchorNode: scrollContainer,
@@ -888,7 +887,7 @@ describe("createAgentExperience streaming scroll", () => {
888
887
  // Auto-follow paused: the selection isn't dragged out from under the user.
889
888
  expect(metrics.getScrollTop()).toBe(600);
890
889
 
891
- // Drag-selecting toward the bottom edge auto-scrolls down that must
890
+ // Drag-selecting toward the bottom edge auto-scrolls down: that must
892
891
  // not read as a resume gesture while the selection is still active.
893
892
  metrics.setScrollTop(metrics.getBottomScrollTop());
894
893
  scrollContainer!.dispatchEvent(new Event("scroll"));
@@ -1023,7 +1022,7 @@ describe("createAgentExperience streaming scroll", () => {
1023
1022
  );
1024
1023
  expect(spacer?.style.height).toBe("84px");
1025
1024
 
1026
- // The spacer's height is invisible to the mocked scroll metrics apply
1025
+ // The spacer's height is invisible to the mocked scroll metrics: apply
1027
1026
  // it manually so the anchor target is reachable, as in a real browser.
1028
1027
  metrics.setScrollHeight(1084);
1029
1028
  raf.flush();
@@ -1078,7 +1077,7 @@ describe("createAgentExperience streaming scroll", () => {
1078
1077
  Object.defineProperty(bubble!, "offsetTop", { value: 500 });
1079
1078
 
1080
1079
  raf.step(1); // anchor frame: starts the scroll animation
1081
- raf.step(1); // first animation frame animation now in flight
1080
+ raf.step(1); // first animation frame: animation now in flight
1082
1081
 
1083
1082
  // Jump to the latest mid-animation: the stale anchor animation must not
1084
1083
  // keep easing scrollTop back toward the old target.
@@ -30,7 +30,7 @@ describe("createAgentExperience stop-streaming submit button", () => {
30
30
  vi.stubGlobal("cancelAnimationFrame", () => {});
31
31
  window.scrollTo = vi.fn();
32
32
 
33
- // Fetch hangs until the caller aborts the signal models an in-flight
33
+ // Fetch hangs until the caller aborts the signal: models an in-flight
34
34
  // SSE stream so the widget stays in the "streaming" state.
35
35
  global.fetch = vi.fn().mockImplementation((_url: string, options: any) => {
36
36
  const signal = options.signal as AbortSignal;
@@ -78,7 +78,7 @@ describe("createAgentExperience stop-streaming submit button", () => {
78
78
  expect(capturedSignals).toHaveLength(1);
79
79
  expect(capturedSignals[0].aborted).toBe(false);
80
80
 
81
- // Second click acts as "stop generating".
81
+ // Second click: acts as "stop generating".
82
82
  submit!.click();
83
83
 
84
84
  await flush();
@@ -89,8 +89,7 @@ describe("createAgentExperience stop-streaming submit button", () => {
89
89
  expect(capturedSignals).toHaveLength(1);
90
90
  // Typed text is preserved so the user can resend after stopping.
91
91
  expect(textarea.value).toBe("");
92
- // (The textarea was cleared on the *first* submit, not by the stop click
93
- // that's fine because after cancel the user can keep typing.)
92
+ // (The textarea was cleared on the *first* submit, not by the stop click: // that's fine because after cancel the user can keep typing.)
94
93
 
95
94
  controller.destroy();
96
95
  });
@@ -62,7 +62,7 @@ const injectSuggestReplies = (
62
62
  args: { suggestions },
63
63
  chunks: [],
64
64
  },
65
- // No executionId/awaitingLocalTool rendering is driven purely by the
65
+ // No executionId/awaitingLocalTool: rendering is driven purely by the
66
66
  // message list; the auto-resume path is covered in
67
67
  // suggest-replies-tool.test.ts.
68
68
  },
@@ -167,7 +167,7 @@ describe("suggest_replies chips UI", () => {
167
167
  expect(chipButtons(mount, "Tell me more")).toHaveLength(1);
168
168
 
169
169
  // A display-only config update (e.g. theme tweak) re-renders the
170
- // suggestions row it must re-apply the agent-chips rule, not fall back
170
+ // suggestions row: it must re-apply the agent-chips rule, not fall back
171
171
  // to the static config chips (which are hidden mid-conversation).
172
172
  controller.update({
173
173
  apiUrl: "https://api.example.com/chat",