@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.
- package/dist/activity-ui.d.cts +3 -15
- package/dist/activity-ui.d.ts +3 -15
- package/dist/animations/glyph-cycle.d.cts +1 -3
- package/dist/animations/glyph-cycle.d.ts +1 -3
- package/dist/animations/wipe.d.cts +1 -3
- package/dist/animations/wipe.d.ts +1 -3
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +3 -15
- package/dist/approval-ui.d.ts +3 -15
- package/dist/approval-ui.js +1 -1
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +3 -21
- package/dist/artifacts-ui.d.ts +3 -21
- package/dist/background-runtime.d.cts +2 -3
- package/dist/background-runtime.d.ts +2 -3
- package/dist/{chunk-DW7Vg_l2.js → chunk-B1P8w9_X.js} +1 -1
- package/dist/{chunk-D6U0E82p.js → chunk-B30Jdgg6.js} +1 -1
- package/dist/{chunk-gu34lr-P.js → chunk-Brdx4NCz.js} +1 -1
- package/dist/{chunk-6CUrH_jm.js → chunk-Bzo9GaAi.js} +1 -1
- package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
- package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
- package/dist/chunk-CNBMBx-Y.js +2 -0
- package/dist/{chunk-ql5N2I_t.js → chunk-CWzHNuVK.js} +1 -1
- package/dist/{chunk-DbGLjU2P.js → chunk-CYpt7PsO.js} +1 -1
- package/dist/chunk-Cd8mp8Sv.js +1 -0
- package/dist/chunk-CsfE8WRG.js +1 -0
- package/dist/{chunk-DOZmcve8.js → chunk-Csr59G5V.js} +1 -1
- package/dist/{chunk-Co3VVwGe.js → chunk-D2pHRNEq.js} +0 -0
- package/dist/chunk-D3kZO8u-.js +1 -0
- package/dist/chunk-DAthf5H3.js +1 -0
- package/dist/{chunk-CXQJG7pb.js → chunk-DZ_aillY.js} +2 -2
- package/dist/{chunk-6C23QQdP.js → chunk-DgJV9zbK.js} +1 -1
- package/dist/chunk-Dv6KtIT1.js +1 -0
- package/dist/chunk-Dws8IrdR.js +1 -0
- package/dist/chunk-ThxxD8oK.js +1 -0
- package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
- package/dist/chunk-kizhspe5.js +1 -0
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
- package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +4 -20
- package/dist/composer-actions-runtime.d.ts +4 -20
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-DnqGCXZz.d.ts → composer-input-BSDNPOqV.d.cts} +1 -7
- package/dist/{composer-input-DnqGCXZz.d.cts → composer-input-BSDNPOqV.d.ts} +1 -7
- package/dist/{composer-store-6Iwy6WLI.d.ts → composer-store-DC-gAjtN.d.cts} +1 -3
- package/dist/{composer-store-6Iwy6WLI.d.cts → composer-store-DC-gAjtN.d.ts} +1 -3
- package/dist/config-diagnostics.d.cts +0 -2
- package/dist/config-diagnostics.d.ts +0 -2
- package/dist/{context-mention-manager-B-rHF98n.d.ts → context-mention-manager-CCpeoGL2.d.cts} +2 -6
- package/dist/{context-mention-manager-B-rHF98n.d.cts → context-mention-manager-CCpeoGL2.d.ts} +2 -6
- package/dist/context-mentions-inline.cjs +1 -1
- package/dist/context-mentions-inline.d.cts +2 -4
- package/dist/context-mentions-inline.d.ts +2 -4
- package/dist/context-mentions-inline.js +1 -1
- package/dist/context-mentions.cjs +2 -2
- package/dist/context-mentions.d.cts +3 -9
- package/dist/context-mentions.d.ts +3 -9
- package/dist/context-mentions.js +1 -1
- package/dist/{defaults-BuOjqUAG.d.ts → defaults-tj5RcN8r.d.cts} +1 -5
- package/dist/{defaults-BuOjqUAG.d.cts → defaults-tj5RcN8r.d.ts} +1 -5
- package/dist/dictation-runtime.cjs +1 -1
- package/dist/dictation-runtime.d.cts +4 -13
- package/dist/dictation-runtime.d.ts +4 -13
- package/dist/dictation-runtime.js +1 -1
- package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.cts} +0 -2
- package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.ts} +0 -2
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +3 -16
- package/dist/event-stream-view.d.ts +3 -16
- package/dist/{fallback-speech-engine-SqEfkGda.d.ts → fallback-speech-engine-gkhbjLiZ.d.cts} +1 -3
- package/dist/{fallback-speech-engine-SqEfkGda.d.cts → fallback-speech-engine-gkhbjLiZ.d.ts} +1 -3
- package/dist/{feature-preferences-DqAmje-2.d.ts → feature-preferences-CerDWGZ7.d.cts} +1 -5
- package/dist/{feature-preferences-DqAmje-2.d.cts → feature-preferences-CerDWGZ7.d.ts} +1 -5
- package/dist/forms-ui.d.cts +2 -4
- package/dist/forms-ui.d.ts +2 -4
- package/dist/{history-confirm-dialog-B3L7QNcO.d.cts → history-confirm-dialog-CrkSB3Ra.d.cts} +2 -14
- package/dist/{history-confirm-dialog-B3L7QNcO.d.ts → history-confirm-dialog-CrkSB3Ra.d.ts} +2 -14
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +8 -27
- package/dist/history-shell.d.ts +8 -27
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/icons-extra.cjs +1 -1
- package/dist/icons-extra.d.cts +0 -2
- package/dist/icons-extra.d.ts +0 -2
- package/dist/index.cjs +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +50 -50
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/{loading-label-runtime-loader-JecjOfJS.d.ts → loading-label-runtime-loader-fWIRrhUF.d.cts} +1 -3
- package/dist/{loading-label-runtime-loader-JecjOfJS.d.cts → loading-label-runtime-loader-fWIRrhUF.d.ts} +1 -3
- package/dist/loading-label-runtime.cjs +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/loading-label-runtime.js +1 -1
- package/dist/markdown-parsers.js +1 -1
- package/dist/{message-bubble-CBYtpDsT.d.ts → message-bubble-Czc0lOHr.d.cts} +20 -3
- package/dist/{message-bubble-CBYtpDsT.d.cts → message-bubble-Czc0lOHr.d.ts} +20 -3
- package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-C30jE-cw.d.cts} +3 -3
- package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-C30jE-cw.d.ts} +3 -3
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/plugin-kit.js +1 -1
- package/dist/preferences-runtime.cjs +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/preferences-runtime.js +1 -1
- package/dist/{preview-_UJwwt2L.d.cts → preview-D-osV3FU.d.cts} +2 -6
- package/dist/{preview-_UJwwt2L.d.ts → preview-D-osV3FU.d.ts} +2 -6
- package/dist/{registry-B5ZV3K-h.d.ts → registry-B6w91tx0.d.cts} +1 -8
- package/dist/{registry-B5ZV3K-h.d.cts → registry-B6w91tx0.d.ts} +1 -8
- package/dist/{session-Bys8_XuB.d.cts → session-BUQrs3Of.d.cts} +10 -93
- package/dist/{session-Bys8_XuB.d.ts → session-BUQrs3Of.d.ts} +10 -93
- package/dist/session-reconnect.d.cts +2 -4
- package/dist/session-reconnect.d.ts +2 -4
- package/dist/smart-dom-reader.d.cts +2 -8
- package/dist/smart-dom-reader.d.ts +2 -8
- package/dist/testing.d.cts +0 -2
- package/dist/testing.d.ts +0 -2
- package/dist/theme-editor-preview.cjs +50 -50
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +25 -25
- package/dist/theme-editor.cjs +25 -5
- package/dist/theme-editor.d.cts +2 -22
- package/dist/theme-editor.d.ts +2 -22
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -4
- package/dist/theme-reference.d.ts +2 -4
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-d5qAQcwv.d.ts → tool-loading-animation-B7XUf8TN.d.cts} +1 -7
- package/dist/{tool-loading-animation-d5qAQcwv.d.cts → tool-loading-animation-B7XUf8TN.d.ts} +1 -7
- package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
- package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
- package/dist/{turn-activity-ui-q6fHVnx-.d.cts → turn-activity-ui-CohuNMHT.d.cts} +2 -12
- package/dist/{turn-activity-ui-q6fHVnx-.d.ts → turn-activity-ui-CohuNMHT.d.ts} +2 -12
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-B_mj21jg.d.cts → types-9i0xcAMT.d.cts} +128 -29
- package/dist/{types-B_mj21jg.d.ts → types-9i0xcAMT.d.ts} +128 -29
- package/dist/ui-extras.js +3 -3
- package/dist/voice-client-tools.d.cts +1 -3
- package/dist/voice-client-tools.d.ts +1 -3
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -12
- package/dist/voice-runtime.d.ts +2 -12
- package/dist/voice-runtime.js +2 -2
- package/dist/voice-worklet-player.d.cts +2 -6
- package/dist/voice-worklet-player.d.ts +2 -6
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.ts → webmcp-runtime-entry-XNaXa-9h.d.cts} +9 -5
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.cts → webmcp-runtime-entry-XNaXa-9h.d.ts} +9 -5
- package/dist/webmcp-runtime.cjs +1 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/webmcp-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +5 -5
- package/src/client-dispatch.ts +13 -0
- package/src/client.ts +37 -2
- package/src/client.webmcp-require-confirm.test.ts +212 -0
- package/src/components/composer-model-picker.ts +0 -14
- package/src/components/composer-overflow-menu.ts +0 -15
- package/src/components/composer-tokens.ts +0 -20
- package/src/components/event-stream-view.test.ts +5 -0
- package/src/components/message-bubble.ts +26 -6
- package/src/components/scroll-to-bottom.test.ts +135 -0
- package/src/components/scroll-to-bottom.ts +50 -13
- package/src/components/user-message-collapse.ts +117 -0
- package/src/defaults.ts +6 -0
- package/src/generated/runtype-openapi-contract.ts +13 -0
- package/src/plugin-kit.test.ts +101 -0
- package/src/plugin-kit.ts +29 -1
- package/src/session.ts +6 -2
- package/src/session.webmcp.test.ts +32 -0
- package/src/styles/widget.css +56 -0
- package/src/theme-reference.ts +2 -2
- package/src/types/theme.ts +29 -0
- package/src/types.ts +84 -5
- package/src/ui.composer-models.test.ts +5 -7
- package/src/ui.composer-overflow.test.ts +14 -21
- package/src/ui.scroll-additive.test.ts +219 -117
- package/src/ui.scroll.test.ts +176 -13
- package/src/ui.ts +230 -107
- package/src/ui.turn-activity.test.ts +7 -7
- package/src/ui.user-message-collapse.test.ts +540 -0
- package/src/utils/auto-follow.test.ts +0 -38
- package/src/utils/auto-follow.ts +0 -35
- package/src/utils/context-mention-controller.test.ts +56 -0
- package/src/utils/theme.test.ts +23 -0
- package/src/utils/tokens.ts +16 -0
- package/src/webmcp-bridge.test.ts +102 -0
- package/src/webmcp-runtime-entry.ts +29 -1
- package/dist/chunk-BAkaTZpd.js +0 -1
- package/dist/chunk-BEDTp-dm.js +0 -1
- package/dist/chunk-C7-m0EmI.js +0 -1
- package/dist/chunk-CNhThrLX.js +0 -1
- package/dist/chunk-CVMIncEY.js +0 -1
- package/dist/chunk-CzLEhoB_.js +0 -2
- package/dist/chunk-DVgzoVCB.js +0 -1
- package/dist/chunk-d54BJdKd.js +0 -1
- 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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
badge.
|
|
144
|
-
|
|
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;
|
package/src/plugin-kit.test.ts
CHANGED
|
@@ -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 (
|
|
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 {
|