@runtypelabs/persona 4.14.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.
Files changed (119) hide show
  1. package/README.md +4 -4
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
  5. package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-QJ4LV2KW.js +1 -0
  9. package/dist/codegen.cjs +12 -11
  10. package/dist/codegen.js +14 -13
  11. package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
  12. package/dist/context-mentions.cjs +11 -8
  13. package/dist/context-mentions.d.cts +520 -21
  14. package/dist/context-mentions.d.ts +520 -21
  15. package/dist/context-mentions.js +11 -8
  16. package/dist/index.cjs +69 -68
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +595 -46
  19. package/dist/index.d.ts +595 -46
  20. package/dist/index.global.js +60 -57
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +69 -68
  23. package/dist/index.js.map +1 -1
  24. package/dist/launcher.global.js +5 -2
  25. package/dist/launcher.global.js.map +1 -1
  26. package/dist/smart-dom-reader.d.cts +520 -21
  27. package/dist/smart-dom-reader.d.ts +520 -21
  28. package/dist/testing.cjs +3 -3
  29. package/dist/testing.d.cts +7 -0
  30. package/dist/testing.d.ts +7 -0
  31. package/dist/testing.js +3 -3
  32. package/dist/theme-editor-preview.cjs +67 -67
  33. package/dist/theme-editor-preview.d.cts +534 -22
  34. package/dist/theme-editor-preview.d.ts +534 -22
  35. package/dist/theme-editor-preview.js +57 -57
  36. package/dist/theme-editor.cjs +10 -10
  37. package/dist/theme-editor.d.cts +521 -23
  38. package/dist/theme-editor.d.ts +521 -23
  39. package/dist/theme-editor.js +10 -10
  40. package/dist/theme-reference.cjs +1 -1
  41. package/dist/theme-reference.d.cts +104 -3
  42. package/dist/theme-reference.d.ts +104 -3
  43. package/dist/theme-reference.js +1 -1
  44. package/dist/widget.css +1 -1
  45. package/package.json +1 -1
  46. package/src/ask-user-question-tool.ts +10 -7
  47. package/src/components/artifact-pane.test.ts +27 -0
  48. package/src/components/artifact-pane.ts +7 -3
  49. package/src/components/artifact-preview.ts +1 -1
  50. package/src/components/composer-parts.test.ts +103 -4
  51. package/src/components/composer-parts.ts +53 -15
  52. package/src/components/header-builder.ts +27 -2
  53. package/src/components/header-layouts.test.ts +56 -0
  54. package/src/components/header-layouts.ts +29 -9
  55. package/src/components/launcher-surface.test.ts +403 -0
  56. package/src/components/launcher.ts +297 -2
  57. package/src/components/messages.ts +5 -2
  58. package/src/components/panel.ts +92 -71
  59. package/src/components/pill-composer-builder.test.ts +18 -0
  60. package/src/components/suggestions.test.ts +273 -0
  61. package/src/components/suggestions.ts +528 -86
  62. package/src/components/welcome.test.ts +121 -0
  63. package/src/components/welcome.ts +305 -0
  64. package/src/components/widget-view.test.ts +5 -3
  65. package/src/components/widget-view.ts +18 -7
  66. package/src/defaults.ts +7 -5
  67. package/src/generated/runtype-openapi-contract.ts +17 -0
  68. package/src/index-core.ts +29 -2
  69. package/src/launcher-global.ts +11 -8
  70. package/src/plugins/types.ts +136 -3
  71. package/src/presets.ts +3 -3
  72. package/src/session.ts +10 -8
  73. package/src/styles/widget-styles.test.ts +382 -0
  74. package/src/styles/widget.css +778 -4
  75. package/src/suggest-replies-tool.test.ts +233 -2
  76. package/src/suggest-replies-tool.ts +115 -39
  77. package/src/testing/mock-stream.test.ts +19 -0
  78. package/src/testing/mock-stream.ts +20 -0
  79. package/src/theme-editor/preview-utils.test.ts +34 -0
  80. package/src/theme-editor/preview-utils.ts +77 -2
  81. package/src/theme-editor/sections.test.ts +33 -0
  82. package/src/theme-editor/sections.ts +83 -6
  83. package/src/theme-reference.ts +34 -5
  84. package/src/types/theme.ts +72 -3
  85. package/src/types.ts +312 -18
  86. package/src/ui.body-top-slot.test.ts +95 -0
  87. package/src/ui.content-width.test.ts +91 -0
  88. package/src/ui.detached-panel.test.ts +1 -1
  89. package/src/ui.docked.test.ts +7 -4
  90. package/src/ui.launcher-teaser.test.ts +162 -0
  91. package/src/ui.rebuild-composer.test.ts +491 -0
  92. package/src/ui.render-welcome.test.ts +505 -0
  93. package/src/ui.scroll-additive.test.ts +91 -21
  94. package/src/ui.scroll.test.ts +302 -14
  95. package/src/ui.suggest-replies.test.ts +1112 -1
  96. package/src/ui.ts +1008 -149
  97. package/src/ui.welcome.test.ts +504 -0
  98. package/src/utils/artifact-file.test.ts +1 -1
  99. package/src/utils/artifact-gate.test.ts +27 -0
  100. package/src/utils/artifact-gate.ts +9 -0
  101. package/src/utils/attachment-manager.test.ts +37 -0
  102. package/src/utils/attachment-manager.ts +15 -0
  103. package/src/utils/auto-follow.test.ts +23 -0
  104. package/src/utils/auto-follow.ts +11 -1
  105. package/src/utils/code-generators.test.ts +40 -0
  106. package/src/utils/code-generators.ts +22 -0
  107. package/src/utils/config-merge.test.ts +24 -0
  108. package/src/utils/config-merge.ts +2 -0
  109. package/src/utils/content-width.test.ts +37 -0
  110. package/src/utils/content-width.ts +17 -0
  111. package/src/utils/icons.test.ts +26 -0
  112. package/src/utils/icons.ts +8 -0
  113. package/src/utils/plugin-storage.test.ts +94 -0
  114. package/src/utils/plugin-storage.ts +98 -0
  115. package/src/utils/theme.test.ts +216 -0
  116. package/src/utils/tokens.ts +113 -3
  117. package/src/welcome.test.ts +213 -0
  118. package/src/welcome.ts +130 -0
  119. package/dist/chunk-MMUPR2JW.js +0 -1
