@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
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
4
  import {
5
- approvalDetailsExpansionState,
6
5
  createApprovalBubble,
7
6
  humanizeToolName,
8
7
  updateApprovalDetailsUI,
@@ -11,6 +10,7 @@ import {
11
10
  getWebMcpToolDisplayTitle,
12
11
  recordWebMcpToolDisplayTitles,
13
12
  } from "../webmcp-bridge";
13
+ import { createTheme, themeToCssVariables } from "../utils/tokens";
14
14
  import type {
15
15
  AgentWidgetApproval,
16
16
  AgentWidgetConfig,
@@ -74,6 +74,51 @@ describe("createApprovalBubble shadow", () => {
74
74
  });
75
75
  });
76
76
 
77
+ describe("createApprovalBubble resolved-status colors", () => {
78
+ const colorFor = (status: "approved" | "denied" | "timeout") =>
79
+ createApprovalBubble(makeMessage({ status }))
80
+ .querySelector("[data-approval-icon] svg")
81
+ ?.getAttribute("stroke") ?? "";
82
+
83
+ const resolveCssVariable = (
84
+ value: string,
85
+ variables: Record<string, string>
86
+ ): string | undefined => {
87
+ const match = /^var\((--[\w-]+)(?:,\s*(.+))?\)$/.exec(value.trim());
88
+ if (!match) return value;
89
+ return variables[match[1]] ??
90
+ (match[2] ? resolveCssVariable(match[2], variables) : undefined);
91
+ };
92
+
93
+ it("resolves each legacy feedback color through emitted palette tokens", () => {
94
+ const variables = themeToCssVariables(createTheme());
95
+
96
+ expect(resolveCssVariable(colorFor("approved"), variables)).toBe("#16a34a");
97
+ expect(resolveCssVariable(colorFor("denied"), variables)).toBe("#dc2626");
98
+ expect(resolveCssVariable(colorFor("timeout"), variables)).toBe("#ca8a04");
99
+ });
100
+
101
+ it("keeps host feedback token overrides ahead of palette fallbacks", () => {
102
+ const variables = {
103
+ ...themeToCssVariables(createTheme()),
104
+ "--persona-feedback-error": "rebeccapurple",
105
+ };
106
+
107
+ expect(resolveCssVariable(colorFor("denied"), variables)).toBe(
108
+ "rebeccapurple"
109
+ );
110
+ });
111
+
112
+ it("gives the pending Deny label and icon a color emitted by the default theme", () => {
113
+ const bubble = createApprovalBubble(makeMessage());
114
+ const deny = bubble.querySelector<HTMLButtonElement>('[data-approval-action="deny"]')!;
115
+ const variables = themeToCssVariables(createTheme());
116
+ expect(resolveCssVariable(deny.style.color, variables)).toBe("#dc2626");
117
+ expect(resolveCssVariable(deny.querySelector("svg")!.getAttribute("stroke")!, variables))
118
+ .toBe("#dc2626");
119
+ });
120
+ });
121
+
77
122
  const TOOL_DESCRIPTION =
78
123
  "Add products to the shopping cart. IMPORTANT: If a product has required options you MUST include the variantId.";
79
124
 
@@ -95,12 +140,14 @@ const getDetails = (bubble: HTMLElement) =>
95
140
  bubble.querySelector("[data-approval-details]") as HTMLElement | null;
96
141
 
97
142
  beforeEach(() => {
98
- approvalDetailsExpansionState.clear();
143
+ expansionState = new Map<string, boolean>();
99
144
  // Reset display titles recorded by a prior test (the map is rebuilt from
100
145
  // each full snapshot, so recording an empty snapshot clears it).
101
146
  recordWebMcpToolDisplayTitles([]);
102
147
  });
103
148
 
