@runtypelabs/persona 4.23.2 → 4.25.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 (166) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
  4. package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +2 -0
  8. package/dist/animations-extra.d.ts +2 -0
  9. package/dist/approval-ui-3NMYNXBD.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +278 -37
  12. package/dist/approval-ui.d.ts +278 -37
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
  15. package/dist/artifacts-ui.d.cts +273 -25
  16. package/dist/artifacts-ui.d.ts +273 -25
  17. package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
  18. package/dist/codegen.cjs +11 -11
  19. package/dist/codegen.js +13 -13
  20. package/dist/context-mentions.d.cts +273 -25
  21. package/dist/context-mentions.d.ts +273 -25
  22. package/dist/event-stream-view.d.cts +273 -25
  23. package/dist/event-stream-view.d.ts +273 -25
  24. package/dist/forms-ui.d.cts +319 -27
  25. package/dist/forms-ui.d.ts +319 -27
  26. package/dist/history-view.d.cts +2 -0
  27. package/dist/history-view.d.ts +2 -0
  28. package/dist/index.cjs +70 -69
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +344 -39
  31. package/dist/index.d.ts +344 -39
  32. package/dist/index.global.js +57 -54
  33. package/dist/index.global.js.map +1 -1
  34. package/dist/index.js +70 -69
  35. package/dist/index.js.map +1 -1
  36. package/dist/install.global.js +1 -1
  37. package/dist/install.global.js.map +1 -1
  38. package/dist/launcher.global.js +2 -2
  39. package/dist/launcher.global.js.map +1 -1
  40. package/dist/session-reconnect.d.cts +273 -25
  41. package/dist/session-reconnect.d.ts +273 -25
  42. package/dist/smart-dom-reader.d.cts +273 -25
  43. package/dist/smart-dom-reader.d.ts +273 -25
  44. package/dist/theme-editor-preview.cjs +74 -73
  45. package/dist/theme-editor-preview.d.cts +280 -25
  46. package/dist/theme-editor-preview.d.ts +280 -25
  47. package/dist/theme-editor-preview.js +51 -50
  48. package/dist/theme-editor.cjs +10 -10
  49. package/dist/theme-editor.d.cts +293 -27
  50. package/dist/theme-editor.d.ts +293 -27
  51. package/dist/theme-editor.js +10 -10
  52. package/dist/theme-reference.cjs +1 -1
  53. package/dist/theme-reference.d.cts +1522 -11
  54. package/dist/theme-reference.d.ts +1522 -11
  55. package/dist/theme-reference.js +1 -1
  56. package/dist/voice-runtime-7DA7MJH4.js +1 -0
  57. package/dist/voice-runtime.cjs +1 -1
  58. package/dist/voice-runtime.d.cts +33 -0
  59. package/dist/voice-runtime.d.ts +33 -0
  60. package/dist/voice-runtime.js +1 -1
  61. package/dist/widget.css +1 -1
  62. package/package.json +3 -2
  63. package/src/__fixtures__/defaults-parity.v4.json +776 -0
  64. package/src/approval-ui-entry.ts +0 -1
  65. package/src/approval-ui-loader.ts +5 -4
  66. package/src/approval-ui.ts +0 -1
  67. package/src/client-identity-proof.test.ts +193 -0
  68. package/src/client-session-warm.test.ts +216 -0
  69. package/src/client-visitor-history.test.ts +6 -0
  70. package/src/client.ts +122 -30
  71. package/src/codegen.test.ts +24 -0
  72. package/src/components/activity-row.test.ts +100 -0
  73. package/src/components/activity-row.ts +136 -0
  74. package/src/components/approval-actions.test.ts +8 -5
  75. package/src/components/approval-actions.ts +19 -6
  76. package/src/components/approval-bubble.test.ts +56 -9
  77. package/src/components/approval-bubble.ts +36 -41
  78. package/src/components/composer-builder.ts +5 -2
  79. package/src/components/composer-parts.ts +10 -2
  80. package/src/components/header-builder.test.ts +32 -0
  81. package/src/components/header-builder.ts +7 -4
  82. package/src/components/header-layouts.test.ts +12 -0
  83. package/src/components/header-layouts.ts +3 -2
  84. package/src/components/launcher.ts +11 -2
  85. package/src/components/message-bubble.test.ts +22 -0
  86. package/src/components/message-bubble.ts +7 -3
  87. package/src/components/panel.test.ts +52 -1
  88. package/src/components/panel.ts +47 -8
  89. package/src/components/pill-composer-builder.test.ts +2 -0
  90. package/src/components/pill-composer-builder.ts +0 -1
  91. package/src/components/reasoning-bubble.ts +29 -17
  92. package/src/components/suggestions.test.ts +1 -0
  93. package/src/components/suggestions.ts +0 -16
  94. package/src/components/tool-bubble.ts +131 -114
  95. package/src/components/tool-details.test.ts +87 -0
  96. package/src/components/tool-details.ts +102 -0
  97. package/src/components/welcome.test.ts +8 -0
  98. package/src/components/welcome.ts +4 -2
  99. package/src/defaults-parity.test.ts +158 -0
  100. package/src/defaults.test.ts +69 -0
  101. package/src/defaults.ts +128 -49
  102. package/src/generated/runtype-openapi-contract.ts +2 -2
  103. package/src/index-core.ts +4 -0
  104. package/src/install.test.ts +24 -0
  105. package/src/install.ts +3 -1
  106. package/src/launcher-global.ts +2 -1
  107. package/src/launcher-variants.test.ts +52 -0
  108. package/src/runtime/init-update-reset.test.ts +21 -0
  109. package/src/session.ts +59 -7
  110. package/src/session.voice.test.ts +72 -1
  111. package/src/styles/widget-styles.test.ts +19 -0
  112. package/src/styles/widget.css +295 -23
  113. package/src/theme-editor/index.ts +2 -0
  114. package/src/theme-editor/preview-utils.test.ts +8 -0
  115. package/src/theme-editor/preview-utils.ts +7 -3
  116. package/src/theme-editor/sections.test.ts +31 -0
  117. package/src/theme-editor/sections.ts +55 -1
  118. package/src/theme-editor/state-preferences.test.ts +49 -0
  119. package/src/theme-editor/state.ts +73 -15
  120. package/src/theme-reference.test.ts +20 -0
  121. package/src/theme-reference.ts +11 -5
  122. package/src/tool-call-display-defaults.test.ts +33 -1
  123. package/src/types/theme.ts +80 -3
  124. package/src/types.ts +207 -22
  125. package/src/ui.activity-row.test.ts +91 -0
  126. package/src/ui.approval-plugin.test.ts +63 -0
  127. package/src/ui.composer-compact.test.ts +24 -2
  128. package/src/ui.composer-placement.test.ts +23 -0
  129. package/src/ui.detached-panel.test.ts +33 -6
  130. package/src/ui.docked.test.ts +19 -0
  131. package/src/ui.header-update-stability.test.ts +25 -0
  132. package/src/ui.identity-proof.test.ts +58 -0
  133. package/src/ui.launcher-teaser.test.ts +25 -0
  134. package/src/ui.message-width.test.ts +17 -2
  135. package/src/ui.reasoning-display.test.ts +92 -0
  136. package/src/ui.rebuild-composer.test.ts +20 -3
  137. package/src/ui.scroll.test.ts +32 -0
  138. package/src/ui.session-init.test.ts +312 -0
  139. package/src/ui.status-indicator.test.ts +82 -0
  140. package/src/ui.suggest-replies.test.ts +11 -0
  141. package/src/ui.ts +422 -74
  142. package/src/ui.v5-defaults.test.ts +144 -0
  143. package/src/ui.voice-prewarm.test.ts +222 -0
  144. package/src/ui.welcome.test.ts +92 -3
  145. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
  146. package/src/utils/code-generators.ts +14 -0
  147. package/src/utils/composer-compact.test.ts +0 -2
  148. package/src/utils/composer-compact.ts +2 -4
  149. package/src/utils/composer-placement.ts +2 -3
  150. package/src/utils/config-merge.test.ts +81 -5
  151. package/src/utils/config-merge.ts +13 -3
  152. package/src/utils/defaults-provenance.ts +67 -0
  153. package/src/utils/dock.ts +7 -1
  154. package/src/utils/launcher-variant.ts +16 -0
  155. package/src/utils/panel-config.ts +97 -0
  156. package/src/utils/status-indicator.ts +39 -0
  157. package/src/utils/theme.test.ts +182 -0
  158. package/src/utils/theme.ts +131 -18
  159. package/src/utils/tokens.test.ts +122 -1
  160. package/src/utils/tokens.ts +177 -17
  161. package/src/voice/runtype-voice-provider.ts +238 -27
  162. package/src/voice/voice.test.ts +272 -2
  163. package/src/welcome.test.ts +23 -0
  164. package/src/welcome.ts +16 -9
  165. package/dist/approval-ui-K7R5Q5OC.js +0 -1
  166. package/dist/voice-runtime-EFR3R5ES.js +0 -1
