@runtypelabs/persona 3.18.0 → 3.19.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 +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +281 -4
- package/dist/index.d.ts +281 -4
- package/dist/index.global.js +102 -1636
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +1438 -619
- package/dist/theme-editor.d.cts +119 -1
- package/dist/theme-editor.d.ts +119 -1
- package/dist/theme-editor.js +1552 -619
- package/dist/widget.css +348 -0
- package/package.json +1 -1
- package/src/components/composer-builder.test.ts +52 -0
- package/src/components/composer-builder.ts +67 -490
- package/src/components/composer-parts.test.ts +152 -0
- package/src/components/composer-parts.ts +452 -0
- package/src/components/header-builder.ts +22 -299
- package/src/components/header-parts.ts +360 -0
- package/src/components/panel.test.ts +61 -0
- package/src/components/panel.ts +262 -5
- package/src/components/pill-composer-builder.test.ts +85 -0
- package/src/components/pill-composer-builder.ts +183 -0
- package/src/index.ts +4 -0
- package/src/runtime/init.ts +4 -2
- package/src/runtime/persist-state.test.ts +152 -0
- package/src/styles/widget.css +348 -0
- package/src/types.ts +121 -1
- package/src/ui.component-directive.test.ts +183 -0
- package/src/ui.composer-bar.test.ts +1009 -0
- package/src/ui.ts +809 -72
- package/src/utils/attachment-manager.ts +1 -1
- package/src/utils/dock.test.ts +45 -0
- package/src/utils/dock.ts +3 -0
- package/src/utils/icons.ts +314 -58
- package/src/utils/stream-animation.ts +7 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
import { componentRegistry, type ComponentRenderer } from "./components/registry";
|
|
7
|
+
|
|
8
|
+
const createMount = () => {
|
|
9
|
+
const mount = document.createElement("div");
|
|
10
|
+
document.body.appendChild(mount);
|
|
11
|
+
return mount;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const directiveMessage = (
|
|
15
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
16
|
+
{
|
|
17
|
+
id,
|
|
18
|
+
rawContent,
|
|
19
|
+
content = "",
|
|
20
|
+
createdAt = "2026-04-28T00:00:00.000Z",
|
|
21
|
+
}: {
|
|
22
|
+
id: string;
|
|
23
|
+
rawContent: string;
|
|
24
|
+
content?: string;
|
|
25
|
+
createdAt?: string;
|
|
26
|
+
}
|
|
27
|
+
) => {
|
|
28
|
+
controller.injectTestMessage({
|
|
29
|
+
type: "message",
|
|
30
|
+
message: {
|
|
31
|
+
id,
|
|
32
|
+
role: "assistant",
|
|
33
|
+
content,
|
|
34
|
+
rawContent,
|
|
35
|
+
createdAt,
|
|
36
|
+
streaming: false,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("component directive bubble — listener preservation across morphs", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
// The component registry is a singleton; clear it between tests so each
|
|
44
|
+
// test starts from a clean slate.
|
|
45
|
+
componentRegistry.clear();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
componentRegistry.clear();
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps the renderer's submit listener alive after subsequent transcript updates", () => {
|
|
55
|
+
const submitSpy = vi.fn();
|
|
56
|
+
|
|
57
|
+
const mount = createMount();
|
|
58
|
+
const controller = createAgentExperience(mount, {
|
|
59
|
+
apiUrl: "https://api.example.com/chat",
|
|
60
|
+
launcher: { enabled: false },
|
|
61
|
+
parserType: "json",
|
|
62
|
+
enableComponentStreaming: true,
|
|
63
|
+
components: {
|
|
64
|
+
TestForm: ((props) => {
|
|
65
|
+
const root = document.createElement("div");
|
|
66
|
+
root.setAttribute("data-test-id", "test-form");
|
|
67
|
+
const form = document.createElement("form");
|
|
68
|
+
form.setAttribute("data-test-id", "test-form-el");
|
|
69
|
+
form.addEventListener("submit", (event) => {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
submitSpy(String(props.label ?? ""));
|
|
72
|
+
});
|
|
73
|
+
const button = document.createElement("button");
|
|
74
|
+
button.type = "submit";
|
|
75
|
+
button.setAttribute("data-test-id", "test-form-submit");
|
|
76
|
+
button.textContent = "Submit";
|
|
77
|
+
form.appendChild(button);
|
|
78
|
+
root.appendChild(form);
|
|
79
|
+
return root;
|
|
80
|
+
}) satisfies ComponentRenderer,
|
|
81
|
+
},
|
|
82
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
83
|
+
|
|
84
|
+
directiveMessage(controller, {
|
|
85
|
+
id: "msg-1",
|
|
86
|
+
rawContent: JSON.stringify({ component: "TestForm", props: { label: "alpha" } }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const formBefore = mount.querySelector<HTMLFormElement>('[data-test-id="test-form-el"]');
|
|
90
|
+
expect(formBefore).not.toBeNull();
|
|
91
|
+
|
|
92
|
+
// Force another render pass — this is what triggered the bug pre-fix:
|
|
93
|
+
// Idiomorph would replace the form via innerHTML serialization, dropping
|
|
94
|
+
// the addEventListener-attached submit handler.
|
|
95
|
+
controller.injectTestMessage({
|
|
96
|
+
type: "message",
|
|
97
|
+
message: {
|
|
98
|
+
id: "msg-2",
|
|
99
|
+
role: "user",
|
|
100
|
+
content: "follow-up",
|
|
101
|
+
createdAt: "2026-04-28T00:00:01.000Z",
|
|
102
|
+
streaming: false,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// The form node should be the SAME instance — preserved by the live
|
|
107
|
+
// wrapper's `data-preserve-runtime` and the post-morph hydrate skipping
|
|
108
|
+
// when fingerprint is unchanged.
|
|
109
|
+
const formAfter = mount.querySelector<HTMLFormElement>('[data-test-id="test-form-el"]');
|
|
110
|
+
expect(formAfter).toBe(formBefore);
|
|
111
|
+
|
|
112
|
+
const submitBtn = mount.querySelector<HTMLButtonElement>('[data-test-id="test-form-submit"]');
|
|
113
|
+
expect(submitBtn).not.toBeNull();
|
|
114
|
+
submitBtn!.click();
|
|
115
|
+
|
|
116
|
+
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(submitSpy).toHaveBeenCalledWith("alpha");
|
|
118
|
+
|
|
119
|
+
controller.destroy();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("rebuilds and re-hydrates the bubble when the directive props change", () => {
|
|
123
|
+
const calls: string[] = [];
|
|
124
|
+
|
|
125
|
+
const mount = createMount();
|
|
126
|
+
const controller = createAgentExperience(mount, {
|
|
127
|
+
apiUrl: "https://api.example.com/chat",
|
|
128
|
+
launcher: { enabled: false },
|
|
129
|
+
parserType: "json",
|
|
130
|
+
enableComponentStreaming: true,
|
|
131
|
+
components: {
|
|
132
|
+
TestBadge: ((props) => {
|
|
133
|
+
const el = document.createElement("div");
|
|
134
|
+
el.setAttribute("data-test-id", "test-badge");
|
|
135
|
+
el.textContent = String(props.label ?? "");
|
|
136
|
+
calls.push(String(props.label ?? ""));
|
|
137
|
+
return el;
|
|
138
|
+
}) satisfies ComponentRenderer,
|
|
139
|
+
},
|
|
140
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
141
|
+
|
|
142
|
+
directiveMessage(controller, {
|
|
143
|
+
id: "badge-1",
|
|
144
|
+
rawContent: JSON.stringify({ component: "TestBadge", props: { label: "first" } }),
|
|
145
|
+
});
|
|
146
|
+
expect(mount.querySelector('[data-test-id="test-badge"]')?.textContent).toBe("first");
|
|
147
|
+
|
|
148
|
+
// Same id, new props — fingerprint changes, so renderer should be invoked
|
|
149
|
+
// again and the new element hydrated into the live wrapper.
|
|
150
|
+
directiveMessage(controller, {
|
|
151
|
+
id: "badge-1",
|
|
152
|
+
rawContent: JSON.stringify({ component: "TestBadge", props: { label: "second" } }),
|
|
153
|
+
});
|
|
154
|
+
expect(mount.querySelector('[data-test-id="test-badge"]')?.textContent).toBe("second");
|
|
155
|
+
expect(calls).toEqual(["first", "second"]);
|
|
156
|
+
|
|
157
|
+
controller.destroy();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("falls back to the standard render path when the directive component is not registered", () => {
|
|
161
|
+
const mount = createMount();
|
|
162
|
+
const controller = createAgentExperience(mount, {
|
|
163
|
+
apiUrl: "https://api.example.com/chat",
|
|
164
|
+
launcher: { enabled: false },
|
|
165
|
+
parserType: "json",
|
|
166
|
+
enableComponentStreaming: true,
|
|
167
|
+
components: {},
|
|
168
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
169
|
+
|
|
170
|
+
directiveMessage(controller, {
|
|
171
|
+
id: "missing-1",
|
|
172
|
+
rawContent: JSON.stringify({ component: "NotRegistered", props: {} }),
|
|
173
|
+
content: "fallback text",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// No directive stub left in the DOM.
|
|
177
|
+
expect(mount.querySelector('[data-component-directive-stub="true"]')).toBeNull();
|
|
178
|
+
// Standard bubble renders the message text.
|
|
179
|
+
expect(mount.textContent).toContain("fallback text");
|
|
180
|
+
|
|
181
|
+
controller.destroy();
|
|
182
|
+
});
|
|
183
|
+
});
|