@runtypelabs/persona 4.15.0 → 4.16.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/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +283 -9
- package/dist/context-mentions.d.ts +283 -9
- package/dist/index.cjs +68 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -10
- package/dist/index.d.ts +299 -10
- package/dist/index.global.js +57 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +283 -9
- package/dist/smart-dom-reader.d.ts +283 -9
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +7 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +64 -64
- package/dist/theme-editor-preview.d.cts +283 -9
- package/dist/theme-editor-preview.d.ts +283 -9
- package/dist/theme-editor-preview.js +56 -56
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +283 -10
- package/dist/theme-editor.d.ts +283 -10
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +66 -3
- package/dist/theme-reference.d.ts +66 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/header-builder.ts +27 -2
- package/src/components/header-layouts.test.ts +56 -0
- package/src/components/header-layouts.ts +29 -9
- package/src/components/launcher-surface.test.ts +403 -0
- package/src/components/launcher.ts +297 -2
- package/src/components/panel.ts +69 -74
- package/src/components/suggestions.test.ts +40 -0
- package/src/components/suggestions.ts +213 -153
- package/src/components/welcome.test.ts +121 -0
- package/src/components/welcome.ts +305 -0
- package/src/components/widget-view.test.ts +5 -3
- package/src/components/widget-view.ts +14 -7
- package/src/defaults.ts +2 -2
- package/src/index-core.ts +12 -1
- package/src/launcher-global.ts +10 -7
- package/src/plugins/types.ts +66 -1
- package/src/styles/widget-styles.test.ts +220 -0
- package/src/styles/widget.css +353 -9
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/sections.ts +30 -1
- package/src/theme-reference.ts +26 -4
- package/src/types/theme.ts +40 -3
- package/src/types.ts +164 -6
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.docked.test.ts +7 -4
- package/src/ui.launcher-teaser.test.ts +162 -0
- package/src/ui.rebuild-composer.test.ts +491 -0
- package/src/ui.render-welcome.test.ts +505 -0
- package/src/ui.scroll-additive.test.ts +91 -21
- package/src/ui.scroll.test.ts +302 -14
- package/src/ui.suggest-replies.test.ts +46 -0
- package/src/ui.ts +747 -136
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-gate.test.ts +27 -0
- package/src/utils/artifact-gate.ts +9 -0
- package/src/utils/attachment-manager.test.ts +37 -0
- package/src/utils/attachment-manager.ts +15 -0
- package/src/utils/auto-follow.test.ts +23 -0
- package/src/utils/auto-follow.ts +11 -1
- package/src/utils/config-merge.ts +2 -0
- package/src/utils/content-width.test.ts +37 -0
- package/src/utils/content-width.ts +17 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +137 -0
- package/src/utils/tokens.ts +49 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
3
|
+
import { getBubbleClasses } from "./message-bubble";
|
|
4
|
+
import type {
|
|
5
|
+
AgentWidgetConfig,
|
|
6
|
+
AgentWidgetWelcomeIcon,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import type { ResolvedWelcomeConfig } from "../welcome";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Welcome surface DOM. The host is permanent: it mounts once with the panel
|
|
12
|
+
* and every variant/visibility change is applied to it in place, so live
|
|
13
|
+
* `controller.update()` never rebuilds it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface WelcomeHostElements {
|
|
17
|
+
/** Permanent host; also carries `data-persona-intro-card` for slot parity. */
|
|
18
|
+
host: HTMLElement;
|
|
19
|
+
iconHolder: HTMLElement;
|
|
20
|
+
title: HTMLElement;
|
|
21
|
+
subtitle: HTMLElement;
|
|
22
|
+
starterSuggestions: HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const WELCOME_ICON_SIZE = 40;
|
|
26
|
+
|
|
27
|
+
/** Replaces the holder's content; hides it when no icon is configured. */
|
|
28
|
+
export const applyWelcomeIcon = (
|
|
29
|
+
holder: HTMLElement,
|
|
30
|
+
icon: AgentWidgetWelcomeIcon | undefined
|
|
31
|
+
): void => {
|
|
32
|
+
holder.replaceChildren();
|
|
33
|
+
if (!icon) {
|
|
34
|
+
holder.hidden = true;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof icon === "function") {
|
|
39
|
+
let custom: HTMLElement | SVGElement | null = null;
|
|
40
|
+
try {
|
|
41
|
+
custom = icon();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn("[persona] welcome.icon renderer threw", error);
|
|
44
|
+
}
|
|
45
|
+
if (!custom) {
|
|
46
|
+
holder.hidden = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
holder.appendChild(custom);
|
|
50
|
+
holder.hidden = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (icon.type === "lucide") {
|
|
55
|
+
const svg = renderLucideIcon(icon.name, WELCOME_ICON_SIZE);
|
|
56
|
+
if (!svg) {
|
|
57
|
+
holder.hidden = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
holder.appendChild(svg);
|
|
61
|
+
} else if (icon.type === "image") {
|
|
62
|
+
holder.appendChild(
|
|
63
|
+
createNode("img", {
|
|
64
|
+
className: "persona-welcome-icon-image",
|
|
65
|
+
attrs: { src: icon.url, alt: icon.alt },
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
holder.appendChild(
|
|
70
|
+
createNode("span", {
|
|
71
|
+
className: "persona-welcome-icon-text",
|
|
72
|
+
text: icon.text,
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
holder.hidden = false;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the welcome host. `starterSuggestions` is created by the caller so the
|
|
81
|
+
* suggestion surface keeps its existing wiring.
|
|
82
|
+
*/
|
|
83
|
+
export const buildWelcomeHost = (
|
|
84
|
+
config: AgentWidgetConfig | undefined,
|
|
85
|
+
starterSuggestions: HTMLElement,
|
|
86
|
+
options: { flatShadow?: boolean } = {}
|
|
87
|
+
): WelcomeHostElements => {
|
|
88
|
+
const iconHolder = createNode("div", {
|
|
89
|
+
className: "persona-welcome-icon",
|
|
90
|
+
attrs: { "data-persona-welcome-icon": "" },
|
|
91
|
+
});
|
|
92
|
+
iconHolder.hidden = true;
|
|
93
|
+
|
|
94
|
+
// Typography flows through `components.introCard.title` / `.subtitle`;
|
|
95
|
+
// the CSS fallbacks reproduce the utility classes these replaced.
|
|
96
|
+
const title = createElement("h2", "persona-welcome-title");
|
|
97
|
+
const subtitle = createElement("p", "persona-welcome-subtitle");
|
|
98
|
+
|
|
99
|
+
// Background and box-shadow flow through the themable `components.introCard`
|
|
100
|
+
// tokens; both default to flat. Docked mode always stays flat. Padding comes
|
|
101
|
+
// from `--persona-intro-card-padding` (see widget.css), which zeroes its
|
|
102
|
+
// horizontal component when the card resolves flat so the text shares the
|
|
103
|
+
// content column's left edge instead of carrying a phantom card inset.
|
|
104
|
+
const host = createNode(
|
|
105
|
+
"div",
|
|
106
|
+
{
|
|
107
|
+
className: "persona-welcome persona-rounded-2xl",
|
|
108
|
+
attrs: {
|
|
109
|
+
"data-persona-welcome": "",
|
|
110
|
+
"data-persona-intro-card": "",
|
|
111
|
+
"data-persona-welcome-variant": "card",
|
|
112
|
+
},
|
|
113
|
+
style: {
|
|
114
|
+
background: "var(--persona-intro-card-bg, transparent)",
|
|
115
|
+
boxShadow: options.flatShadow
|
|
116
|
+
? "none"
|
|
117
|
+
: "var(--persona-intro-card-shadow, none)",
|
|
118
|
+
border: "var(--persona-intro-card-border, none)",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
iconHolder,
|
|
122
|
+
title,
|
|
123
|
+
subtitle,
|
|
124
|
+
starterSuggestions
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return { host, iconHolder, title, subtitle, starterSuggestions };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Show/hide the permanent host. The body gap tightens when nothing renders
|
|
132
|
+
* there so a hidden welcome leaves no phantom section spacing.
|
|
133
|
+
*/
|
|
134
|
+
export const applyWelcomeVisibility = (
|
|
135
|
+
body: HTMLElement,
|
|
136
|
+
host: HTMLElement,
|
|
137
|
+
visible: boolean
|
|
138
|
+
): void => {
|
|
139
|
+
host.hidden = !visible;
|
|
140
|
+
host.style.display = visible ? "" : "none";
|
|
141
|
+
body.classList.toggle("persona-gap-6", visible);
|
|
142
|
+
body.classList.toggle("persona-gap-3", !visible);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Apply resolved config to the permanent host. Every field is set on every
|
|
147
|
+
* call so `controller.update()` can drop a field and see it revert.
|
|
148
|
+
*/
|
|
149
|
+
export const applyWelcomeConfig = (
|
|
150
|
+
elements: WelcomeHostElements,
|
|
151
|
+
resolved: ResolvedWelcomeConfig
|
|
152
|
+
): void => {
|
|
153
|
+
elements.title.textContent = resolved.title;
|
|
154
|
+
elements.subtitle.textContent = resolved.subtitle;
|
|
155
|
+
// An empty subtitle omits the paragraph entirely, margin included.
|
|
156
|
+
elements.subtitle.hidden = resolved.subtitle === "";
|
|
157
|
+
applyWelcomeIcon(elements.iconHolder, resolved.icon);
|
|
158
|
+
elements.host.setAttribute("data-persona-welcome-variant", resolved.variant);
|
|
159
|
+
elements.host.setAttribute("data-persona-welcome-dismiss", resolved.dismiss);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Marks the element a `renderWelcome` plugin returned. */
|
|
163
|
+
export const WELCOME_PLUGIN_CONTENT_ATTR = "data-persona-welcome-plugin";
|
|
164
|
+
const WELCOME_OVERLAY_ATTR = "data-persona-welcome-overlay";
|
|
165
|
+
const WELCOME_OVERLAY_BODY_CLASS = "persona-welcome-overlay-active";
|
|
166
|
+
|
|
167
|
+
// An absolute child of a scrolled scroller sits at the content origin, not
|
|
168
|
+
// the visible viewport, and overflow: hidden freezes scrollTop rather than
|
|
169
|
+
// resetting it. Zero the scroll while the overlay is up, restore on clear.
|
|
170
|
+
const overlayScrollTop = new WeakMap<HTMLElement, number>();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Mount plugin content inside the permanent host. The default children are
|
|
174
|
+
* hidden by CSS while the content attribute is set, so nothing about their
|
|
175
|
+
* own hidden state has to be saved and restored.
|
|
176
|
+
*/
|
|
177
|
+
export const mountWelcomePluginContent = (
|
|
178
|
+
body: HTMLElement,
|
|
179
|
+
host: HTMLElement,
|
|
180
|
+
element: HTMLElement
|
|
181
|
+
): void => {
|
|
182
|
+
element.setAttribute(WELCOME_PLUGIN_CONTENT_ATTR, "");
|
|
183
|
+
host.appendChild(element);
|
|
184
|
+
host.setAttribute("data-persona-welcome-content", "plugin");
|
|
185
|
+
// Plugin content renders regardless of derived visibility and overlays the
|
|
186
|
+
// transcript, which is what makes "return to home" over a conversation work.
|
|
187
|
+
host.setAttribute(WELCOME_OVERLAY_ATTR, "");
|
|
188
|
+
if (!body.classList.contains(WELCOME_OVERLAY_BODY_CLASS)) {
|
|
189
|
+
overlayScrollTop.set(body, body.scrollTop);
|
|
190
|
+
body.classList.add(WELCOME_OVERLAY_BODY_CLASS);
|
|
191
|
+
body.scrollTop = 0;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Remove plugin content and hand the host back to the default renderer. */
|
|
196
|
+
export const clearWelcomePluginContent = (
|
|
197
|
+
body: HTMLElement,
|
|
198
|
+
host: HTMLElement,
|
|
199
|
+
element: HTMLElement | null
|
|
200
|
+
): void => {
|
|
201
|
+
if (element && element !== host) element.remove();
|
|
202
|
+
host.removeAttribute("data-persona-welcome-content");
|
|
203
|
+
host.removeAttribute(WELCOME_OVERLAY_ATTR);
|
|
204
|
+
if (body.classList.contains(WELCOME_OVERLAY_BODY_CLASS)) {
|
|
205
|
+
body.classList.remove(WELCOME_OVERLAY_BODY_CLASS);
|
|
206
|
+
body.scrollTop = overlayScrollTop.get(body) ?? body.scrollTop;
|
|
207
|
+
overlayScrollTop.delete(body);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Greeting host: a sibling above `messagesWrapper`, so it is pinned at
|
|
213
|
+
* transcript position zero without ever entering the morph target.
|
|
214
|
+
*/
|
|
215
|
+
export const buildGreetingHost = (): HTMLElement => {
|
|
216
|
+
const host = createNode("div", {
|
|
217
|
+
className: "persona-welcome-greeting persona-flex persona-flex-col",
|
|
218
|
+
attrs: { "data-persona-welcome-greeting": "" },
|
|
219
|
+
});
|
|
220
|
+
host.hidden = true;
|
|
221
|
+
return host;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Render the display-only greeting bubble. Plain text by contract: no
|
|
226
|
+
* markdown pipeline, no action affordances, no message id, never a session
|
|
227
|
+
* message.
|
|
228
|
+
*/
|
|
229
|
+
export const renderWelcomeGreeting = (
|
|
230
|
+
host: HTMLElement,
|
|
231
|
+
text: string | undefined,
|
|
232
|
+
config: AgentWidgetConfig | undefined
|
|
233
|
+
): void => {
|
|
234
|
+
host.replaceChildren();
|
|
235
|
+
if (!text) {
|
|
236
|
+
host.hidden = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const layout = config?.layout?.messages?.layout ?? "bubble";
|
|
241
|
+
const bubble = createNode("div", {
|
|
242
|
+
className: getBubbleClasses("assistant", layout).join(" "),
|
|
243
|
+
attrs: { "data-persona-theme-zone": "assistant-message" },
|
|
244
|
+
text,
|
|
245
|
+
style:
|
|
246
|
+
layout === "flat"
|
|
247
|
+
? {
|
|
248
|
+
backgroundColor: "transparent",
|
|
249
|
+
color: "var(--persona-message-assistant-text, var(--persona-text))",
|
|
250
|
+
}
|
|
251
|
+
: {
|
|
252
|
+
backgroundColor:
|
|
253
|
+
"var(--persona-message-assistant-bg, var(--persona-container))",
|
|
254
|
+
color: "var(--persona-message-assistant-text, var(--persona-text))",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
host.appendChild(createNode("div", { className: "persona-flex" }, bubble));
|
|
259
|
+
host.hidden = false;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const prefersReducedMotion = (): boolean =>
|
|
263
|
+
typeof window !== "undefined" &&
|
|
264
|
+
typeof window.matchMedia === "function" &&
|
|
265
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Hero dismiss. WAAPI, not a CSS transition: morph re-renders strip
|
|
269
|
+
* post-render CSS state and cancel transitions mid-stream. Shares the
|
|
270
|
+
* starters' entrance motion spec so the two read as one system.
|
|
271
|
+
*
|
|
272
|
+
* Returns the animation so the caller can hold the host visible while it runs
|
|
273
|
+
* and cancel it on a re-show. `onFinished` receives it, or null when the motion
|
|
274
|
+
* was skipped and the callback ran synchronously.
|
|
275
|
+
*/
|
|
276
|
+
export const animateWelcomeOut = (
|
|
277
|
+
host: HTMLElement,
|
|
278
|
+
onFinished: (animation: Animation | null) => void
|
|
279
|
+
): Animation | null => {
|
|
280
|
+
if (
|
|
281
|
+
prefersReducedMotion() ||
|
|
282
|
+
typeof host.animate !== "function" ||
|
|
283
|
+
!host.isConnected
|
|
284
|
+
) {
|
|
285
|
+
onFinished(null);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
// `forwards`, not `backwards`: backwards only fills the delay phase, so the
|
|
289
|
+
// element snaps back to opacity 1 for a frame after the keyframes end.
|
|
290
|
+
const animation = host.animate(
|
|
291
|
+
[
|
|
292
|
+
{ opacity: "1", transform: "none" },
|
|
293
|
+
{ opacity: "0", transform: "translateY(-4px)" },
|
|
294
|
+
],
|
|
295
|
+
{ duration: 180, easing: "ease-out", fill: "forwards" }
|
|
296
|
+
);
|
|
297
|
+
let settled = false;
|
|
298
|
+
const settle = () => {
|
|
299
|
+
if (settled) return;
|
|
300
|
+
settled = true;
|
|
301
|
+
onFinished(animation);
|
|
302
|
+
};
|
|
303
|
+
animation.finished.then(settle).catch(settle);
|
|
304
|
+
return animation;
|
|
305
|
+
};
|
|
@@ -129,15 +129,17 @@ describe("createWidgetView: replacement helpers", () => {
|
|
|
129
129
|
describe("resolveLauncher", () => {
|
|
130
130
|
const config: AgentWidgetConfig = { apiUrl: "/api" };
|
|
131
131
|
|
|
132
|
-
it("builds the default launcher
|
|
132
|
+
it("builds the default launcher surface and wires onToggle", () => {
|
|
133
133
|
const onToggle = vi.fn();
|
|
134
134
|
const { instance, element } = resolveLauncher({ config, plugins: [], onToggle });
|
|
135
135
|
|
|
136
136
|
expect(instance).not.toBeNull();
|
|
137
137
|
expect(element).toBe(instance?.element);
|
|
138
|
-
|
|
138
|
+
// The mounted element is the surface wrapper; the button is inside it.
|
|
139
|
+
expect(element.tagName).toBe("DIV");
|
|
140
|
+
expect(instance?.launcher.element.tagName).toBe("BUTTON");
|
|
139
141
|
|
|
140
|
-
element.dispatchEvent(new Event("click"));
|
|
142
|
+
instance!.launcher.element.dispatchEvent(new Event("click"));
|
|
141
143
|
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
142
144
|
});
|
|
143
145
|
|
|
@@ -2,7 +2,7 @@ import { AgentWidgetConfig } from "../types";
|
|
|
2
2
|
import { AgentWidgetPlugin } from "../plugins/types";
|
|
3
3
|
import { createWrapper, buildPanel, PanelElements } from "./panel";
|
|
4
4
|
import { HeaderElements } from "./header-builder";
|
|
5
|
-
import { createLauncherButton,
|
|
5
|
+
import { createLauncherButton, createLauncherSurface, LauncherSurface } from "./launcher";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Widget view assembly layer.
|
|
@@ -34,6 +34,9 @@ export interface WidgetTranscriptRefs {
|
|
|
34
34
|
transcriptSuggestions: HTMLElement;
|
|
35
35
|
/** Absolute slot above the composer where interactive sheets mount. */
|
|
36
36
|
composerOverlay: HTMLElement;
|
|
37
|
+
welcomeHost: HTMLElement;
|
|
38
|
+
welcomeIconHolder: HTMLElement;
|
|
39
|
+
greetingHost: HTMLElement;
|
|
37
40
|
introTitle: HTMLElement;
|
|
38
41
|
introSubtitle: HTMLElement;
|
|
39
42
|
}
|
|
@@ -124,6 +127,9 @@ export const createWidgetView = ({
|
|
|
124
127
|
starterSuggestions: panelElements.starterSuggestions,
|
|
125
128
|
transcriptSuggestions: panelElements.transcriptSuggestions,
|
|
126
129
|
composerOverlay: panelElements.composerOverlay,
|
|
130
|
+
welcomeHost: panelElements.welcomeHost,
|
|
131
|
+
welcomeIconHolder: panelElements.welcomeIconHolder,
|
|
132
|
+
greetingHost: panelElements.greetingHost,
|
|
127
133
|
introTitle: panelElements.introTitle,
|
|
128
134
|
introSubtitle: panelElements.introSubtitle,
|
|
129
135
|
};
|
|
@@ -199,11 +205,11 @@ export interface ResolveLauncherOptions {
|
|
|
199
205
|
|
|
200
206
|
export interface ResolvedLauncher {
|
|
201
207
|
/**
|
|
202
|
-
* The default launcher
|
|
208
|
+
* The default launcher surface (with `update`/`destroy`), or null when a
|
|
203
209
|
* plugin supplied a custom launcher element.
|
|
204
210
|
*/
|
|
205
|
-
instance:
|
|
206
|
-
/** The element to mount: either the default
|
|
211
|
+
instance: LauncherSurface | null;
|
|
212
|
+
/** The element to mount: either the default surface or the plugin's element. */
|
|
207
213
|
element: HTMLElement;
|
|
208
214
|
}
|
|
209
215
|
|
|
@@ -213,8 +219,9 @@ export interface ResolvedLauncher {
|
|
|
213
219
|
* both the initial-build and re-enable call sites in ui.ts.
|
|
214
220
|
*
|
|
215
221
|
* When a plugin returns a custom element, `instance` is null (the plugin owns
|
|
216
|
-
* its own updates); otherwise the default
|
|
217
|
-
* returned so the caller can drive
|
|
222
|
+
* its own updates, including any teaser); otherwise the default
|
|
223
|
+
* `LauncherSurface` controller is returned so the caller can drive
|
|
224
|
+
* `update()`/`destroy()`.
|
|
218
225
|
*/
|
|
219
226
|
export const resolveLauncher = ({
|
|
220
227
|
config,
|
|
@@ -233,6 +240,6 @@ export const resolveLauncher = ({
|
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
242
|
|
|
236
|
-
const instance =
|
|
243
|
+
const instance = createLauncherSurface(config, onToggle);
|
|
237
244
|
return { instance, element: instance.element };
|
|
238
245
|
};
|
package/src/defaults.ts
CHANGED
|
@@ -79,8 +79,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
79
79
|
colorScheme: "light",
|
|
80
80
|
launcher: DEFAULT_LAUNCHER_CONFIG,
|
|
81
81
|
copy: {
|
|
82
|
-
welcomeTitle:
|
|
83
|
-
|
|
82
|
+
// No welcomeTitle / welcomeSubtitle here on purpose: `resolveWelcomeConfig`
|
|
83
|
+
// owns those defaults so presence still means "the host set this".
|
|
84
84
|
inputPlaceholder: "How can I help...",
|
|
85
85
|
sendButtonLabel: "Send",
|
|
86
86
|
},
|
package/src/index-core.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type {
|
|
|
31
31
|
AgentWidgetInitOptions,
|
|
32
32
|
AgentWidgetMessage,
|
|
33
33
|
AgentWidgetLauncherConfig,
|
|
34
|
+
AgentWidgetLauncherTeaserConfig,
|
|
34
35
|
AgentWidgetDockConfig,
|
|
35
36
|
AgentWidgetEvent,
|
|
36
37
|
AgentWidgetStreamParser,
|
|
@@ -151,7 +152,11 @@ export type {
|
|
|
151
152
|
AgentWidgetSuggestionSurfaceConfig,
|
|
152
153
|
AgentWidgetStarterSuggestionsConfig,
|
|
153
154
|
AgentWidgetFollowUpSuggestionsConfig,
|
|
154
|
-
AgentWidgetSuggestionsConfig
|
|
155
|
+
AgentWidgetSuggestionsConfig,
|
|
156
|
+
AgentWidgetWelcomeConfig,
|
|
157
|
+
AgentWidgetWelcomeIcon,
|
|
158
|
+
AgentWidgetWelcomeVariant,
|
|
159
|
+
AgentWidgetWelcomeDismiss
|
|
155
160
|
} from "./types";
|
|
156
161
|
|
|
157
162
|
export {
|
|
@@ -341,8 +346,13 @@ export type {
|
|
|
341
346
|
AgentWidgetPlugin,
|
|
342
347
|
AgentWidgetTransformSuggestionsContext,
|
|
343
348
|
AgentWidgetRenderSuggestionContext,
|
|
349
|
+
AgentWidgetRenderWelcomeContext,
|
|
344
350
|
AgentWidgetSuggestionSelectContext
|
|
345
351
|
} from "./plugins/types";
|
|
352
|
+
// Welcome resolver: `renderWelcome` receives the resolved shape.
|
|
353
|
+
export { resolveWelcomeConfig, isWelcomeVisible } from "./welcome";
|
|
354
|
+
export type { ResolvedWelcomeConfig } from "./welcome";
|
|
355
|
+
export type { AgentWidgetPluginStorage } from "./utils/plugin-storage";
|
|
346
356
|
export { pluginRegistry } from "./plugins/registry";
|
|
347
357
|
|
|
348
358
|
// Stream animation plugin API: lets consumers register custom animations
|
|
@@ -447,6 +457,7 @@ export type {
|
|
|
447
457
|
SemanticSpacing,
|
|
448
458
|
SemanticTypography,
|
|
449
459
|
ComponentTokens,
|
|
460
|
+
TextStyleTokens,
|
|
450
461
|
ArtifactToolbarTokens,
|
|
451
462
|
ArtifactTabTokens,
|
|
452
463
|
ArtifactPaneTokens,
|
package/src/launcher-global.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Critical-path launcher entry: built to `launcher.global.js` (IIFE).
|
|
3
3
|
*
|
|
4
|
-
* Ships ONLY the real collapsed launcher (`
|
|
4
|
+
* Ships ONLY the real collapsed launcher surface (`createLauncherSurface`,
|
|
5
|
+
* which carries the optional launcher teaser) plus the
|
|
5
6
|
* theme application path, so the launcher paints pixel-identically to the full
|
|
6
7
|
* widget from a tiny bundle. The heavy conversation panel is deferred until
|
|
7
8
|
* first open by the installer (Phase 2).
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
* window.AgentWidgetLauncher.mount({ target, config, onOpen })
|
|
17
18
|
* → { root, element, update, destroy }
|
|
18
19
|
*/
|
|
19
|
-
import {
|
|
20
|
+
import { createLauncherSurface } from "./components/launcher";
|
|
20
21
|
import { applyThemeVariables } from "./utils/theme";
|
|
21
22
|
import { DEFAULT_LAUNCHER_CONFIG } from "./defaults";
|
|
22
23
|
import type { AgentWidgetConfig } from "./types";
|
|
@@ -100,20 +101,22 @@ export const mount = (
|
|
|
100
101
|
root.setAttribute(CRITICAL_LAUNCHER_ATTR, "true");
|
|
101
102
|
applyThemeVariables(root, config);
|
|
102
103
|
|
|
103
|
-
const
|
|
104
|
-
root.appendChild(
|
|
104
|
+
const surface = createLauncherSurface(config, onOpen);
|
|
105
|
+
root.appendChild(surface.element);
|
|
105
106
|
target.appendChild(root);
|
|
106
107
|
|
|
107
108
|
return {
|
|
108
109
|
root,
|
|
109
|
-
element: launcher.element,
|
|
110
|
+
element: surface.launcher.element,
|
|
110
111
|
update: (next: AgentWidgetConfig) => {
|
|
111
112
|
const merged = mergeCriticalLauncherConfig(next);
|
|
112
113
|
applyThemeVariables(root, merged);
|
|
113
|
-
|
|
114
|
+
surface.update(merged);
|
|
114
115
|
},
|
|
116
|
+
// Handoff destroys the whole surface, so a pending teaser timer cannot
|
|
117
|
+
// outlive the critical launcher.
|
|
115
118
|
destroy: () => {
|
|
116
|
-
|
|
119
|
+
surface.destroy();
|
|
117
120
|
root.remove();
|
|
118
121
|
},
|
|
119
122
|
};
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ResolvedWelcomeConfig } from "../welcome";
|
|
2
|
+
import type { AgentWidgetPluginStorage } from "../utils/plugin-storage";
|
|
1
3
|
import {
|
|
2
4
|
AgentWidgetMessage,
|
|
3
5
|
AgentWidgetConfig,
|
|
@@ -13,7 +15,8 @@ import {
|
|
|
13
15
|
AgentWidgetResolvedSuggestion,
|
|
14
16
|
AgentWidgetSuggestionSource,
|
|
15
17
|
AgentWidgetSuggestionSurface,
|
|
16
|
-
AgentWidgetSuggestionVariant
|
|
18
|
+
AgentWidgetSuggestionVariant,
|
|
19
|
+
AgentWidgetWelcomeVariant
|
|
17
20
|
} from "../types";
|
|
18
21
|
|
|
19
22
|
export type AgentWidgetTransformSuggestionsContext = {
|
|
@@ -45,6 +48,28 @@ export type AgentWidgetRenderSuggestionContext = {
|
|
|
45
48
|
select: () => void;
|
|
46
49
|
};
|
|
47
50
|
|
|
51
|
+
export type AgentWidgetRenderWelcomeContext = {
|
|
52
|
+
/** Alias-resolved welcome config; never the raw `copy.welcome*` shape. */
|
|
53
|
+
config: ResolvedWelcomeConfig;
|
|
54
|
+
variant: AgentWidgetWelcomeVariant;
|
|
55
|
+
/** Derived visibility for this render; governs the default renderer only. */
|
|
56
|
+
visible: boolean;
|
|
57
|
+
/** The core-owned welcome card, live-updated by `controller.update()`. */
|
|
58
|
+
defaultRenderer: () => HTMLElement;
|
|
59
|
+
sendMessage: (text: string) => void;
|
|
60
|
+
/** Re-run welcome arbitration: cleanups run, previous content is removed. */
|
|
61
|
+
requestRender: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Persona's starter renderer, wired through the full select pipeline:
|
|
64
|
+
* `onSuggestionSelect`, cancelable `persona:suggestion:*` events, send/fill.
|
|
65
|
+
*/
|
|
66
|
+
renderStarter: (suggestion: AgentWidgetSuggestion) => HTMLElement;
|
|
67
|
+
/** Sync store namespaced by `persistState.keyPrefix` plus the plugin id. */
|
|
68
|
+
storage: AgentWidgetPluginStorage;
|
|
69
|
+
/** Teardown for this render; runs before the next one and on destroy. */
|
|
70
|
+
onCleanup: (fn: () => void) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
48
73
|
export type AgentWidgetSuggestionSelectContext = {
|
|
49
74
|
suggestion: AgentWidgetResolvedSuggestion;
|
|
50
75
|
surface: AgentWidgetSuggestionSurface;
|
|
@@ -115,6 +140,37 @@ export interface AgentWidgetPlugin {
|
|
|
115
140
|
onToggle: () => void;
|
|
116
141
|
}) => HTMLElement | null;
|
|
117
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Custom renderer for the welcome surface (card, hero, greeting stack).
|
|
145
|
+
* First plugin returning an element wins; null falls through to the default,
|
|
146
|
+
* same contract as `renderSuggestion`.
|
|
147
|
+
*
|
|
148
|
+
* The core owns the welcome host: it stays mounted for the panel's lifetime
|
|
149
|
+
* and content is swapped inside it. A plugin element renders regardless of
|
|
150
|
+
* `ctx.visible` (derived visibility governs the default renderer only) and
|
|
151
|
+
* the host overlays the transcript while that element is active, so a home
|
|
152
|
+
* screen can return over an existing conversation. Return null again to
|
|
153
|
+
* clear the overlay.
|
|
154
|
+
*
|
|
155
|
+
* The ctx carries no controller: plugins are host-instantiated, so close a
|
|
156
|
+
* factory over the init handle (`plugin.attach(controller)`).
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* renderWelcome: ({ config, renderStarter, requestRender, onCleanup }) => {
|
|
161
|
+
* const root = document.createElement("div");
|
|
162
|
+
* root.textContent = config.title;
|
|
163
|
+
* root.appendChild(renderStarter({ label: "Track my order" }));
|
|
164
|
+
* const timer = setInterval(requestRender, 60_000);
|
|
165
|
+
* onCleanup(() => clearInterval(timer));
|
|
166
|
+
* return root;
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
renderWelcome?: (
|
|
171
|
+
context: AgentWidgetRenderWelcomeContext
|
|
172
|
+
) => HTMLElement | null;
|
|
173
|
+
|
|
118
174
|
/**
|
|
119
175
|
* Custom renderer for panel header
|
|
120
176
|
* Return null to use default renderer
|
|
@@ -157,6 +213,15 @@ export interface AgentWidgetPlugin {
|
|
|
157
213
|
* Omitted when `config.voiceRecognition.enabled` is not true.
|
|
158
214
|
*/
|
|
159
215
|
onVoiceToggle?: () => void;
|
|
216
|
+
/**
|
|
217
|
+
* Re-run composer arbitration and swap the footer in place. The hook runs
|
|
218
|
+
* once at panel construction, so this is how a composer plugin unlocks
|
|
219
|
+
* (returns null) or re-renders later. Pending attachments, the mention
|
|
220
|
+
* affordances, and the composer listener registry are re-wired by the core.
|
|
221
|
+
*/
|
|
222
|
+
requestRender: () => void;
|
|
223
|
+
/** Same sync store as the `renderWelcome` ctx, namespaced per plugin id. */
|
|
224
|
+
storage: AgentWidgetPluginStorage;
|
|
160
225
|
}) => HTMLElement | null;
|
|
161
226
|
|
|
162
227
|
/**
|