@@ -0,0 +1,52 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { createAgentExperience } from "./ui";
4
+ import { mount as mountCritical } from "./launcher-global";
5
+ import { getActiveTheme, themeToCssVariables } from "./utils/theme";
6
+ import type { AgentWidgetConfig } from "./types";
7
+
8
+ afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); });
9
+
10
+ describe.each([false, true])("launcher variants (v5=%s)", (v5Defaults) => {
11
+ it.each([undefined, "pill", "circle"] as const)("matches deferred and full rendering for %s", (variant) => {
12
+ window.scrollTo = vi.fn();
13
+ const config: AgentWidgetConfig = {
14
+ apiUrl: "/dispatch", persistState: false, future: { v5Defaults },
15
+ launcher: { ...(variant ? { variant } : {}), title: "Open support", teaser: { text: "Need help?" } },
16
+ };
17
+ const host = document.body.appendChild(document.createElement("div"));
18
+ const target = document.body.appendChild(document.createElement("div"));
19
+ const full = createAgentExperience(host, config);
20
+ const critical = mountCritical({ target, config, onOpen: vi.fn() });
21
+ const button = host.querySelector<HTMLButtonElement>(".persona-launcher-surface > button")!;
22
+ try {
23
+ const circle = variant ? variant === "circle" : v5Defaults;
24
+ expect(button.dataset.personaLauncherVariant).toBe(circle ? "circle" : undefined);
25
+ expect(button.innerHTML).toBe(critical.element.innerHTML);
26
+ expect(button.getAttribute("style")).toBe(critical.element.getAttribute("style"));
27
+ expect(host.style.getPropertyValue("--persona-launcher-bg"))
28
+ .toBe(critical.root.style.getPropertyValue("--persona-launcher-bg"));
29
+ if (circle) expect(button.getAttribute("aria-label")).toBe("Open support");
30
+ full.update({ launcher: { variant: circle ? "pill" : "circle" } });
31
+ critical.update({ ...config, launcher: { ...config.launcher, variant: circle ? "pill" : "circle" } });
32
+ expect(button.dataset.personaLauncherVariant).toBe(circle ? undefined : "circle");
33
+ expect(button.innerHTML).toBe(critical.element.innerHTML);
34
+ } finally { full.destroy(); critical.destroy(); }
35
+ });
36
+
37
+ it.each(["light", "dark"] as const)("resolves circle geometry and explicit tokens in %s", colorScheme => {
38
+ const config: AgentWidgetConfig = { future: { v5Defaults }, colorScheme, launcher: { variant: "circle" } };
39
+ const css = themeToCssVariables(getActiveTheme(config));
40
+ expect(css["--persona-components-launcher-size"]).toBe("48px");
41
+ expect(css["--persona-components-launcher-offset"]).toBe("20px");
42
+ expect(css["--persona-launcher-shadow"]).toBe("0 8px 24px rgba(0,0,0,.16)");
43
+ expect(css["--persona-launcher-bg"]).not.toBe(css["--persona-launcher-fg"]);
44
+ const custom = themeToCssVariables(getActiveTheme({ ...config, theme: { components: { launcher: {
45
+ size: "56px", iconSize: "20px", offset: "32px", background: "#123456", foreground: "#ffffff", shadow: "none",
46
+ } } } }));
47
+ expect(custom["--persona-components-launcher-size"]).toBe("56px");
48
+ expect(custom["--persona-components-launcher-offset"]).toBe("32px");
49
+ expect(custom["--persona-launcher-bg"]).toBe("#123456");
50
+ expect(custom["--persona-launcher-shadow"]).toBe("none");
51
+ });
52
+ });
@@ -78,4 +78,25 @@ describe("handle.update explicit-undefined reset reaches the controller", () =>
78
78
 
79
79
  handle.destroy();
80
80
  });
