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