@runtypelabs/persona 4.33.0 → 4.35.1

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 (220) hide show
  1. package/dist/activity-ui.d.cts +3 -15
  2. package/dist/activity-ui.d.ts +3 -15
  3. package/dist/animations/glyph-cycle.d.cts +1 -3
  4. package/dist/animations/glyph-cycle.d.ts +1 -3
  5. package/dist/animations/wipe.d.cts +1 -3
  6. package/dist/animations/wipe.d.ts +1 -3
  7. package/dist/approval-ui.cjs +2 -2
  8. package/dist/approval-ui.d.cts +3 -15
  9. package/dist/approval-ui.d.ts +3 -15
  10. package/dist/approval-ui.js +1 -1
  11. package/dist/artifacts-ui.cjs +1 -1
  12. package/dist/artifacts-ui.d.cts +3 -21
  13. package/dist/artifacts-ui.d.ts +3 -21
  14. package/dist/background-runtime.d.cts +2 -3
  15. package/dist/background-runtime.d.ts +2 -3
  16. package/dist/{chunk-DW7Vg_l2.js → chunk-B1P8w9_X.js} +1 -1
  17. package/dist/{chunk-D6U0E82p.js → chunk-B30Jdgg6.js} +1 -1
  18. package/dist/{chunk-gu34lr-P.js → chunk-Brdx4NCz.js} +1 -1
  19. package/dist/{chunk-6CUrH_jm.js → chunk-Bzo9GaAi.js} +1 -1
  20. package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
  21. package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
  22. package/dist/chunk-CNBMBx-Y.js +2 -0
  23. package/dist/{chunk-ql5N2I_t.js → chunk-CWzHNuVK.js} +1 -1
  24. package/dist/{chunk-DbGLjU2P.js → chunk-CYpt7PsO.js} +1 -1
  25. package/dist/chunk-Cd8mp8Sv.js +1 -0
  26. package/dist/chunk-CsfE8WRG.js +1 -0
  27. package/dist/{chunk-DOZmcve8.js → chunk-Csr59G5V.js} +1 -1
  28. package/dist/{chunk-Co3VVwGe.js → chunk-D2pHRNEq.js} +0 -0
  29. package/dist/chunk-D3kZO8u-.js +1 -0
  30. package/dist/chunk-DAthf5H3.js +1 -0
  31. package/dist/{chunk-CXQJG7pb.js → chunk-DZ_aillY.js} +2 -2
  32. package/dist/{chunk-6C23QQdP.js → chunk-DgJV9zbK.js} +1 -1
  33. package/dist/chunk-Dv6KtIT1.js +1 -0
  34. package/dist/chunk-Dws8IrdR.js +1 -0
  35. package/dist/chunk-ThxxD8oK.js +1 -0
  36. package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
  37. package/dist/chunk-kizhspe5.js +1 -0
  38. package/dist/client-dispatch.js +1 -1
  39. package/dist/client-history.js +1 -1
  40. package/dist/client-stream.js +1 -1
  41. package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
  42. package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
  43. package/dist/codegen.cjs +1 -1
  44. package/dist/codegen.d.cts +1 -1
  45. package/dist/codegen.d.ts +1 -1
  46. package/dist/codegen.js +1 -1
  47. package/dist/composer-actions-runtime.cjs +1 -1
  48. package/dist/composer-actions-runtime.d.cts +4 -20
  49. package/dist/composer-actions-runtime.d.ts +4 -20
  50. package/dist/composer-actions-runtime.js +1 -1
  51. package/dist/{composer-input-DnqGCXZz.d.ts → composer-input-BSDNPOqV.d.cts} +1 -7
  52. package/dist/{composer-input-DnqGCXZz.d.cts → composer-input-BSDNPOqV.d.ts} +1 -7
  53. package/dist/{composer-store-6Iwy6WLI.d.ts → composer-store-DC-gAjtN.d.cts} +1 -3
  54. package/dist/{composer-store-6Iwy6WLI.d.cts → composer-store-DC-gAjtN.d.ts} +1 -3
  55. package/dist/config-diagnostics.d.cts +0 -2
  56. package/dist/config-diagnostics.d.ts +0 -2
  57. package/dist/{context-mention-manager-B-rHF98n.d.ts → context-mention-manager-CCpeoGL2.d.cts} +2 -6
  58. package/dist/{context-mention-manager-B-rHF98n.d.cts → context-mention-manager-CCpeoGL2.d.ts} +2 -6
  59. package/dist/context-mentions-inline.cjs +1 -1
  60. package/dist/context-mentions-inline.d.cts +2 -4
  61. package/dist/context-mentions-inline.d.ts +2 -4
  62. package/dist/context-mentions-inline.js +1 -1
  63. package/dist/context-mentions.cjs +2 -2
  64. package/dist/context-mentions.d.cts +3 -9
  65. package/dist/context-mentions.d.ts +3 -9
  66. package/dist/context-mentions.js +1 -1
  67. package/dist/{defaults-BuOjqUAG.d.ts → defaults-tj5RcN8r.d.cts} +1 -5
  68. package/dist/{defaults-BuOjqUAG.d.cts → defaults-tj5RcN8r.d.ts} +1 -5
  69. package/dist/dictation-runtime.cjs +1 -1
  70. package/dist/dictation-runtime.d.cts +4 -13
  71. package/dist/dictation-runtime.d.ts +4 -13
  72. package/dist/dictation-runtime.js +1 -1
  73. package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.cts} +0 -2
  74. package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.ts} +0 -2
  75. package/dist/event-stream-view.cjs +1 -1
  76. package/dist/event-stream-view.d.cts +3 -16
  77. package/dist/event-stream-view.d.ts +3 -16
  78. package/dist/{fallback-speech-engine-SqEfkGda.d.ts → fallback-speech-engine-gkhbjLiZ.d.cts} +1 -3
  79. package/dist/{fallback-speech-engine-SqEfkGda.d.cts → fallback-speech-engine-gkhbjLiZ.d.ts} +1 -3
  80. package/dist/{feature-preferences-DqAmje-2.d.ts → feature-preferences-CerDWGZ7.d.cts} +1 -5
  81. package/dist/{feature-preferences-DqAmje-2.d.cts → feature-preferences-CerDWGZ7.d.ts} +1 -5
  82. package/dist/forms-ui.d.cts +2 -4
  83. package/dist/forms-ui.d.ts +2 -4
  84. package/dist/{history-confirm-dialog-B3L7QNcO.d.cts → history-confirm-dialog-CrkSB3Ra.d.cts} +2 -14
  85. package/dist/{history-confirm-dialog-B3L7QNcO.d.ts → history-confirm-dialog-CrkSB3Ra.d.ts} +2 -14
  86. package/dist/history-shell.cjs +1 -1
  87. package/dist/history-shell.d.cts +8 -27
  88. package/dist/history-shell.d.ts +8 -27
  89. package/dist/history-shell.js +1 -1
  90. package/dist/history-view.d.cts +1 -1
  91. package/dist/history-view.d.ts +1 -1
  92. package/dist/icons-extra.cjs +1 -1
  93. package/dist/icons-extra.d.cts +0 -2
  94. package/dist/icons-extra.d.ts +0 -2
  95. package/dist/index.cjs +44 -44
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.d.cts +10 -10
  98. package/dist/index.d.ts +10 -10
  99. package/dist/index.global.js +1 -1
  100. package/dist/index.global.js.map +1 -1
  101. package/dist/index.js +50 -50
  102. package/dist/index.js.map +1 -1
  103. package/dist/install.global.js +1 -1
  104. package/dist/install.global.js.map +1 -1
  105. package/dist/launcher.global.js +2 -2
  106. package/dist/launcher.global.js.map +1 -1
  107. package/dist/{loading-label-runtime-loader-JecjOfJS.d.ts → loading-label-runtime-loader-fWIRrhUF.d.cts} +1 -3
  108. package/dist/{loading-label-runtime-loader-JecjOfJS.d.cts → loading-label-runtime-loader-fWIRrhUF.d.ts} +1 -3
  109. package/dist/loading-label-runtime.cjs +1 -1
  110. package/dist/loading-label-runtime.d.cts +1 -1
  111. package/dist/loading-label-runtime.d.ts +1 -1
  112. package/dist/loading-label-runtime.js +1 -1
  113. package/dist/markdown-parsers.js +1 -1
  114. package/dist/{message-bubble-CBYtpDsT.d.ts → message-bubble-Czc0lOHr.d.cts} +20 -3
  115. package/dist/{message-bubble-CBYtpDsT.d.cts → message-bubble-Czc0lOHr.d.ts} +20 -3
  116. package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-C30jE-cw.d.cts} +3 -3
  117. package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-C30jE-cw.d.ts} +3 -3
  118. package/dist/plugin-kit.cjs +1 -1
  119. package/dist/plugin-kit.d.cts +1 -1
  120. package/dist/plugin-kit.d.ts +1 -1
  121. package/dist/plugin-kit.js +1 -1
  122. package/dist/preferences-runtime.cjs +1 -1
  123. package/dist/preferences-runtime.d.cts +1 -1
  124. package/dist/preferences-runtime.d.ts +1 -1
  125. package/dist/preferences-runtime.js +1 -1
  126. package/dist/{preview-_UJwwt2L.d.cts → preview-D-osV3FU.d.cts} +2 -6
  127. package/dist/{preview-_UJwwt2L.d.ts → preview-D-osV3FU.d.ts} +2 -6
  128. package/dist/{registry-B5ZV3K-h.d.ts → registry-B6w91tx0.d.cts} +1 -8
  129. package/dist/{registry-B5ZV3K-h.d.cts → registry-B6w91tx0.d.ts} +1 -8
  130. package/dist/{session-Bys8_XuB.d.cts → session-BUQrs3Of.d.cts} +10 -93
  131. package/dist/{session-Bys8_XuB.d.ts → session-BUQrs3Of.d.ts} +10 -93
  132. package/dist/session-reconnect.d.cts +2 -4
  133. package/dist/session-reconnect.d.ts +2 -4
  134. package/dist/smart-dom-reader.d.cts +2 -8
  135. package/dist/smart-dom-reader.d.ts +2 -8
  136. package/dist/testing.d.cts +0 -2
  137. package/dist/testing.d.ts +0 -2
  138. package/dist/theme-editor-preview.cjs +50 -50
  139. package/dist/theme-editor-preview.d.cts +1 -1
  140. package/dist/theme-editor-preview.d.ts +1 -1
  141. package/dist/theme-editor-preview.js +25 -25
  142. package/dist/theme-editor.cjs +25 -5
  143. package/dist/theme-editor.d.cts +2 -22
  144. package/dist/theme-editor.d.ts +2 -22
  145. package/dist/theme-editor.js +1 -1
  146. package/dist/theme-reference.cjs +1 -1
  147. package/dist/theme-reference.d.cts +2 -4
  148. package/dist/theme-reference.d.ts +2 -4
  149. package/dist/theme-reference.js +1 -1
  150. package/dist/{tool-loading-animation-d5qAQcwv.d.ts → tool-loading-animation-B7XUf8TN.d.cts} +1 -7
  151. package/dist/{tool-loading-animation-d5qAQcwv.d.cts → tool-loading-animation-B7XUf8TN.d.ts} +1 -7
  152. package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
  153. package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
  154. package/dist/{turn-activity-ui-q6fHVnx-.d.cts → turn-activity-ui-CohuNMHT.d.cts} +2 -12
  155. package/dist/{turn-activity-ui-q6fHVnx-.d.ts → turn-activity-ui-CohuNMHT.d.ts} +2 -12
  156. package/dist/turn-activity-ui.d.cts +1 -1
  157. package/dist/turn-activity-ui.d.ts +1 -1
  158. package/dist/{types-B_mj21jg.d.cts → types-9i0xcAMT.d.cts} +128 -29
  159. package/dist/{types-B_mj21jg.d.ts → types-9i0xcAMT.d.ts} +128 -29
  160. package/dist/ui-extras.js +3 -3
  161. package/dist/voice-client-tools.d.cts +1 -3
  162. package/dist/voice-client-tools.d.ts +1 -3
  163. package/dist/voice-runtime.cjs +1 -1
  164. package/dist/voice-runtime.d.cts +2 -12
  165. package/dist/voice-runtime.d.ts +2 -12
  166. package/dist/voice-runtime.js +2 -2
  167. package/dist/voice-worklet-player.d.cts +2 -6
  168. package/dist/voice-worklet-player.d.ts +2 -6
  169. package/dist/{webmcp-runtime-entry-ClEO3qdI.d.ts → webmcp-runtime-entry-XNaXa-9h.d.cts} +9 -5
  170. package/dist/{webmcp-runtime-entry-ClEO3qdI.d.cts → webmcp-runtime-entry-XNaXa-9h.d.ts} +9 -5
  171. package/dist/webmcp-runtime.cjs +1 -1
  172. package/dist/webmcp-runtime.d.cts +1 -1
  173. package/dist/webmcp-runtime.d.ts +1 -1
  174. package/dist/webmcp-runtime.js +1 -1
  175. package/dist/widget.css +1 -1
  176. package/package.json +5 -5
  177. package/src/client-dispatch.ts +13 -0
  178. package/src/client.ts +37 -2
  179. package/src/client.webmcp-require-confirm.test.ts +212 -0
  180. package/src/components/composer-model-picker.ts +0 -14
  181. package/src/components/composer-overflow-menu.ts +0 -15
  182. package/src/components/composer-tokens.ts +0 -20
  183. package/src/components/event-stream-view.test.ts +5 -0
  184. package/src/components/message-bubble.ts +26 -6
  185. package/src/components/scroll-to-bottom.test.ts +135 -0
  186. package/src/components/scroll-to-bottom.ts +50 -13
  187. package/src/components/user-message-collapse.ts +117 -0
  188. package/src/defaults.ts +6 -0
  189. package/src/generated/runtype-openapi-contract.ts +13 -0
  190. package/src/plugin-kit.test.ts +101 -0
  191. package/src/plugin-kit.ts +29 -1
  192. package/src/session.ts +6 -2
  193. package/src/session.webmcp.test.ts +32 -0
  194. package/src/styles/widget.css +56 -0
  195. package/src/theme-reference.ts +2 -2
  196. package/src/types/theme.ts +29 -0
  197. package/src/types.ts +84 -5
  198. package/src/ui.composer-models.test.ts +5 -7
  199. package/src/ui.composer-overflow.test.ts +14 -21
  200. package/src/ui.scroll-additive.test.ts +219 -117
  201. package/src/ui.scroll.test.ts +176 -13
  202. package/src/ui.ts +230 -107
  203. package/src/ui.turn-activity.test.ts +7 -7
  204. package/src/ui.user-message-collapse.test.ts +540 -0
  205. package/src/utils/auto-follow.test.ts +0 -38
  206. package/src/utils/auto-follow.ts +0 -35
  207. package/src/utils/context-mention-controller.test.ts +56 -0
  208. package/src/utils/theme.test.ts +23 -0
  209. package/src/utils/tokens.ts +16 -0
  210. package/src/webmcp-bridge.test.ts +102 -0
  211. package/src/webmcp-runtime-entry.ts +29 -1
  212. package/dist/chunk-BAkaTZpd.js +0 -1
  213. package/dist/chunk-BEDTp-dm.js +0 -1
  214. package/dist/chunk-C7-m0EmI.js +0 -1
  215. package/dist/chunk-CNhThrLX.js +0 -1
  216. package/dist/chunk-CVMIncEY.js +0 -1
  217. package/dist/chunk-CzLEhoB_.js +0 -2
  218. package/dist/chunk-DVgzoVCB.js +0 -1
  219. package/dist/chunk-d54BJdKd.js +0 -1
  220. package/dist/chunk-dLJWKfOb.js +0 -1