81
+
82
+ it("updates the root defaults-version attribute", () => {
83
+ window.scrollTo = vi.fn();
84
+ document.body.innerHTML = `<div id="target"></div>`;
85
+
86
+ const handle = initAgentWidget({
87
+ target: "#target",
88
+ config: {
89
+ apiUrl: "https://api.example.com/chat",
90
+ launcher: { enabled: false },
91
+ future: { v5Defaults: false },
92
+ },
93
+ });
94
+ const root = () => document.querySelector<HTMLElement>("[data-persona-root]")!;
95
+
96
+ expect(root().getAttribute("data-persona-defaults")).toBe("v4");
97
+ handle.update({ future: { v5Defaults: true } });
98
+ expect(root().getAttribute("data-persona-defaults")).toBe("v5");
99
+
100
+ handle.destroy();
101
+ });
81
102
  });
package/src/session.ts CHANGED
@@ -144,6 +144,7 @@ const CONNECTION_CONFIG_KEYS = [
144
144
  "getStoredSessionId",
145
145
  "setStoredSessionId",
146
146
  "getIdentityProof",
147
+ "identityProvider",
147
148
  "getStoredConversationId",
148
149
  "setStoredConversationId",
149
150
  "clearStoredConversationId",
@@ -909,6 +910,33 @@ export class AgentWidgetSession {
909
910
  }
910
911
  }