149
+ let expansionState: Map<string, boolean>;
150
+
104
151
  describe("humanizeToolName", () => {
105
152
  it("converts snake_case to a sentence", () => {
106
153
  expect(humanizeToolName("add_to_cart")).toBe("Add to cart");
@@ -157,8 +204,8 @@ describe("createApprovalBubble summary and details", () => {
157
204
  });
158
205
 
159
206
  it("respects a per-message expansion override from prior toggling", () => {
160
- approvalDetailsExpansionState.set("msg-1", true);
161
- const bubble = createApprovalBubble(makeDetailedMessage());
207
+ expansionState.set("msg-1", true);
208
+ const bubble = createApprovalBubble(makeDetailedMessage(), undefined, expansionState);
162
209
  expect(getToggle(bubble)?.getAttribute("aria-expanded")).toBe("true");
163
210
  expect(getDetails(bubble)?.style.display).toBe("");
164
211
  });
@@ -204,7 +251,7 @@ describe("createApprovalBubble summary and details", () => {
204
251
  recordWebMcpToolDisplayTitles([
205
252
  { name: "add_to_cart", description: "", title: "Add to Cart" },
206
253
  ]);
207
- const bubble = createApprovalBubble(makeDetailedMessage());
254
+ const bubble = createApprovalBubble(makeDetailedMessage(), undefined, expansionState);
208
255
  expect(getSummary(bubble)?.textContent).toBe(
209
256
  "The assistant wants to use “Add to Cart”."
210
257
  );
@@ -265,14 +312,14 @@ describe("updateApprovalDetailsUI", () => {
265
312
  const bubble = createApprovalBubble(makeDetailedMessage());
266
313
  expect(getDetails(bubble)?.style.display).toBe("none");
267
314
 
268
- approvalDetailsExpansionState.set("msg-1", true);
269
- updateApprovalDetailsUI("msg-1", bubble);
315
+ expansionState.set("msg-1", true);
316
+ updateApprovalDetailsUI("msg-1", bubble, undefined, expansionState);
270
317
  expect(getDetails(bubble)?.style.display).toBe("");
271
318
  expect(getToggle(bubble)?.getAttribute("aria-expanded")).toBe("true");
272
319
  expect(getToggle(bubble)?.textContent).toContain("Hide details");
273
320
 
274
- approvalDetailsExpansionState.set("msg-1", false);
275
- updateApprovalDetailsUI("msg-1", bubble);
321
+ expansionState.set("msg-1", false);
322
+ updateApprovalDetailsUI("msg-1", bubble, undefined, expansionState);
276
323
  expect(getDetails(bubble)?.style.display).toBe("none");
277
324
  expect(getToggle(bubble)?.textContent).toContain("Show details");
278
325
  });
@@ -9,14 +9,6 @@ import { ChevronDown, ChevronUp, ShieldAlert, ShieldCheck, ShieldX } from "lucid
9
9
  import { renderIconNode } from "../utils/icon-node";
10
10
  import { WEBMCP_TOOL_PREFIX } from "../webmcp-bridge";
11
11
 
12
- /**
13
- * Per-message expanded/collapsed state for the technical-details section.
14
- * Absent means "use the config default" (`approval.detailsDisplay`). Lives at
15
- * module scope so the choice survives idiomorph re-renders, mirroring
16
- * `toolExpansionState` in tool-bubble.ts.
17
- */
18
- export const approvalDetailsExpansionState = new Map<string, boolean>();
19
-
20
12
  /**
21
13
  * Turn a wire tool name into a user-facing label: strips the `webmcp:`
22
14
  * prefix and splits snake_case / kebab-case / camelCase into a sentence
@@ -41,10 +33,11 @@ const resolveApprovalConfig = (config?: AgentWidgetConfig) =>
41
33
 
42
34
  const isDetailsExpanded = (
43
35
  messageId: string,
36
+ expansionState: Map<string, boolean>,
44
37
  config?: AgentWidgetConfig
45
38
  ): boolean => {
46
39
  const detailsMode = resolveApprovalConfig(config)?.detailsDisplay ?? "collapsed";
47
- return approvalDetailsExpansionState.get(messageId) ?? detailsMode === "expanded";
40
+ return expansionState.get(messageId) ?? detailsMode === "expanded";
48
41
  };
49
42
 
50
43
  const applyDetailsToggleState = (
@@ -72,18 +65,19 @@ const applyDetailsToggleState = (
72
65
 
73
66
  /**
74
67
  * Sync the technical-details section (toggle label/chevron + visibility) with
75
- * `approvalDetailsExpansionState`. Called from the ui.ts expansion event
68
+ * the widget instance's expansion state. Called from the ui.ts expansion event
76
69
  * delegation after a toggle click.
77
70
  */
78
71
  export const updateApprovalDetailsUI = (
79
72
  messageId: string,
80
73
  bubble: HTMLElement,
81
- config?: AgentWidgetConfig
74
+ config?: AgentWidgetConfig,
75
+ expansionState = new Map<string, boolean>()
82
76
  ): void => {
83
77
  const toggle = bubble.querySelector('button[data-bubble-type="approval"]') as HTMLElement | null;
84
78
  const details = bubble.querySelector("[data-approval-details]") as HTMLElement | null;
85
79
  if (!toggle || !details) return;
86
- const expanded = isDetailsExpanded(messageId, config);
80
+ const expanded = isDetailsExpanded(messageId, expansionState, config);
87
81
  applyDetailsToggleState(toggle, expanded, config);
88
82
  details.style.display = expanded ? "" : "none";
89
83
  };
@@ -112,16 +106,16 @@ export const updateApprovalBubbleUI = (
112
106
  // Update badge color
113
107
  if (approval.status === "approved") {
114
108
  statusBadge.className = "persona-inline-flex persona-items-center persona-px-2 persona-py-0.5 persona-rounded-full persona-text-xs persona-font-medium";
115
- statusBadge.style.backgroundColor = "var(--persona-palette-colors-success-100, #dcfce7)";
116
- statusBadge.style.color = "var(--persona-palette-colors-success-700, #15803d)";
109
+ statusBadge.style.backgroundColor = "var(--persona-palette-colors-success-100)";
110
+ statusBadge.style.color = "var(--persona-palette-colors-success-700)";
117
111
  } else if (approval.status === "denied") {
118
112
  statusBadge.className = "persona-inline-flex persona-items-center persona-px-2 persona-py-0.5 persona-rounded-full persona-text-xs persona-font-medium";
119
- statusBadge.style.backgroundColor = "var(--persona-palette-colors-error-100, #fee2e2)";
120
- statusBadge.style.color = "var(--persona-palette-colors-error-700, #b91c1c)";
113
+ statusBadge.style.backgroundColor = "var(--persona-palette-colors-error-100)";
114
+ statusBadge.style.color = "var(--persona-palette-colors-error-700)";
121
115
  } else if (approval.status === "timeout") {
122
116
  statusBadge.className = "persona-inline-flex persona-items-center persona-px-2 persona-py-0.5 persona-rounded-full persona-text-xs persona-font-medium";
123
- statusBadge.style.backgroundColor = "var(--persona-palette-colors-warning-100, #fef3c7)";
124
- statusBadge.style.color = "var(--persona-palette-colors-warning-700, #b45309)";
117
+ statusBadge.style.backgroundColor = "var(--persona-palette-colors-warning-100)";
118
+ statusBadge.style.color = "var(--persona-palette-colors-warning-700)";
125
119
  }
126
120
  statusBadge.setAttribute("data-approval-status", approval.status);
127
121
  }
@@ -133,9 +127,9 @@ export const updateApprovalBubbleUI = (
133
127
  const iconData = approval.status === "denied" ? ShieldX
134
128
  : approval.status === "timeout" ? ShieldAlert
135
129
  : ShieldCheck;
136
- const iconColor = approval.status === "approved" ? "var(--persona-feedback-success, #16a34a)"
137
- : approval.status === "denied" ? "var(--persona-feedback-error, #dc2626)"
138
- : approval.status === "timeout" ? "var(--persona-feedback-warning, #ca8a04)"
130
+ const iconColor = approval.status === "approved" ? "var(--persona-feedback-success, var(--persona-palette-colors-success-600))"
131
+ : approval.status === "denied" ? "var(--persona-feedback-error, var(--persona-palette-colors-error-600))"
132
+ : approval.status === "timeout" ? "var(--persona-feedback-warning, var(--persona-palette-colors-warning-600))"
139
133
  : (approvalConfig?.titleColor ?? "currentColor");
140
134
  const icon = renderIconNode(iconData, 20, iconColor, 2);
141
135
  if (icon) {
@@ -155,7 +149,8 @@ export const updateApprovalBubbleUI = (
155
149
  */
156
150
  export const createApprovalBubble = (
157
151
  message: AgentWidgetMessage,
158
- config?: AgentWidgetConfig
152
+ config?: AgentWidgetConfig,
153
+ expansionState = new Map<string, boolean>()
159
154
  ): HTMLElement => {
160
155
  const approval = message.approval;
161
156
  const approvalConfig = config?.approval !== false ? config?.approval : undefined;
@@ -178,12 +173,12 @@ export const createApprovalBubble = (
178
173
  bubble.setAttribute("data-message-id", message.id);
179
174
 
180
175
  // Apply styling: use semantic tokens with config overrides
181
- bubble.style.backgroundColor = approvalConfig?.backgroundColor ?? "var(--persona-approval-bg, #fefce8)";
182
- bubble.style.borderColor = approvalConfig?.borderColor ?? "var(--persona-approval-border, #fef08a)";
176
+ bubble.style.backgroundColor = approvalConfig?.backgroundColor ?? "var(--persona-approval-bg)";
177
+ bubble.style.borderColor = approvalConfig?.borderColor ?? "var(--persona-approval-border)";
183
178
  bubble.style.boxShadow =
184
179
  approvalConfig?.shadow !== undefined
185
180
  ? (approvalConfig.shadow.trim() === "" ? "none" : approvalConfig.shadow)
186
- : "var(--persona-approval-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))";
181
+ : "var(--persona-approval-shadow)";
187
182
 
188
183
  if (!approval) {
189
184
  return bubble;
@@ -201,9 +196,9 @@ export const createApprovalBubble = (
201
196
  const iconData = approval.status === "denied" ? ShieldX
202
197
  : approval.status === "timeout" ? ShieldAlert
203
198
  : ShieldCheck;
204
- const iconColor = approval.status === "approved" ? "var(--persona-feedback-success, #16a34a)"
205
- : approval.status === "denied" ? "var(--persona-feedback-error, #dc2626)"
206
- : approval.status === "timeout" ? "var(--persona-feedback-warning, #ca8a04)"
199
+ const iconColor = approval.status === "approved" ? "var(--persona-feedback-success, var(--persona-palette-colors-success-600))"
200
+ : approval.status === "denied" ? "var(--persona-feedback-error, var(--persona-palette-colors-error-600))"
201
+ : approval.status === "timeout" ? "var(--persona-feedback-warning, var(--persona-palette-colors-warning-600))"
207
202
  : (approvalConfig?.titleColor ?? "currentColor");
208
203
  const icon = renderIconNode(iconData, 20, iconColor, 2);
209
204
  if (icon) {
@@ -227,16 +222,16 @@ export const createApprovalBubble = (
227
222
  const badge = createElement("span", "persona-inline-flex persona-items-center persona-px-2 persona-py-0.5 persona-rounded-full persona-text-xs persona-font-medium");
228
223
  badge.setAttribute("data-approval-status", approval.status);
229
224
  if (approval.status === "approved") {
230
- badge.style.backgroundColor = "var(--persona-palette-colors-success-100, #dcfce7)";
231
- badge.style.color = "var(--persona-palette-colors-success-700, #15803d)";
225
+ badge.style.backgroundColor = "var(--persona-palette-colors-success-100)";
226
+ badge.style.color = "var(--persona-palette-colors-success-700)";
232
227
  badge.textContent = "Approved";
233
228
  } else if (approval.status === "denied") {
234
- badge.style.backgroundColor = "var(--persona-palette-colors-error-100, #fee2e2)";
235
- badge.style.color = "var(--persona-palette-colors-error-700, #b91c1c)";
229
+ badge.style.backgroundColor = "var(--persona-palette-colors-error-100)";
230
+ badge.style.color = "var(--persona-palette-colors-error-700)";
236
231
  badge.textContent = "Denied";
237
232
  } else if (approval.status === "timeout") {
238
- badge.style.backgroundColor = "var(--persona-palette-colors-warning-100, #fef3c7)";
239
- badge.style.color = "var(--persona-palette-colors-warning-700, #b45309)";
233
+ badge.style.backgroundColor = "var(--persona-palette-colors-warning-100)";
234
+ badge.style.color = "var(--persona-palette-colors-warning-700)";
240
235
  badge.textContent = "Timeout";
241
236
  }
242
237
  titleRow.appendChild(badge);
@@ -303,7 +298,7 @@ export const createApprovalBubble = (
303
298
  const showDescriptionInDetails = Boolean(approval.description) && !summaryFallsBackToDescription;
304
299
  const hasDetails = showDescriptionInDetails || Boolean(approval.parameters);
305
300
  if (detailsMode !== "hidden" && hasDetails) {
306
- const expanded = isDetailsExpanded(message.id, config);
301
+ const expanded = isDetailsExpanded(message.id, expansionState, config);
307
302
 
308
303
  const toggle = createElement(
309
304
  "button",
@@ -364,10 +359,10 @@ export const createApprovalBubble = (
364
359
  // Approve button
365
360
  const approveBtn = createElement("button", "persona-inline-flex persona-items-center persona-px-3 persona-py-1.5 persona-rounded-md persona-text-xs persona-font-medium persona-border-none persona-cursor-pointer") as HTMLButtonElement;
366
361
  approveBtn.type = "button";
367
- approveBtn.style.backgroundColor = approvalConfig?.approveButtonColor ?? "var(--persona-approval-approve-bg, #22c55e)";
368
- approveBtn.style.color = approvalConfig?.approveButtonTextColor ?? "#ffffff";
362
+ approveBtn.style.backgroundColor = approvalConfig?.approveButtonColor ?? "var(--persona-approval-approve-bg)";
363
+ approveBtn.style.color = approvalConfig?.approveButtonTextColor ?? "var(--persona-button-primary-fg)";
369
364
  approveBtn.setAttribute("data-approval-action", "approve");
370
- const approveIcon = renderIconNode(ShieldCheck, 14, approvalConfig?.approveButtonTextColor ?? "#ffffff", 2);
365
+ const approveIcon = renderIconNode(ShieldCheck, 14, approvalConfig?.approveButtonTextColor ?? "var(--persona-button-primary-fg)", 2);
371
366
  if (approveIcon) {
372
367
  approveIcon.style.marginRight = "4px";
373
368
  approveBtn.appendChild(approveIcon);
@@ -379,10 +374,10 @@ export const createApprovalBubble = (
379
374
  const denyBtn = createElement("button", "persona-inline-flex persona-items-center persona-px-3 persona-py-1.5 persona-rounded-md persona-text-xs persona-font-medium persona-cursor-pointer") as HTMLButtonElement;
380
375
  denyBtn.type = "button";
381
376
  denyBtn.style.backgroundColor = approvalConfig?.denyButtonColor ?? "transparent";
382
- denyBtn.style.color = approvalConfig?.denyButtonTextColor ?? "var(--persona-feedback-error, #dc2626)";
383
- denyBtn.style.border = `1px solid ${approvalConfig?.denyButtonTextColor ? approvalConfig.denyButtonTextColor : "var(--persona-palette-colors-error-200, #fca5a5)"}`;
377
+ denyBtn.style.color = approvalConfig?.denyButtonTextColor ?? "var(--persona-feedback-error, var(--persona-palette-colors-error-600))";
378
+ denyBtn.style.border = `1px solid ${approvalConfig?.denyButtonTextColor ? approvalConfig.denyButtonTextColor : "var(--persona-palette-colors-error-200)"}`;
384
379
  denyBtn.setAttribute("data-approval-action", "deny");
385
- const denyIcon = renderIconNode(ShieldX, 14, approvalConfig?.denyButtonTextColor ?? "var(--persona-feedback-error, #dc2626)", 2);
380
+ const denyIcon = renderIconNode(ShieldX, 14, approvalConfig?.denyButtonTextColor ?? "var(--persona-feedback-error, var(--persona-palette-colors-error-600))", 2);
386
381
  if (denyIcon) {
387
382
  denyIcon.style.marginRight = "4px";
388
383
  denyBtn.appendChild(denyIcon);
@@ -75,7 +75,6 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
75
75
  className:
76
76
  "persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-persona-border persona-bg-persona-input-background persona-px-4 persona-py-3",
77
77
  attrs: { "data-persona-composer-form": "" },
78
- style: { outline: "none" },
79
78
  });
80
79
 
81
80
  const { textarea, attachAutoResize } = createComposerTextarea(config);
@@ -144,7 +143,11 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
144
143
  }
145
144
  });
146
145
 
147
- footer.append(suggestions, composerForm, statusText);
146
+ if (config?.statusIndicator?.mode === "transient") {
147
+ footer.append(suggestions, statusText, composerForm);
148
+ } else {
149
+ footer.append(suggestions, composerForm, statusText);
150
+ }
148
151
 
149
152
  return {
150
153
  footer,
@@ -1,3 +1,4 @@
1
+ import { applyStatusIndicatorState } from "../utils/status-indicator";
1
2
  import { createElement, createNode, cx } from "../utils/dom";
2
3
  import { renderLucideIcon } from "../utils/icons";
3
4
  import { AgentWidgetConfig } from "../types";
@@ -293,6 +294,14 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
293
294
  sendIcon = renderLucideIcon(iconName, iconSize, iconColor, iconStroke);
294
295
  }
295
296
  stopIcon = renderLucideIcon(stopIconName, iconSize, iconColor, iconStroke);
297
+ // Keep explicit per-control sizing authoritative; otherwise use the theme.
298
+ if (!sendButtonConfig.iconSize) {
299
+ for (const icon of [sendIcon, stopIcon]) {
300
+ if (!icon) continue;
301
+ icon.style.width = `var(--persona-components-composer-sendIconSize, ${iconSize}px)`;
302
+ icon.style.height = `var(--persona-components-composer-sendIconSize, ${iconSize}px)`;
303
+ }
304
+ }
296
305
 
297
306
  if (sendIcon && stopIcon) {
298
307
  // One grid cell, both glyphs in it. `data-mode` on the stack decides
@@ -538,8 +547,7 @@ export const createStatusText = (config?: AgentWidgetConfig): HTMLElement => {
538
547
  );
539
548
  statusText.setAttribute("data-persona-composer-status", "");
540
549
 
541
- const isVisible = statusConfig.visible ?? true;
542
- statusText.style.display = isVisible ? "" : "none";
550
+ applyStatusIndicatorState(statusText, statusConfig, "idle");
543
551
  const idleLabel = statusConfig.idleText ?? "Online";
544
552
  if (statusConfig.idleLink) {
545
553
  const link = createElement("a") as HTMLAnchorElement;
@@ -1,9 +1,41 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { describe, expect, it } from "vitest";
4
+ import {
5
+ DEFAULT_HEADER_ICON_SIZE,
6
+ DEFAULT_LAUNCHER_CONFIG,
7
+ } from "../defaults";
4
8
  import { buildHeader } from "./header-builder";
5
9
 
6
10
  describe("buildHeader tooltips", () => {
11
+ it.each([{ v5Defaults: false }, { v5Defaults: true }])(
12
+ "uses the standard padding token under v5Defaults=%s",
13
+ (future) => {
14
+ const { header } = buildHeader({ config: { future } as any });
15
+ expect(header.style.padding).toBe(
16
+ "var(--persona-components-header-padding, 20px 24px)"
17
+ );
18
+ }
19
+ );
20
+ it.each([false, true])("preserves legacy numeric glyph sizing for non-pixel icon boxes (v5=%s)", (v5Defaults) => {
21
+ const { iconHolder } = buildHeader({ config: {
22
+ apiUrl: "/dispatch", future: { v5Defaults },
23
+ launcher: { headerIconSize: "2rem", headerIconName: "bot" },
24
+ } });
25
+ const glyph = iconHolder.querySelector("svg")!;
26
+ expect(iconHolder.style.width).toBe("2rem");
27
+ expect(glyph.getAttribute("width")).toBe("1.2");
28
+ expect(glyph.style.width).toBe("calc(2px * var(--persona-components-header-iconScale, 0.6))");
29
+ });
30
+
31
+ it("uses the shared launcher icon default", () => {
32
+ const { iconHolder } = buildHeader({});
33
+
34
+ expect(DEFAULT_LAUNCHER_CONFIG.headerIconSize).toBe(DEFAULT_HEADER_ICON_SIZE);
35
+ expect(iconHolder.style.height).toBe(DEFAULT_HEADER_ICON_SIZE);
36
+ expect(iconHolder.style.width).toBe(DEFAULT_HEADER_ICON_SIZE);
37
+ });
38
+
7
39
  it("portals the close-button tooltip into the mounted document", () => {
8
40
  const iframeDocument = document.implementation.createHTMLDocument("preview");
9
41
  const { header, closeButton, closeButtonWrapper } = buildHeader({
@@ -1,6 +1,7 @@
1
1
  import { createElement, createNode } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
3
  import { AgentWidgetConfig } from "../types";
4
+ import { DEFAULT_HEADER_ICON_SIZE } from "../defaults";
4
5
  import { createCloseButton, createClearChatButton } from "./header-parts";
5
6
 
6
7
  /** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
@@ -10,7 +11,7 @@ export const HEADER_THEME_CSS = {
10
11
  subtitleColor:
11
12
  "var(--persona-header-subtitle-fg, var(--persona-text-muted, var(--persona-muted, #9ca3af)))",
12
13
  actionIconColor:
13
- "var(--persona-header-action-icon-fg, var(--persona-muted, #9ca3af))",
14
+ "var(--persona-header-control-state-fg, var(--persona-header-action-icon-fg, var(--persona-muted, #9ca3af)))",
14
15
  /** One knob for both header builders; the Messages rail header shares it. */
15
16
  minHeight: "var(--persona-header-min-height, auto)",
16
17
  } as const;
@@ -64,9 +65,10 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
64
65
 
65
66
  const header = createNode("div", {
66
67
  className:
67
- "persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5",
68
+ "persona-widget-header persona-flex persona-items-center persona-gap-3",
68
69
  attrs: { "data-persona-theme-zone": "header" },
69
70
  style: {
71
+ padding: "var(--persona-components-header-padding, 20px 24px)",
70
72
  minHeight: HEADER_THEME_CSS.minHeight,
71
73
  backgroundColor: "var(--persona-header-bg, var(--persona-surface, #ffffff))",
72
74
  borderBottomColor: "var(--persona-header-border, var(--persona-divider, #f1f5f9))",
@@ -77,7 +79,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
77
79
  });
78
80
 
79
81
  const launcher = config?.launcher ?? {};
80
- const headerIconSize = launcher.headerIconSize ?? "48px";
82
+ const headerIconSize = launcher.headerIconSize ?? DEFAULT_HEADER_ICON_SIZE;
81
83
  const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
82
84
  const headerIconHidden = launcher.headerIconHidden ?? false;
83
85
  const headerIconName = launcher.headerIconName;
@@ -100,6 +102,8 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
100
102
  const iconSize = parseFloat(headerIconSize) || 24;
101
103
  const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "currentColor", 1);
102
104
  if (iconSvg) {
105
+ iconSvg.style.width = `calc(${iconSize}px * var(--persona-components-header-iconScale, 0.6))`;
106
+ iconSvg.style.height = `calc(${iconSize}px * var(--persona-components-header-iconScale, 0.6))`;
103
107
  iconHolder.replaceChildren(iconSvg);
104
108
  } else {
105
109
  // Fallback to agentIconText if Lucide icon fails
@@ -238,4 +242,3 @@ export const attachHeaderToContainer = (
238
242
  container.appendChild(headerElements.clearChatButtonWrapper);
239
243
  }
240
244
  };
241
-
@@ -16,6 +16,18 @@ const build = (layoutHeaderConfig: Parameters<typeof buildMinimalHeader>[0]["lay
16
16
  });
17
17
 
18
18
  describe("minimal header trailing actions", () => {
19
+ it.each([{ v5Defaults: false }, { v5Defaults: true }])(
20
+ "uses the minimal padding token under v5Defaults=%s",
21
+ (future) => {
22
+ const elements = buildMinimalHeader({
23
+ config: { apiUrl: "https://example.com/api", future } as AgentWidgetConfig,
24
+ });
25
+ expect(elements.header.style.padding).toBe(
26
+ "var(--persona-components-header-minimalPadding, var(--persona-components-header-padding, 16px 24px))"
27
+ );
28
+ }
29
+ );
30
+
19
31
  it("clusters actions at the trailing edge with the close button's chrome", () => {
20
32
  const onAction = vi.fn();
21
33
  const elements = build({
@@ -122,10 +122,12 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
122
122
  // max(title 24px, cluster 32px) + py-4 + border = the strip's 65px.
123
123
  const header = createElement(
124
124
  "div",
125
- "persona-flex persona-items-center persona-justify-between persona-px-6 persona-py-4"
125
+ "persona-flex persona-items-center persona-justify-between"
126
126
  );
127
127
  header.setAttribute("data-persona-theme-zone", "header");
128
128
  header.style.minHeight = HEADER_THEME_CSS.minHeight;
129
+ header.style.padding =
130
+ "var(--persona-components-header-minimalPadding, var(--persona-components-header-padding, 16px 24px))";
129
131
  header.style.backgroundColor = 'var(--persona-header-bg, var(--persona-surface, #ffffff))';
130
132
  header.style.borderBottomColor = 'var(--persona-header-border, var(--persona-divider, #f1f5f9))';
131
133
  header.style.boxShadow = 'var(--persona-header-shadow, none)';
@@ -393,4 +395,3 @@ export const buildHeaderWithLayout = (
393
395
 
394
396
  return headerElements;
395
397
  };
396
-
@@ -1,3 +1,4 @@
1
+ import { resolveLauncherVariant } from "../utils/launcher-variant";
1
2
  import { createElement, createNode, cx } from "../utils/dom";
2
3
  import { AgentWidgetConfig, AgentWidgetLauncherTeaserConfig } from "../types";
3
4
  import { positionMap } from "../utils/positioning";
@@ -31,6 +32,14 @@ export const createLauncherButton = (
31
32
  const update = (newConfig: AgentWidgetConfig) => {
32
33
  const launcher = newConfig.launcher ?? {};
33
34
  const dockedMode = isDockedMountMode(newConfig);
35
+ const circle = resolveLauncherVariant(newConfig) === "circle";
36
+ if (circle) {
37
+ button.dataset.personaLauncherVariant = "circle";
38
+ button.setAttribute("aria-label", launcher.title ?? "Open chat");
39
+ } else {
40
+ button.removeAttribute("data-persona-launcher-variant");
41
+ button.removeAttribute("aria-label");
42
+ }
34
43
 
35
44
  const titleEl = button.querySelector("[data-role='launcher-title']");
36
45
  if (titleEl) {
@@ -49,7 +58,7 @@ export const createLauncherButton = (
49
58
  // Hide/show text container
50
59
  const textContainer = button.querySelector(".persona-flex-col");
51
60
  if (textContainer) {
52
- if (launcher.textHidden || dockedMode) {
61
+ if (launcher.textHidden || dockedMode || circle) {
53
62
  (textContainer as HTMLElement).style.display = "none";
54
63
  } else {
55
64
  (textContainer as HTMLElement).style.display = "";
@@ -153,7 +162,7 @@ export const createLauncherButton = (
153
162
  callToActionIconEl.style.padding = "";
154
163
  }
155
164
 
156
- if (launcher.callToActionIconHidden) {
165
+ if (launcher.callToActionIconHidden || circle) {
157
166
  callToActionIconEl.style.display = "none";
158
167
  } else {
159
168
  callToActionIconEl.style.display = dockedMode ? "none" : "";
@@ -359,6 +359,20 @@ describe("createStandardBubble: components.message geometry tokens", () => {
359
359
  });
360
360
  });
361
361
 
362
+ it.each([false, true])("consumes selected default geometry with v5Defaults=%s", (v5Defaults) => {
363
+ for (const role of ["user", "assistant"] as const) {
364
+ const bubble = createStandardBubble(
365
+ makeMessage({ id: `default-${role}`, role, content: "Hi" }),
366
+ ({ text }) => text, undefined, undefined, undefined,
367
+ { widgetConfig: { future: { v5Defaults } } as AgentWidgetConfig }
368
+ );
369
+ expect(bubble.style.padding).toBe(v5Defaults
370
+ ? `var(--persona-message-${role}-padding, 0.75rem 1.25rem)` : "");
371
+ expect(bubble.style.lineHeight).toBe(v5Defaults
372
+ ? `var(--persona-message-${role}-line-height, 1.75)` : "");
373
+ }
374
+ });
375
+
362
376
  it("routes each configured property through its role token with the bubble preset as fallback", () => {
363
377
  const bubble = createStandardBubble(
364
378
  makeMessage({ id: "geo-user", role: "user", content: "Hi" }),
@@ -896,6 +910,14 @@ describe("createMessageActions: chromeless action buttons", () => {
896
910
  const actionButtons = (row: HTMLElement) =>
897
911
  Array.from(row.querySelectorAll<HTMLButtonElement>("button[data-action]"));
898
912
 
913
+ it("keeps voting controls hidden unless explicitly enabled", () => {
914
+ const row = createMessageActions(makeMessage({ id: "actions-default-votes" }), {});
915
+
916
+ expect(actionButtons(row).map((btn) => btn.getAttribute("data-action"))).toEqual([
917
+ "copy",
918
+ ]);
919
+ });
920
+
899
921
  it("styles every built-in action through the message-action class alone", () => {
900
922
  const row = createMessageActions(makeMessage({ id: "actions-builtin" }), {
901
923
  showCopy: true,
@@ -18,6 +18,7 @@ import {
18
18
  StopReasonKind
19
19
  } from "../types";
20
20
  import type { MessageGeometryTokens } from "../types/theme";
21
+ import { resolveThemeDefaults } from "../utils/tokens";
21
22
  import { createMentionTokenElement } from "../utils/mention-token";
22
23
  import { createIconButton } from "../utils/buttons";
23
24
  import { renderLucideIcon } from "../utils/icons";
@@ -820,8 +821,8 @@ export const createMessageActions = (
820
821
  eligibility?: MessageActionEligibility
821
822
  ): HTMLElement => {
822
823
  const showCopy = actionsConfig.showCopy ?? true;
823
- const showUpvote = actionsConfig.showUpvote ?? true;
824
- const showDownvote = actionsConfig.showDownvote ?? true;
824
+ const showUpvote = actionsConfig.showUpvote ?? false;
825
+ const showDownvote = actionsConfig.showDownvote ?? false;
825
826
  const showReadAloud = actionsConfig.showReadAloud ?? false;
826
827
  // Retry and edit are per-message: the config flag only opts in, the caller's
827
828
  // eligibility decides which single message actually shows the control.
@@ -1002,12 +1003,15 @@ export const createStandardBubble = (
1002
1003
 
1003
1004
  bubble.setAttribute("data-persona-theme-zone", message.role === "user" ? "user-message" : "assistant-message");
1004
1005
 
1006
+ const defaultMessageTokens = resolveThemeDefaults(options?.widgetConfig?.future).components.message;
1005
1007
  const messageTokens = options?.widgetConfig?.theme?.components?.message;
1006
1008
  applyBubbleGeometry(
1007
1009
  bubble,
1008
1010
  message.role,
1009
1011
  layout,
1010
- message.role === "user" ? messageTokens?.user : messageTokens?.assistant
1012
+ message.role === "user"
1013
+ ? { ...defaultMessageTokens.user, ...messageTokens?.user, ...options?.widgetConfig?.darkTheme?.components?.message?.user }
1014
+ : { ...defaultMessageTokens.assistant, ...messageTokens?.assistant, ...options?.widgetConfig?.darkTheme?.components?.message?.assistant }
1011
1015
  );
1012
1016
 
1013
1017
  // Apply component-level color overrides via CSS variables