@@ -0,0 +1,121 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import {
6
+ animateWelcomeOut,
7
+ applyWelcomeConfig,
8
+ applyWelcomeVisibility,
9
+ buildGreetingHost,
10
+ buildWelcomeHost,
11
+ renderWelcomeGreeting,
12
+ } from "./welcome";
13
+ import { resolveWelcomeConfig } from "../welcome";
14
+ import type { AgentWidgetConfig } from "../types";
15
+
16
+ const makeHost = () => {
17
+ const starters = document.createElement("div");
18
+ return buildWelcomeHost(undefined, starters);
19
+ };
20
+
21
+ describe("welcome host", () => {
22
+ it("carries the permanent host attributes", () => {
23
+ const { host } = makeHost();
24
+ expect(host.hasAttribute("data-persona-welcome")).toBe(true);
25
+ expect(host.hasAttribute("data-persona-intro-card")).toBe(true);
26
+ });
27
+
28
+ it("gives the title and subtitle their tokenized typography classes", () => {
29
+ const { title, subtitle } = makeHost();
30
+ expect(title.tagName).toBe("H2");
31
+ expect(title.className).toBe("persona-welcome-title");
32
+ expect(subtitle.tagName).toBe("P");
33
+ expect(subtitle.className).toBe("persona-welcome-subtitle");
34
+ });
35
+
36
+ it("applies resolved config in place", () => {
37
+ const elements = makeHost();
38
+ applyWelcomeConfig(
39
+ elements,
40
+ resolveWelcomeConfig({
41
+ welcome: { title: "Hi", subtitle: "Scope", variant: "hero" },
42
+ } as AgentWidgetConfig)
43
+ );
44
+ expect(elements.title.textContent).toBe("Hi");
45
+ expect(elements.subtitle.textContent).toBe("Scope");
46
+ expect(elements.host.getAttribute("data-persona-welcome-variant")).toBe(
47
+ "hero"
48
+ );
49
+ expect(elements.host.getAttribute("data-persona-welcome-dismiss")).toBe(
50
+ "on-first-message"
51
+ );
52
+ });
53
+
54
+ it("tightens the body gap when the surface is hidden", () => {
55
+ const { host } = makeHost();
56
+ const body = document.createElement("div");
57
+ body.className = "persona-gap-6";
58
+ applyWelcomeVisibility(body, host, false);
59
+ expect(body.classList.contains("persona-gap-3")).toBe(true);
60
+ expect(body.classList.contains("persona-gap-6")).toBe(false);
61
+ applyWelcomeVisibility(body, host, true);
62
+ expect(body.classList.contains("persona-gap-6")).toBe(true);
63
+ expect(host.hidden).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe("greeting bubble", () => {
68
+ it("renders the greeting as plain text, never markup", () => {
69
+ const host = buildGreetingHost();
70
+ renderWelcomeGreeting(host, "<b>bold</b> & fine", undefined);
71
+ expect(host.querySelector("b")).toBeNull();
72
+ expect(host.textContent).toBe("<b>bold</b> & fine");
73
+ });
74
+
75
+ it("clears and hides when the greeting is removed", () => {
76
+ const host = buildGreetingHost();
77
+ renderWelcomeGreeting(host, "Hi", undefined);
78
+ expect(host.hidden).toBe(false);
79
+ renderWelcomeGreeting(host, undefined, undefined);
80
+ expect(host.hidden).toBe(true);
81
+ expect(host.childElementCount).toBe(0);
82
+ });
83
+ });
84
+
85
+ describe("hero dismiss animation", () => {
86
+ it("runs on the Web Animations API and settles after it finishes", async () => {
87
+ const { host } = makeHost();
88
+ document.body.appendChild(host);
89
+ let resolveFinished: () => void = () => {};
90
+ const finished = new Promise<void>((resolve) => {
91
+ resolveFinished = resolve;
92
+ });
93
+ const animate = vi.fn(
94
+ (_keyframes: unknown, _options?: { fill?: string }) =>
95
+ ({ finished }) as unknown as Animation
96
+ );
97
+ (host as unknown as { animate: typeof animate }).animate = animate;
98
+
99
+ const onFinished = vi.fn();
100
+ const animation = animateWelcomeOut(host, onFinished);
101
+ expect(animate).toHaveBeenCalledTimes(1);
102
+ // `forwards`: `backwards` only fills the delay phase, so the element would
103
+ // flash back to opacity 1 after the keyframes end.
104
+ expect(animate.mock.calls[0]![1]?.fill).toBe("forwards");
105
+ expect(animation).toBeTruthy();
106
+ expect(onFinished).not.toHaveBeenCalled();
107
+
108
+ resolveFinished();
109
+ await finished;
110
+ await Promise.resolve();
111
+ expect(onFinished).toHaveBeenCalledWith(animation);
112
+ host.remove();
113
+ });
114
+
115
+ it("settles immediately without Web Animations support", () => {
116
+ const { host } = makeHost();
117
+ const onFinished = vi.fn();
118
+ expect(animateWelcomeOut(host, onFinished)).toBeNull();
119
+ expect(onFinished).toHaveBeenCalledWith(null);
120
+ });
121
+ });
@@ -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 controller and wires onToggle", () => {
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
- expect(element.tagName).toBe("BUTTON");
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, LauncherButton } from "./launcher";
5
+ import { createLauncherButton, createLauncherSurface, LauncherSurface } from "./launcher";
6
6
 
7
7
  /**
8
8
  * Widget view assembly layer.
@@ -30,8 +30,13 @@ export interface WidgetTranscriptRefs {
30
30
  container: HTMLElement;
31
31
  body: HTMLElement;
32
32
  messagesWrapper: HTMLElement;
33
+ starterSuggestions: HTMLElement;
34
+ transcriptSuggestions: HTMLElement;
33
35
  /** Absolute slot above the composer where interactive sheets mount. */
34
36
  composerOverlay: HTMLElement;
37
+ welcomeHost: HTMLElement;
38
+ welcomeIconHolder: HTMLElement;
39
+ greetingHost: HTMLElement;
35
40
  introTitle: HTMLElement;
36
41
  introSubtitle: HTMLElement;
37
42
  }
@@ -119,7 +124,12 @@ export const createWidgetView = ({
119
124
  container: panelElements.container,
120
125
  body: panelElements.body,
121
126
  messagesWrapper: panelElements.messagesWrapper,
127
+ starterSuggestions: panelElements.starterSuggestions,
128
+ transcriptSuggestions: panelElements.transcriptSuggestions,
122
129
  composerOverlay: panelElements.composerOverlay,
130
+ welcomeHost: panelElements.welcomeHost,
131
+ welcomeIconHolder: panelElements.welcomeIconHolder,
132
+ greetingHost: panelElements.greetingHost,
123
133
  introTitle: panelElements.introTitle,
124
134
  introSubtitle: panelElements.introSubtitle,
125
135
  };
@@ -195,11 +205,11 @@ export interface ResolveLauncherOptions {
195
205
 
196
206
  export interface ResolvedLauncher {
197
207
  /**
198
- * The default launcher controller (with `update`/`destroy`), or null when a
208
+ * The default launcher surface (with `update`/`destroy`), or null when a
199
209
  * plugin supplied a custom launcher element.
200
210
  */
201
- instance: LauncherButton | null;
202
- /** The element to mount: either the default button or the plugin's element. */
211
+ instance: LauncherSurface | null;
212
+ /** The element to mount: either the default surface or the plugin's element. */
203
213
  element: HTMLElement;
204
214
  }
205
215
 
@@ -209,8 +219,9 @@ export interface ResolvedLauncher {
209
219
  * both the initial-build and re-enable call sites in ui.ts.
210
220
  *
211
221
  * When a plugin returns a custom element, `instance` is null (the plugin owns
212
- * its own updates); otherwise the default `LauncherButton` controller is
213
- * returned so the caller can drive `update()`/`destroy()`.
222
+ * its own updates, including any teaser); otherwise the default
223
+ * `LauncherSurface` controller is returned so the caller can drive
224
+ * `update()`/`destroy()`.
214
225
  */
215
226
  export const resolveLauncher = ({
216
227
  config,
@@ -229,6 +240,6 @@ export const resolveLauncher = ({
229
240
  }
230
241
  }
231
242
 
232
- const instance = createLauncherButton(config, onToggle);
243
+ const instance = createLauncherSurface(config, onToggle);
233
244
  return { instance, element: instance.element };
234
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: "Hello 👋",
83
- welcomeSubtitle: "Ask anything about your account or products.",
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
  },
@@ -169,10 +169,12 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
169
169
  submitLabel: "Send",
170
170
  },
171
171
  },
172
+ // Placeholder copy that models the right shape: verb-first, user-voice,
173
+ // one line. Integrators should swap in domain prompts.
172
174
  suggestionChips: [
173
- "What can you help me with?",
174
- "Tell me about your features",
175
- "How does this work?",
175
+ "Show me what you can help with",
176
+ "Walk me through getting started",
177
+ "Answer a question about this page",
176
178
  ],
177
179
  suggestionChipsConfig: {
178
180
  fontFamily: "sans-serif",
@@ -260,7 +260,24 @@ export type RuntypeExecutionStreamEvent = ({
260
260
  type: "source";
261
261
  url?: string;
262
262
  [key: string]: unknown;
263
+ } | {
264
+ executionId: string;
265
+ iteration?: number;
266
+ seq: number;
267
+ state: Record<string, unknown>;
268
+ type: "state_snapshot";
263
269
  } | ({
270
+ executionId: string;
271
+ iteration?: number;
272
+ patch: Array<{
273
+ from?: string;
274
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
275
+ path: string;
276
+ value?: unknown;
277
+ }>;
278
+ seq: number;
279
+ type: "state_delta";
280
+ }) | ({
264
281
  executionId: string;
265
282
  hiddenParameterNames?: Array<string>;
266
283
  iteration?: number;
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,
@@ -140,7 +141,22 @@ export type {
140
141
  AskUserQuestionPrompt,
141
142
  AskUserQuestionOption,
142
143
  AgentWidgetAskUserQuestionFeature,
143
- AgentWidgetAskUserQuestionStyles
144
+ AgentWidgetAskUserQuestionStyles,
145
+ AgentWidgetSuggestion,
146
+ AgentWidgetSuggestionChipsConfig,
147
+ AgentWidgetResolvedSuggestion,
148
+ AgentWidgetSuggestionBehavior,
149
+ AgentWidgetSuggestionVariant,
150
+ AgentWidgetSuggestionSurface,
151
+ AgentWidgetSuggestionSource,
152
+ AgentWidgetSuggestionSurfaceConfig,
153
+ AgentWidgetStarterSuggestionsConfig,
154
+ AgentWidgetFollowUpSuggestionsConfig,
155
+ AgentWidgetSuggestionsConfig,
156
+ AgentWidgetWelcomeConfig,
157
+ AgentWidgetWelcomeIcon,
158
+ AgentWidgetWelcomeVariant,
159
+ AgentWidgetWelcomeDismiss
144
160
  } from "./types";
145
161
 
146
162
  export {
@@ -326,7 +342,17 @@ export { VERSION } from "./version";
326
342
  export type { AgentWidgetInitHandle };
327
343
 
328
344
  // Plugin system exports
329
- export type { AgentWidgetPlugin } from "./plugins/types";
345
+ export type {
346
+ AgentWidgetPlugin,
347
+ AgentWidgetTransformSuggestionsContext,
348
+ AgentWidgetRenderSuggestionContext,
349
+ AgentWidgetRenderWelcomeContext,
350
+ AgentWidgetSuggestionSelectContext
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";
330
356
  export { pluginRegistry } from "./plugins/registry";
331
357
 
332
358
  // Stream animation plugin API: lets consumers register custom animations
@@ -431,6 +457,7 @@ export type {
431
457
  SemanticSpacing,
432
458
  SemanticTypography,
433
459
  ComponentTokens,
460
+ TextStyleTokens,
434
461
  ArtifactToolbarTokens,
435
462
  ArtifactTabTokens,
436
463
  ArtifactPaneTokens,
@@ -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 (`createLauncherButton`) plus the
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 { createLauncherButton } from "./components/launcher";
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";
@@ -76,7 +77,7 @@ const mergeCriticalLauncherConfig = (
76
77
  * Mirrors the full widget's DOM exactly (`runtime/init.ts` + `ui.ts`): a
77
78
  * `[data-persona-root]` wrapper carries the theme CSS variables and the launcher
78
79
  * button is its child. Keeping the theme vars on the wrapper (not the button)
79
- * leaves the button's own inline style byte-identical to the full widget's, so
80
+ * leaves the button's own inline style matching the full widget's, so
80
81
  * the eventual mount-then-remove handoff is invisible.
81
82
  */
82
83
  export const mount = (
@@ -100,20 +101,22 @@ export const mount = (
100
101
  root.setAttribute(CRITICAL_LAUNCHER_ATTR, "true");
101
102
  applyThemeVariables(root, config);
102
103
 
103
- const launcher = createLauncherButton(config, onOpen);
104
- root.appendChild(launcher.element);
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
- launcher.update(merged);
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
- launcher.destroy();
119
+ surface.destroy();
117
120
  root.remove();
118
121
  },
119
122
  };