911
912
 
913
+ /**
914
+ * Warm the voice path ahead of the mic click (`voiceRecognition.prewarm`).
915
+ * Session-owned providers (`runtype`, `custom`) get `provider.prewarm()` once
916
+ * the lazy voice runtime has installed them; the browser (Web Speech) path
917
+ * warms the client-token session instead. Fire-and-forget and silent.
918
+ */
919
+ public prewarmVoice(): void {
920
+ if (!usesSessionVoice(this.config.voiceRecognition?.provider)) {
921
+ this.warmClientSession();
922
+ return;
923
+ }
924
+ const generation = this.voiceSetupGeneration;
925
+ const run = () => {
926
+ const provider = this.voiceProvider;
927
+ if (!provider?.prewarm || generation !== this.voiceSetupGeneration) return;
928
+ if (this.voiceActive || this.isBargeInActive()) return;
929
+ void Promise.resolve()
930
+ .then(() => provider.prewarm?.())
931
+ .catch(() => {});
932
+ };
933
+ if (!this.voiceProvider && this.voiceSetupPromise) {
934
+ void this.voiceSetupPromise.then(run, () => {});
935
+ return;
936
+ }
937
+ run();
938
+ }
939
+
912
940
  /**
913
941
  * Cleanup voice resources
914
942
  */
@@ -974,7 +1002,9 @@ export class AgentWidgetSession {
974
1002
  clientToken: providerConfig.runtype?.clientToken ?? this.config.clientToken,
975
1003
  host: providerConfig.runtype?.host ?? this.config.apiUrl,
976
1004
  voiceId: providerConfig.runtype?.voiceId,
977
- createPlaybackEngine: providerConfig.runtype?.createPlaybackEngine
1005
+ createPlaybackEngine: providerConfig.runtype?.createPlaybackEngine,
1006
+ prewarmMode: providerConfig.runtype?.prewarmMode,
1007
+ attachIdleMs: providerConfig.runtype?.attachIdleMs
978
1008
  }