@@ -40,6 +40,49 @@ describe("scroll-to-bottom chrome", () => {
40
40
  control.destroy();
41
41
  });
42
42
 
43
+ it("names a pending approval below with a labelled shield pill, then reverts", () => {
44
+ const control = createScrollToBottomControl(icon, "transcript");
45
+ const config: AgentWidgetConfig = { future: { v5Defaults: true } };
46
+ control.update(config, true, false, 2, true);
47
+ const el = control.element;
48
+ expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
49
+ expect(el.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("true");
50
+ expect(el.textContent).toBe("Approval needed");
51
+ expect(el.querySelector("svg")?.getAttribute("data-icon")).toBe("shield-check");
52
+ // The label replaces the unread count; assistive tech hears where it leads.
53
+ expect(el.querySelector<HTMLElement>("[data-persona-scroll-to-bottom-count]")?.style.display).toBe("none");
54
+ expect(el.getAttribute("aria-label")).toBe("Approval needed below. Jump to latest");
55
+
56
+ // Hidden, the approval state never shows.
57
+ control.update(config, false, false, 0, true);
58
+ expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(false);
59
+
60
+ control.update(config, true, false, 2, false);
61
+ expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(false);
62
+ expect(el.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("false");
63
+ expect(el.querySelector("svg")?.getAttribute("data-icon")).toBe("arrow-down");
64
+ expect(el.getAttribute("aria-label")).toBe("Jump to latest (2 new)");
65
+ control.destroy();
66
+ });
67
+
68
+ it("uses a custom approval label, opts out with an empty one, and tells custom glyphs", () => {
69
+ const states: string[] = [];
70
+ const renderIcon = vi.fn((_context: AgentWidgetScrollToBottomIconContext) => ({
71
+ element: document.createElement("span"),
72
+ update: (next: AgentWidgetScrollToBottomIconContext) => { states.push(next.state.activity); },
73
+ }));
74
+ const control = createScrollToBottomControl(icon, "transcript");
75
+ control.update({ features: { scrollToBottom: { approvalLabel: "Review request", renderIcon } } }, true, false, 0, true);
76
+ expect(control.element.textContent).toBe("Review request");
77
+ expect(renderIcon.mock.calls[0][0].state.activity).toBe("approval-below");
78
+ control.update({ features: { scrollToBottom: { approvalLabel: "", renderIcon } } }, true, false, 1, true);
79
+ expect(control.element.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("false");
80
+ expect(control.element.getAttribute("aria-label")).toBe("Approval needed below. Jump to latest");
81
+ control.update({ features: { scrollToBottom: { renderIcon } } }, true, false, 0, false);
82
+ expect(states.at(-1)).toBe("idle");
83
+ control.destroy();
84
+ });
85
+
43
86
  it("makes fading controls inert immediately and returns focus only when owned, including Shadow DOM", () => {
44
87
  const host = document.createElement("div"); document.body.appendChild(host);
45
88
  const root = host.attachShadow({ mode: "open" });
@@ -122,6 +165,98 @@ describe("scroll-to-bottom chrome", () => {
122
165
  control.destroy(); warning.mockRestore();
123
166
  });
124
167
 
168
+ describe("approval morph", () => {
169
+ const stubAnimate = () => {
170
+ const calls: { target: Element; keyframes: Keyframe[]; options: KeyframeAnimationOptions; animation: { cancel: ReturnType<typeof vi.fn> } }[] = [];
171
+ const original = (HTMLElement.prototype as { animate?: unknown }).animate;
172
+ (HTMLElement.prototype as { animate?: unknown }).animate = function (this: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions) {
173
+ const animation = { cancel: vi.fn(), finished: new Promise<void>(() => {}) };
174
+ calls.push({ target: this, keyframes, options, animation });
175
+ return animation;
176
+ };
177
+ return { calls, restore: () => { (HTMLElement.prototype as { animate?: unknown }).animate = original; } };
178
+ };
179
+
180
+ it("morphs a visible arrow into the pill on the theme's motion tokens, and back", () => {
181
+ const { calls, restore } = stubAnimate();
182
+ try {
183
+ const control = createScrollToBottomControl(icon, "transcript");
184
+ document.body.appendChild(control.element);
185
+ control.element.style.setProperty("--persona-motion-duration-base", "0.3s");
186
+ control.element.style.setProperty("--persona-motion-easing", "cubic-bezier(0.2, 0, 0, 1)");
187
+ const config: AgentWidgetConfig = { future: { v5Defaults: true } };
188
+ control.update(config, true);
189
+ expect(calls).toHaveLength(0);
190
+
191
+ control.update(config, true, false, 0, true);
192
+ const [own, labelFade] = calls;
193
+ expect(own.target).toBe(control.element);
194
+ expect(own.keyframes).toHaveLength(2);
195
+ expect(own.keyframes[0]).toMatchObject({ overflow: "hidden" });
196
+ expect(own.keyframes[0]).toHaveProperty("width");
197
+ expect(own.keyframes[1]).toHaveProperty("backgroundColor");
198
+ expect(own.options).toEqual({ duration: 300, easing: "cubic-bezier(0.2, 0, 0, 1)" });
199
+ // The label fades in as the clipped pill widens.
200
+ expect(labelFade.target.textContent).toBe("Approval needed");
201
+ expect(calls).toHaveLength(2);
202
+
203
+ // Ordinary syncs in the same state leave a running morph alone.
204
+ control.update(config, true, false, 0, true);
205
+ expect(calls).toHaveLength(2);
206
+ expect(calls.every((call) => call.animation.cancel.mock.calls.length === 0)).toBe(true);
207
+
208
+ // Back to the arrow: the running morph is replaced, unclipped (the
209
+ // count badge hangs outside the button).
210
+ control.update(config, true, false, 2, false);
211
+ expect(calls.slice(0, 2).every((call) => call.animation.cancel.mock.calls.length === 1)).toBe(true);
212
+ expect(calls).toHaveLength(3);
213
+ expect(calls[2].keyframes[0]).not.toHaveProperty("overflow");
214
+ control.destroy();
215
+ } finally { restore(); }
216
+ });
217
+
218
+ it("swaps instantly when the theme turns motion off, and falls back when the duration is missing", () => {
219
+ const { calls, restore } = stubAnimate();
220
+ try {
221
+ const config: AgentWidgetConfig = { future: { v5Defaults: true } };
222
+ const off = createScrollToBottomControl(icon, "transcript");
223
+ off.element.style.setProperty("--persona-motion-duration-base", "0ms");
224
+ off.update(config, true);
225
+ off.update(config, true, false, 0, true);
226
+ expect(calls).toHaveLength(0);
227
+ expect(off.element.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
228
+ off.destroy();
229
+
230
+ const unset = createScrollToBottomControl(icon, "transcript");
231
+ unset.update(config, true);
232
+ unset.update(config, true, false, 0, true);
233
+ expect(calls[0].options.duration).toBe(200);
234
+ unset.destroy();
235
+ } finally { restore(); }
236
+ });
237
+
238
+ it("does not morph a control that appears, hides, or prefers reduced motion", () => {
239
+ const { calls, restore } = stubAnimate();
240
+ try {
241
+ const config: AgentWidgetConfig = { future: { v5Defaults: true } };
242
+ const control = createScrollToBottomControl(icon, "transcript");
243
+ control.update(config, false);
244
+ control.update(config, true, false, 0, true); // appears already as the pill
245
+ control.update(config, false, false, 0, true); // hides
246
+ expect(calls).toHaveLength(0);
247
+ control.destroy();
248
+
249
+ vi.stubGlobal("matchMedia", () => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }));
250
+ const reduced = createScrollToBottomControl(icon, "transcript");
251
+ reduced.update(config, true);
252
+ reduced.update(config, true, false, 0, true);
253
+ expect(calls).toHaveLength(0);
254
+ expect(reduced.element.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
255
+ reduced.destroy();
256
+ } finally { restore(); }
257
+ });
258
+ });
259
+
125
260
  it("provides live reduced-motion changes and releases its media listener", () => {
126
261
  let change: (() => void) | undefined;
127
262
  const media = { matches: true, addEventListener: vi.fn((_event, callback) => { change = callback; }), removeEventListener: vi.fn() };
@@ -25,9 +25,7 @@ export function createScrollToBottomControl(
25
25
  const label = createElement("span", "");
26
26
  const badge = createElement("span", "");
27
27
  badge.setAttribute("data-persona-scroll-to-bottom-count", "");
28
- element.appendChild(glyph);
29
- element.appendChild(label);
30
- element.appendChild(badge);
28
+ element.append(glyph, label, badge);
31
29
  let config: AgentWidgetConfig = {};
32
30
  let state: AgentWidgetScrollToBottomIconState = { activity: "idle", visible: false, hovered: false, focused: false, reducedMotion: false };
33
31
  // Keep renderer identity separate from its handle, including null fallbacks.
@@ -36,6 +34,26 @@ export function createScrollToBottomControl(
36
34
  let abort: AbortController | undefined;
37
35
  let builtin = "";
38
36
  const media = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
37
+ // Arrow <-> "Approval needed" pill: one button changing shape, so morph its
38
+ // width and colors on the theme's motion tokens while the new label fades in.
39
+ let morphs: Animation[] = [];
40
+ const paint = () => {
41
+ const style = getComputedStyle(element);
42
+ return { width: `${element.offsetWidth}px`, backgroundColor: style.backgroundColor, borderColor: style.borderColor, color: style.color };
43
+ };
44
+ const morph = (from: Keyframe) => {
45
+ const style = getComputedStyle(element);
46
+ const value = style.getPropertyValue("--persona-motion-duration-base");
47
+ const ms = parseFloat(value) * (value.includes("ms") ? 1 : 1000);
48
+ // "0ms" turns motion off; only a missing or invalid value falls back.
49
+ if (ms === 0) return;
50
+ const timing = { duration: ms > 0 ? ms : 200, easing: style.getPropertyValue("--persona-motion-easing").trim() || "ease-out" };
51
+ // Clip only while widening into the pill: the arrow's count badge hangs
52
+ // outside the button.
53
+ const clip = label.style.display === "none" ? null : { overflow: "hidden" };
54
+ morphs = [element.animate([{ ...from, ...clip }, { ...paint(), ...clip }], timing)];
55
+ if (clip) morphs.push(label.animate([{ opacity: 0 }, { opacity: 1 }], timing));
56
+ };
39
57
  state.reducedMotion = media?.matches ?? false;
40
58
  const warn = (error: unknown) => console.warn("Persona scroll icon failed", error);
41
59
  const context = (): AgentWidgetScrollToBottomIconContext => ({ config, state: { ...state }, view, signal: abort!.signal });
@@ -51,7 +69,9 @@ export function createScrollToBottomControl(
51
69
  if (handle?.element) return;
52
70
  const feature = config.features?.scrollToBottom;
53
71
  const dots = state.activity === "generating-below" && (feature?.streamingIndicator ?? (config.future?.v5Defaults ? "dots" : "arrow")) === "dots";
54
- const key = dots ? "dots" : `arrow:${feature?.iconName ?? "arrow-down"}`;
72
+ // Same glyph as the approval card it points at, so the two read as one thing.
73
+ const iconName = state.activity === "approval-below" ? "shield-check" : feature?.iconName ?? "arrow-down";
74
+ const key = dots ? "dots" : `arrow:${iconName}`;
55
75
  if (builtin === key) return;
56
76
  builtin = key;
57
77
  glyph.innerHTML = "";
@@ -64,7 +84,7 @@ export function createScrollToBottomControl(
64
84
  }
65
85
  glyph.appendChild(dotGroup);
66
86
  } else {
67
- const icon = renderIcon(feature?.iconName ?? "arrow-down", "14px", "currentColor", 2);
87
+ const icon = renderIcon(iconName, "14px", "currentColor", 2);
68
88
  if (icon) glyph.appendChild(icon);
69
89
  }
70
90
  };
@@ -106,11 +126,12 @@ export function createScrollToBottomControl(
106
126
 
107
127
  return {
108
128
  element,
109
- update(nextConfig: AgentWidgetConfig, visible: boolean, active = false, count = 0) {
129
+ /** `approval` marks a pending approval below the fold: the control names it. */
130
+ update(nextConfig: AgentWidgetConfig, visible: boolean, active = false, count = 0, approval = false) {
110
131
  const previous = config.features?.scrollToBottom;
111
132
  const next = nextConfig.features?.scrollToBottom;
112
133
  const configChanged = config.future?.v5Defaults !== nextConfig.future?.v5Defaults ||
113
- (["enabled", "iconName", "label", "streamingIndicator", "transition", "scrollBehavior", "scrollDurationMs"] as (keyof AgentWidgetScrollToBottomFeature)[])
134
+ (["enabled", "iconName", "label", "approvalLabel", "streamingIndicator", "transition", "scrollBehavior", "scrollDurationMs"] as (keyof AgentWidgetScrollToBottomFeature)[])
114
135
  .some((key) => previous?.[key] !== next?.[key]);
115
136
  config = nextConfig;
116
137
  const feature = config.features?.scrollToBottom;
@@ -132,21 +153,37 @@ export function createScrollToBottomControl(
132
153
  focusTarget.focus({ preventScroll: true });
133
154
  }
134
155
  }
135
- const text = feature?.label ?? "";
136
- const base = text || "Jump to latest";
137
- element.setAttribute("aria-label", `${base}${count > 0 ? ` (${count} new)` : ""}${active && visible ? " (response streaming below)" : ""}`);
156
+ // A waiting approval outranks streaming and the unread count: the turn is
157
+ // blocked on the reader, so the control says so in words.
158
+ const waiting = approval && visible;
159
+ // Morph only a visible control changing shape, never its fade in or out.
160
+ const reshape = state.visible && visible && (state.activity === "approval-below") !== waiting;
161
+ if (reshape || !visible) morphs.forEach((animation) => animation.cancel());
162
+ const from = reshape && !state.reducedMotion && typeof element.animate === "function" ? paint() : null;
163
+ const approvalText = feature?.approvalLabel ?? "Approval needed";
164
+ const text = waiting && approvalText ? approvalText : feature?.label ?? "";
165
+ const base = feature?.label || "Jump to latest";
166
+ element.setAttribute("aria-label", waiting
167
+ ? `${approvalText || "Approval needed"} below. ${base}`
168
+ : `${base}${count > 0 ? ` (${count} new)` : ""}${active && visible ? " (response streaming below)" : ""}`);
138
169
  element.title = text;
139
170
  element.setAttribute("data-persona-scroll-to-bottom-has-label", String(Boolean(text)));
171
+ if (waiting) element.setAttribute("data-persona-scroll-to-bottom-approval", "");
172
+ else element.removeAttribute("data-persona-scroll-to-bottom-approval");
140
173
  label.textContent = text;
141
174
  label.style.display = text ? "" : "none";
142
- badge.textContent = count > 0 ? String(count) : "";
143
- badge.style.display = count > 0 ? "" : "none";
144
- const nextState = { ...state, visible, hovered: visible && state.hovered, focused: visible && state.focused, activity: active && visible ? "generating-below" as const : "idle" as const };
175
+ const shownCount = waiting ? 0 : count;
176
+ badge.textContent = shownCount > 0 ? String(shownCount) : "";
177
+ badge.style.display = shownCount > 0 ? "" : "none";
178
+ const activity = waiting ? "approval-below" as const : active && visible ? "generating-below" as const : "idle" as const;
179
+ const nextState = { ...state, visible, hovered: visible && state.hovered, focused: visible && state.focused, activity };
145
180
  const changed = configChanged || stateChanged(nextState);
146
181
  state = nextState;
147
182
  updateRenderer(changed);
183
+ if (from) morph(from);
148
184
  },
149
185
  destroy() {
186
+ morphs.forEach((animation) => animation.cancel());
150
187
  dispose();
151
188
  lifetime.abort();
152
189
  media?.removeEventListener?.("change", motion);
@@ -0,0 +1,117 @@
1
+ import type { AgentWidgetConfig } from "../types";
2
+ import { createNode } from "../utils/dom";
3
+
4
+ /**
5
+ * Long user message collapse ("Show more" / "Show less").
6
+ *
7
+ * The default user bubble wraps its content (attachments, mention chips and
8
+ * text) in a clip region. ui.ts measures the region's real rendered height
9
+ * after each morph and decides whether it overflows; the bubble is then
10
+ * re-rendered from that state, so morphs and cached wrappers always agree
11
+ * with the live DOM. The full text stays in the DOM in every state (no nested
12
+ * scroller), so find-in-page, selection, copy and screen readers still reach it.
13
+ */
14
+
15
+ /**
16
+ * Default collapsed height. A bubble line is ~22–24px (0.875rem at
17
+ * leading-relaxed/1.75), so 240px shows about ten lines: enough to recognise
18
+ * the message, while leaving most of a ~640px panel for the reply that
19
+ * anchor-top streams in beneath it.
20
+ */
21
+ export const DEFAULT_USER_MESSAGE_COLLAPSE_MAX_HEIGHT = 240;
22
+
23
+ /**
24
+ * Content within this many px (about two lines) of the limit stays expanded,
25
+ * so the toggle never hides just a line or two behind a button of its own size.
26
+ */
27
+ export const USER_MESSAGE_COLLAPSE_SLACK = 48;
28
+
29
+ /**
30
+ * - `fit`: not measured yet, or short enough; no toggle.
31
+ * - `live`: an interim voice transcript; expanded so the speaker can watch it.
32
+ * - `settling`: the transcript's first final render, still expanded; the
33
+ * post-morph measurement settles it in the same pass, holding the viewport.
34
+ * - `collapsed` / `expanded`: overflowing, with the toggle.
35
+ */
36
+ export type UserMessageCollapseState = "fit" | "live" | "settling" | "collapsed" | "expanded";
37
+
38
+ export type UserMessageCollapseRender = {
39
+ state: UserMessageCollapseState;
40
+ maxHeight: number;
41
+ showMoreLabel: string;
42
+ showLessLabel: string;
43
+ };
44
+
45
+ export const USER_COLLAPSE_REGION_ATTR = "data-persona-user-collapse";
46
+ export const USER_COLLAPSE_TOGGLE_ATTR = "data-persona-user-collapse-toggle";
47
+
48
+ export const isUserMessageCollapseEnabled = (config: AgentWidgetConfig): boolean =>
49
+ config.features?.userMessageCollapse?.enabled === true;
50
+
51
+ export const resolveUserMessageCollapseMaxHeight = (config: AgentWidgetConfig): number => {
52
+ const value = config.features?.userMessageCollapse?.maxHeight;
53
+ return typeof value === "number" && Number.isFinite(value) && value > 0
54
+ ? value
55
+ : DEFAULT_USER_MESSAGE_COLLAPSE_MAX_HEIGHT;
56
+ };
57
+
58
+ export const resolveUserMessageCollapseLabels = (
59
+ config: AgentWidgetConfig
60
+ ): Pick<UserMessageCollapseRender, "showMoreLabel" | "showLessLabel"> => ({
61
+ showMoreLabel: config.copy?.showMoreLabel?.trim() || "Show more",
62
+ showLessLabel: config.copy?.showLessLabel?.trim() || "Show less",
63
+ });
64
+
65
+ /** Whether a measured natural content height warrants collapsing. */
66
+ export const userMessageOverflows = (naturalHeight: number, maxHeight: number): boolean =>
67
+ naturalHeight > maxHeight + USER_MESSAGE_COLLAPSE_SLACK;
68
+
69
+ const regionId = (messageId: string) => `persona-user-collapse-${messageId}`;
70
+
71
+ export const createUserCollapseRegion = (
72
+ messageId: string,
73
+ maxHeight: number
74
+ ): HTMLElement => {
75
+ const region = createNode("div", {
76
+ className: "persona-user-collapse-region",
77
+ attrs: { id: regionId(messageId), [USER_COLLAPSE_REGION_ATTR]: "fit" },
78
+ });
79
+ region.style.setProperty("--persona-user-collapse-max-height", `${maxHeight}px`);
80
+ return region;
81
+ };
82
+
83
+ /**
84
+ * Apply a state to a region and its sibling toggle. Used both while building a
85
+ * fresh bubble and to patch the live bubble in place on a toggle or a new
86
+ * measurement, so the toggle keeps its identity (and focus) across states.
87
+ */
88
+ export const applyUserCollapseState = (
89
+ bubble: HTMLElement,
90
+ region: HTMLElement,
91
+ messageId: string,
92
+ render: UserMessageCollapseRender
93
+ ): void => {
94
+ const { state } = render;
95
+ region.setAttribute(USER_COLLAPSE_REGION_ATTR, state);
96
+ region.style.setProperty("--persona-user-collapse-max-height", `${render.maxHeight}px`);
97
+ let toggle = bubble.querySelector<HTMLButtonElement>(`button[${USER_COLLAPSE_TOGGLE_ATTR}]`);
98
+ if (state !== "collapsed" && state !== "expanded") {
99
+ toggle?.remove();
100
+ return;
101
+ }
102
+ if (!toggle) {
103
+ toggle = createNode("button", {
104
+ className: "persona-user-collapse-toggle",
105
+ attrs: {
106
+ type: "button",
107
+ id: `persona-user-collapse-toggle-${messageId}`,
108
+ [USER_COLLAPSE_TOGGLE_ATTR]: "",
109
+ "aria-controls": regionId(messageId),
110
+ },
111
+ }) as HTMLButtonElement;
112
+ region.after(toggle);
113
+ }
114
+ const expanded = state === "expanded";
115
+ toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
116
+ toggle.textContent = expanded ? render.showLessLabel : render.showMoreLabel;
117
+ };
package/src/defaults.ts CHANGED
@@ -266,6 +266,9 @@ export const DEFAULTS_V5: Partial<AgentWidgetConfig> = {
266
266
  features: {
267
267
  artifacts: { enabled: true },
268
268
  scrollToBottom: { streamingIndicator: "dots", transition: "fade", scrollBehavior: "smooth" },
269
+ // Long sent messages (pasted text, voice transcripts) would otherwise fill
270
+ // the viewport under the anchor-top pin and push the reply out of view.
271
+ userMessageCollapse: { enabled: true },
269
272
  toolCallDisplay: DEFAULT_TOOL_CALL_DISPLAY_V5,
270
273
  reasoningDisplay: DEFAULT_REASONING_DISPLAY_V5,
271
274
  },
@@ -382,6 +385,8 @@ export function mergeWithDefaults(
382
385
  const csa = config.features?.streamAnimation;
383
386
  const dau = defaults.features?.askUserQuestion;
384
387
  const cau = config.features?.askUserQuestion;
388
+ const duc = defaults.features?.userMessageCollapse;
389
+ const cuc = config.features?.userMessageCollapse;
385
390
  const dh = defaults.features?.history;
386
391
  const ch = config.features?.history;
387
392
  const mergedArtifacts =
@@ -459,6 +464,7 @@ export function mergeWithDefaults(
459
464
  ...(mergedStreamAnimation !== undefined ? { streamAnimation: mergedStreamAnimation } : {}),
460
465
  ...(mergedAskUserQuestion !== undefined ? { askUserQuestion: mergedAskUserQuestion } : {}),
461
466
  ...(mergedHistory !== undefined ? { history: mergedHistory } : {}),
467
+ ...(duc !== undefined || cuc !== undefined ? { userMessageCollapse: { ...duc, ...cuc } } : {}),
462
468
  };
463
469
  })(),
464
470
  suggestionChips: config.suggestionChips ?? defaults.suggestionChips,
@@ -36,6 +36,12 @@ export type RuntypeExecutionStreamEvent = ({
36
36
  iterations?: number;
37
37
  kind: "agent" | "flow";
38
38
  outcome?: "completed" | "error" | "aborted";
39
+ output?: {
40
+ validation?: {
41
+ errored?: Array<string>;
42
+ failed?: Array<string>;
43
+ };
44
+ };
39
45
  seq: number;
40
46
  stepErrorCount?: number;
41
47
  stopReason?: string;
@@ -48,7 +54,12 @@ export type RuntypeExecutionStreamEvent = ({
48
54
  output: number;
49
55
  };
50
56
  totalTokensUsed?: number;
57
+ truncatedStepCount?: number;
51
58
  type: "execution_complete";
59
+ warnings?: Array<{
60
+ code: string;
61
+ message: string;
62
+ }>;
52
63
  }) | ({
53
64
  blockReason?: string;
54
65
  code?: string;
@@ -216,6 +227,7 @@ export type RuntypeExecutionStreamEvent = ({
216
227
  };
217
228
  success?: boolean;
218
229
  tokensUsed?: number;
230
+ truncated?: boolean;
219
231
  type: "step_complete";
220
232
  unresolvedVariables?: Array<string>;
221
233
  }) | {
@@ -439,6 +451,7 @@ export type RuntypeExecutionStreamEvent = ({
439
451
  toolCallId: string;
440
452
  type: "tool_output_delta";
441
453
  } | {
454
+ completedAt?: string;
442
455
  error?: string;
443
456
  executionId: string;
444
457
  executionTime?: number;
@@ -262,6 +262,107 @@ describe("createPopover", () => {
262
262
  });
263
263
  });
264
264
 
265
+ describe("createPopover theme carry-over", () => {
266
+ const setupRoot = () => {
267
+ const root = document.createElement("div");
268
+ root.setAttribute("data-persona-root", "true");
269
+ root.setAttribute("data-persona-color-scheme", "dark");
270
+ root.setAttribute("data-persona-defaults", "v5");
271
+ root.style.setProperty("--persona-surface", "#17181c");
272
+ root.style.setProperty("--persona-border", "#2e3038");
273
+ root.style.setProperty("--persona-text", "#f4f4f5");
274
+ root.style.height = "100%";
275
+ const anchor = document.createElement("button");
276
+ root.appendChild(anchor);
277
+ document.body.appendChild(root);
278
+ const content = document.createElement("div");
279
+ return { root, anchor, content };
280
+ };
281
+
282
+ it("copies the root's theme variables and scheme onto body-mounted content", () => {
283
+ const { anchor, content } = setupRoot();
284
+ createPopover({ anchor, content }).open();
285
+
286
+ expect(content.parentElement).toBe(document.body);
287
+ expect(content.style.getPropertyValue("--persona-surface")).toBe("#17181c");
288
+ expect(content.style.getPropertyValue("--persona-border")).toBe("#2e3038");
289
+ expect(content.style.getPropertyValue("--persona-text")).toBe("#f4f4f5");
290
+ expect(content.getAttribute("data-persona-color-scheme")).toBe("dark");
291
+ expect(content.getAttribute("data-persona-defaults")).toBe("v5");
292
+ // Only custom properties travel, never the root's own layout.
293
+ expect(content.style.height).toBe("");
294
+ // Not a second widget root: root-scoped rules and lookups stay unambiguous.
295
+ expect(content.hasAttribute("data-persona-root")).toBe(false);
296
+ });
297
+
298
+ it("follows a live scheme change while open and clears removed variables", async () => {
299
+ const { root, anchor, content } = setupRoot();
300
+ createPopover({ anchor, content }).open();
301
+
302
+ root.style.setProperty("--persona-surface", "#ffffff");
303
+ root.style.removeProperty("--persona-border");
304
+ root.setAttribute("data-persona-color-scheme", "light");
305
+ await flushMicrotasks();
306
+
307
+ expect(content.style.getPropertyValue("--persona-surface")).toBe("#ffffff");
308
+ expect(content.style.getPropertyValue("--persona-border")).toBe("");
309
+ expect(content.getAttribute("data-persona-color-scheme")).toBe("light");
310
+ });
311
+
312
+ it("stops following the root once closed and refreshes on the next open", async () => {
313
+ const { root, anchor, content } = setupRoot();
314
+ const popover = createPopover({ anchor, content });
315
+ popover.open();
316
+ popover.close();
317
+
318
+ root.style.setProperty("--persona-surface", "#ffffff");
319
+ await flushMicrotasks();
320
+ expect(content.style.getPropertyValue("--persona-surface")).toBe("#17181c");
321
+
322
+ popover.open();
323
+ expect(content.style.getPropertyValue("--persona-surface")).toBe("#ffffff");
324
+ });
325
+
326
+ it("leaves content mounted inside the root to plain inheritance", () => {
327
+ const { root, anchor, content } = setupRoot();
328
+ createPopover({ anchor, content, container: root }).open();
329
+
330
+ expect(content.parentElement).toBe(root);
331
+ expect(content.style.getPropertyValue("--persona-surface")).toBe("");
332
+ expect(content.hasAttribute("data-persona-color-scheme")).toBe(false);
333
+ });
334
+
335
+ it("carries the theme into a shadow root, where content sits beside the root", () => {
336
+ const host = document.createElement("div");
337
+ document.body.appendChild(host);
338
+ const shadow = host.attachShadow({ mode: "open" });
339
+ const root = document.createElement("div");
340
+ root.setAttribute("data-persona-root", "true");
341
+ root.setAttribute("data-persona-color-scheme", "dark");
342
+ root.style.setProperty("--persona-surface", "#17181c");
343
+ const anchor = document.createElement("button");
344
+ root.appendChild(anchor);
345
+ shadow.appendChild(root);
346
+ const content = document.createElement("div");
347
+
348
+ createPopover({ anchor, content }).open();
349
+
350
+ expect(content.parentNode).toBe(shadow);
351
+ expect(content.style.getPropertyValue("--persona-surface")).toBe("#17181c");
352
+ expect(content.getAttribute("data-persona-color-scheme")).toBe("dark");
353
+ });
354
+
355
+ it("does nothing for an anchor outside any widget root", () => {
356
+ const anchor = document.createElement("button");
357
+ document.body.appendChild(anchor);
358
+ const content = document.createElement("div");
359
+ createPopover({ anchor, content }).open();
360
+
361
+ expect(content.style.cssText).not.toContain("--persona");
362
+ expect(content.hasAttribute("data-persona-color-scheme")).toBe(false);
363
+ });
364
+ });
365
+
265
366
  describe("viewport-constrained popovers", () => {
266
367
  const rect = (left: number, top: number, width: number, height: number): DOMRect =>
267
368
  ({ left, top, right: left + width, bottom: top + height, width, height }) as DOMRect;
package/src/plugin-kit.ts CHANGED
@@ -156,7 +156,9 @@ export interface PopoverOptions {
156
156
  /**
157
157
  * Where to mount `content`. Defaults to the anchor's shadow root (when
158
158
  * shadowed), its open dialog, or `document.body`: keeping it inside the same style + stacking
159
- * scope as the anchor while escaping the transcript's scroll clipping.
159
+ * scope as the anchor while escaping the transcript's scroll clipping. Mounted
160
+ * outside the anchor's `[data-persona-root]`, `content` carries a copy of the
161
+ * root's theme variables and color scheme, kept current while open.
160
162
  */
161
163
  container?: HTMLElement | ShadowRoot;
162
164
  onOpen?: () => void;
@@ -216,6 +218,24 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
216
218
  const currentContainer = () => options.container ?? defaultContainer(anchor);
217
219
  let open = false;
218
220
  let detach: (() => void) | null = null;
221
+ let carried: string[] = [];
222
+
223
+ // The theme lives on the widget root as inline custom properties plus scheme
224
+ // attributes. Content mounted outside that root (body, dialog, shadow root)
225
+ // would paint the stylesheet's light fallbacks, so it carries a copy.
226
+ const carryTheme = (): void => {
227
+ const root = anchor.closest<HTMLElement>("[data-persona-root]");
228
+ if (!root || root.contains(content)) return;
229
+ const { style } = root;
230
+ for (const name of carried) if (!style.getPropertyValue(name)) content.style.removeProperty(name);
231
+ carried = Array.from(style).filter((name) => name.startsWith("--"));
232
+ for (const name of carried) content.style.setProperty(name, style.getPropertyValue(name));
233
+ for (const attr of ["data-persona-color-scheme", "data-persona-defaults"]) {
234
+ const value = root.getAttribute(attr);
235
+ if (value) content.setAttribute(attr, value);
236
+ else content.removeAttribute(attr);
237
+ }
238
+ };
219
239
 
220
240
  const reposition = (): void => {
221
241
  if (!open) return;
@@ -224,6 +244,7 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
224
244
  const focused = content.contains(content.ownerDocument.activeElement)
225
245
  ? content.ownerDocument.activeElement as HTMLElement : null;
226
246
  container.appendChild(content);
247
+ carryTheme();
227
248
  focused?.focus({ preventScroll: true });
228
249
  }
229
250
  const rect = options.getAnchorRect?.() ?? anchor.getBoundingClientRect();
@@ -321,9 +342,16 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
321
342
  ownerWindow.addEventListener("resize", onReposition);
322
343
  const moveRoot = anchor.closest("[data-persona-root]");
323
344
  moveRoot?.addEventListener("persona:internal:host-moved", onReposition);
345
+ // A live theme or color-scheme change rewrites the root while open.
346
+ let themeObserver: MutationObserver | undefined;
347
+ if (moveRoot && ownerWindow.MutationObserver) {
348
+ themeObserver = new ownerWindow.MutationObserver(carryTheme);
349
+ themeObserver.observe(moveRoot, { attributeFilter: ["style", "data-persona-color-scheme"] });
350
+ }
324
351
 
325
352
  detach = () => {
326
353
  observer?.disconnect();
354
+ themeObserver?.disconnect();
327
355
  ownerWindow.clearTimeout(armId);
328
356
  ownerDocument.removeEventListener("pointerdown", onOutside, true);
329
357
  ownerWindow.removeEventListener("scroll", onReposition, true);
package/src/session.ts CHANGED
@@ -3305,9 +3305,13 @@ export class AgentWidgetSession {
3305
3305
  public requestWebMcpApproval(info: WebMcpConfirmInfo): Promise<boolean> {
3306
3306
  // Per-tool policy hook: auto-allow opted-out tools without any UI. A
3307
3307
  // throwing predicate must not block the call, so fall through to an
3308
- // explicit gate on error.
3308
+ // explicit gate on error. A `requireConfirmFor` match (flagged by the
3309
+ // bridge) never consults it: the surface owner said it must be confirmed.
3309
3310
  try {
3310
- if (this.config.webmcp?.autoApprove?.(info) === true) {
3311
+ if (
3312
+ info.confirmRequired !== true &&
3313
+ this.config.webmcp?.autoApprove?.(info) === true
3314
+ ) {
3311
3315
  return Promise.resolve(true);
3312
3316
  }
3313
3317
  } catch {