979
1009
  };
980
1010
 
@@ -1033,12 +1063,29 @@ export class AgentWidgetSession {
1033
1063
  }
1034
1064
  }
1035
1065
 
1066
+ /**
1067
+ * Early, fire-and-forget client-token init (`sessionInit` trigger or
1068
+ * `controller.initSession()`). Failures stay silent here: no `onError`, no
1069
+ * UI. The send re-runs `initSession()` and reports them as it always has.
1070
+ */
1071
+ public warmClientSession(): void {
1072
+ // The visitor may send before this resolves; the server welcome still
1073
+ // belongs ahead of that first turn when the transcript started empty.
1074
+ const pristine = !this.messages.length;
1075
+ void this.client.warmSession().then((session) => {
1076
+ // A start-new or credential re-init may have replaced it meanwhile.
1077
+ if (session && this.client.getClientSession() === session) {
1078
+ this.setClientSession(session, pristine);
1079
+ }
1080
+ });
1081
+ }
1082
+
1036
1083
  /**
1037
1084
  * Set the client session after initialization
1038
1085
  */
1039
- public setClientSession(session: ClientSession): void {
1086
+ public setClientSession(session: ClientSession, pristine?: boolean): void {
1040
1087
  this.clientSession = session;
1041
- this.injectWelcomeMessage(session);
1088
+ this.injectWelcomeMessage(session, pristine);
1042
1089
  }
1043
1090
 
1044
1091
  /**
@@ -1046,15 +1093,20 @@ export class AgentWidgetSession {
1046
1093
  * reopening a stored conversation is not a new thread, and the hydrated
1047
1094
  * transcript would otherwise race a welcome bubble.
1048
1095
  */
1049
- private injectWelcomeMessage(session: ClientSession): void {
1096
+ private injectWelcomeMessage(
1097
+ session: ClientSession,
1098
+ pristine = !this.messages.length
1099
+ ): void {
1050
1100
  if (this.suppressWelcomeInjection) return;
1051
- if (!session.config.welcomeMessage || this.messages.length !== 0) return;
1101
+ if (!session.config.welcomeMessage || !pristine) return;
1102
+ // A late welcome (the first send raced the init) sorts ahead of that turn.
1103
+ const first = this.messages[0];
1052
1104
  const welcomeMessage: AgentWidgetMessage = {
1053
1105
  id: `welcome-${Date.now()}`,
1054
1106
  role: "assistant",
1055
1107
  content: session.config.welcomeMessage,
1056
- createdAt: new Date().toISOString(),
1057
- sequence: this.nextSequence()
1108
+ createdAt: first?.createdAt ?? new Date().toISOString(),
1109
+ sequence: first ? (first.sequence ?? 0) - 1 : this.nextSequence()
1058
1110
  };
1059
1111
  this.appendMessage(welcomeMessage);
1060
1112
  }
@@ -13,7 +13,8 @@ const h = vi.hoisted(() => {
13
13
  | ((role: 'user' | 'assistant', text: string, isFinal: boolean) => void)
14
14
  | null;
15
15
  metricsCb: ((m: VoiceMetrics) => void) | null;
16
- } = { transcriptCb: null, metricsCb: null };
16
+ prewarms: number;
17
+ } = { transcriptCb: null, metricsCb: null, prewarms: 0 };
17
18
 
18
19
  const fakeProvider = {
19
20
  type: 'runtype' as const,
@@ -30,6 +31,9 @@ const h = vi.hoisted(() => {
30
31
  onMetrics: (cb: typeof state.metricsCb) => {
31
32
  state.metricsCb = cb;
32
33
  },
34
+ prewarm: () => {
35
+ state.prewarms += 1;
36
+ },
33
37
  };
34
38
 
35
39
  return { state, fakeProvider };
@@ -212,3 +216,70 @@ describe('AgentWidgetSession - Runtype TTS config', () => {
212
216
  }
213
217
  });
214
218
  });
219
+
220
+ describe('AgentWidgetSession - prewarmVoice', () => {
221
+ const callbacks = {
222
+ onMessagesChanged: () => {},
223
+ onStatusChanged: () => {},
224
+ onStreamingChanged: () => {},
225
+ onError: () => {},
226
+ };
227
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
228
+ const runtypeSession = () =>
229
+ new AgentWidgetSession(
230
+ {
231
+ apiUrl: 'http://localhost:8000',
232
+ voiceRecognition: {
233
+ enabled: true,
234
+ provider: { type: 'runtype', runtype: { agentId: 'a1' } },
235
+ },
236
+ },
237
+ callbacks,
238
+ );
239
+
240
+ beforeEach(() => {
241
+ h.state.prewarms = 0;
242
+ });
243
+
244
+ it("calls the provider's prewarm once setup has installed it", async () => {
245
+ const session = runtypeSession();
246
+ session.setupVoice();
247
+ await flush();
248
+ session.prewarmVoice();
249
+ await flush();
250
+ expect(h.state.prewarms).toBe(1);
251
+ });
252
+
253
+ it('waits for a setup still loading the voice runtime', async () => {
254
+ const session = runtypeSession();
255
+ session.setupVoice();
256
+ session.prewarmVoice();
257
+ expect(h.state.prewarms).toBe(0);
258
+ await flush();
259
+ expect(h.state.prewarms).toBe(1);
260
+ });
261
+
262
+ it('drops a prewarm whose setup was torn down meanwhile', async () => {
263
+ const session = runtypeSession();
264
+ session.setupVoice();
265
+ session.prewarmVoice();
266
+ session.cleanupVoice();
267
+ await flush();
268
+ expect(h.state.prewarms).toBe(0);
269
+ });
270
+
271
+ it('warms the client session on the browser (Web Speech) path', () => {
272
+ const session = new AgentWidgetSession(
273
+ {
274
+ apiUrl: 'http://localhost:8000',
275
+ clientToken: 'ct_test',
276
+ voiceRecognition: { enabled: true, provider: { type: 'browser' } },
277
+ },
278
+ callbacks,
279
+ );
280
+ const warm = vi.spyOn(session, 'warmClientSession').mockImplementation(() => {});
281
+ session.prewarmVoice();
282
+ expect(warm).toHaveBeenCalledOnce();
283
+ expect(h.state.prewarms).toBe(0);
284
+ });
285
+ });
@@ -189,6 +189,25 @@ describe("suggestion interaction styles", () => {
189
189
  });
190
190
  });
191
191
 
192
+ describe("collapsible widget chrome styles", () => {
193
+ it("uses prefixed aliases while retaining each legacy alias and literal fallback", () => {
194
+ const expectedDeclarations: Array<[string, number]> = [
195
+ ["var(--persona-cw-border, var(--cw-border, #e5e7eb))", 2],
196
+ ["var(--persona-cw-container, var(--cw-container, #f9fafb))", 1],
197
+ ["var(--persona-cw-surface, var(--cw-surface, #ffffff))", 2],
198
+ ["var(--persona-cw-border, var(--cw-border, #f1f5f9))", 1],
199
+ ["var(--persona-cw-container, var(--cw-container, #f8fafc))", 1],
200
+ ["var(--persona-cw-border, var(--cw-border, #d1d5db))", 1],
201
+ ["var(--persona-cw-surface, var(--cw-surface, #fff))", 2],
202
+ ];
203
+
204
+ for (const [declaration, count] of expectedDeclarations) {
205
+ expect(widgetCss).toContain(declaration);
206
+ expect(widgetCss.split(declaration).length - 1).toBe(count);
207
+ }
208
+ });
209
+ });
210
+
192
211
  describe("composer spacing styles", () => {
193
212
  it("themes the composer form padding and gap with utility-matching fallbacks", () => {
194
213
  const selector =