@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
package/src/ui.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { activityVariant, activityDuration, createActivityLifecycle, createActivityGroup } from "./components/activity-row";
2
+ import { applyStatusIndicatorState, placeStatusIndicator } from "./utils/status-indicator";
1
3
  import { usesSessionVoice } from "./utils/voice-support";
2
4
  import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
3
5
  import { resolveSanitizer } from "./utils/sanitize";
@@ -13,6 +15,8 @@ import {
13
15
  import {
14
16
  AgentWidgetConfig,
15
17
  AgentWidgetConfigPatch,
18
+ AgentWidgetSessionInitHook,
19
+ AgentWidgetVoicePrewarmHook,
16
20
  AgentWidgetApprovalDecisionOptions,
17
21
  AgentWidgetMessage,
18
22
  AgentWidgetEvent,
@@ -147,8 +151,9 @@ import {
147
151
  } from "./utils/context-mention-orchestrator";
148
152
  import type { MentionSubmitBundle } from "./utils/context-mention-manager";
149
153
  import { createTextPart, ALL_SUPPORTED_MIME_TYPES } from "./utils/content";
150
- import { applyThemeVariables, createThemeObserver, getActiveTheme } from "./utils/theme";
154
+ import { applyThemeVariables, createThemeObserver, getActiveTheme, getColorScheme } from "./utils/theme";
151
155
  import { resolveTokenValue } from "./utils/tokens";
156
+ import { deepMerge } from "./utils/deep-merge";
152
157
  import { Activity, Check, Copy } from "lucide";
153
158
  import { renderLucideIcon, onExtraIconsReady } from "./utils/icons";
154
159
  import { renderIconNode } from "./utils/icon-node";
@@ -222,6 +227,7 @@ import {
222
227
  attachHeaderToContainer,
223
228
  COMPOSER_BAR_CLEAR_CHAT_ICON_SIZE,
224
229
  COMPOSER_BAR_CLOSE_ICON_SIZE,
230
+ resolvePanelGeometry,
225
231
  } from "./components/panel";
226
232
  import { buildPillComposer } from "./components/pill-composer-builder";
227
233
  import {
@@ -263,8 +269,9 @@ import {
263
269
  getBubbleClasses,
264
270
  CUSTOM_MESSAGE_ACTION_PREFIX,
265
271
  } from "./components/message-bubble";
266
- import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
267
- import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
272
+ import { createReasoningBubble, updateReasoningBubbleUI } from "./components/reasoning-bubble";
273
+ import { copyToolDetail } from "./components/tool-details";
274
+ import { createToolBubble, updateToolBubbleUI } from "./components/tool-bubble";
268
275
  import {
269
276
  buildStructuredAnswers,
270
277
  ensureAskUserQuestionSheet,
@@ -324,8 +331,9 @@ import {
324
331
  } from "./utils/artifact-resize";
325
332
  import { loadFormsUi, getFormsUiSync } from "./forms-ui-loader";
326
333
  import { pluginRegistry } from "./plugins/registry";
327
- import { mergeWithDefaults, DEFAULT_FLOATING_LAUNCHER_WIDTH } from "./defaults";
334
+ import { DEFAULT_HEADER_ICON_SIZE, mergeWithDefaults } from "./defaults";
328
335
  import { mergeConfigUpdate } from "./utils/config-merge";
336
+ import { getPanelAliasProvenance } from "./utils/panel-config";
329
337
  import { createEventBus } from "./utils/events";
330
338
  import {
331
339
  createActionManager,
@@ -701,6 +709,13 @@ type Controller = {
701
709
  ) => Promise<void>;
702
710
  /** Clean local state plus a new owned server conversation. `focus` as on `openConversation`. */
703
711
  startNewConversation: (opts?: { focus?: boolean }) => Promise<void>;
712
+ /**
713
+ * Client token mode: start `/v1/client/init` now, ahead of the first send
714
+ * (e.g. when the visitor hovers your own "Chat with us" button).
715
+ * Fire-and-forget, at most once per session lifetime, errors swallowed; the
716
+ * send reuses the in-flight init. No-op in other modes. See `sessionInit`.
717
+ */
718
+ warmSession: () => void;
704
719
  /** Permanently delete one server record; the active one is replaced. */
705
720
  deleteConversation: (conversationId: string) => Promise<void>;
706
721
  /**
@@ -930,6 +945,18 @@ export const createAgentExperience = (
930
945
  }
931
946
 
932
947
  let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
948
+ // Message ids are only unique within a widget session. Keep disclosure state
949
+ // beside the widget that renders it, not in component module scope.
950
+ const toolExpansionState = new Set<string>();
951
+ const reasoningExpansionState = new Set<string>();
952
+ const activityLifecycle = createActivityLifecycle((id, kind) => {
953
+ messageCache.delete(id);
954
+ const bubble = Array.from(messagesWrapper.querySelectorAll<HTMLElement>("[data-message-id]")).find(node => node.dataset.messageId === id);
955
+ if (!bubble) return;
956
+ if (kind === "tool") updateToolBubbleUI(id, bubble, toolExpansionState, config);
957
+ else updateReasoningBubbleUI(id, bubble, reasoningExpansionState);
958
+ });
959
+ const approvalDetailsExpansionState = new Map<string, boolean>();
933
960
  const applyTooltipTiming = (): void => {
934
961
  configureTooltipTiming({
935
962
  delayMs: config.tooltip?.delayMs ?? DEFAULT_TOOLTIP_DELAY_MS,
@@ -983,7 +1010,7 @@ export const createAgentExperience = (
983
1010
  mod.initApprovalUi({
984
1011
  webMcpToolTitle: getWebMcpToolDisplayTitle,
985
1012
  });
986
- const built = mod.createBuiltInApprovalPlugin();
1013
+ const built = mod.createBuiltInApprovalPlugin(approvalDetailsExpansionState);
987
1014
  builtInApprovalPlugin = built.plugin;
988
1015
  teardownBuiltInApprovals = built.teardown;
989
1016
  };
@@ -1361,7 +1388,7 @@ export const createAgentExperience = (
1361
1388
 
1362
1389
  // Get status indicator config
1363
1390
  const statusConfig = config.statusIndicator ?? {};
1364
- const _getStatusText = (status: AgentWidgetSessionStatus): string => {
1391
+ const _getStatusText = (status: AgentWidgetSessionStatus, statusConfig = config.statusIndicator ?? {}): string => {
1365
1392
  if (status === "idle") return statusConfig.idleText ?? statusCopy.idle;
1366
1393
  if (status === "connecting") return statusConfig.connectingText ?? statusCopy.connecting;
1367
1394
  if (status === "connected") return statusConfig.connectedText ?? statusCopy.connected;
@@ -1373,6 +1400,10 @@ export const createAgentExperience = (
1373
1400
 
1374
1401
  /** Update statusText element, rendering a link for idle status when idleLink is configured. */
1375
1402
  function applyStatusToElement(el: HTMLElement, text: string, statusCfg: typeof statusConfig, status: string): void {
1403
+ if (!isComposerBar() && (statusCfg.mode === "transient" || el.classList.contains("persona-mb-2"))) {
1404
+ placeStatusIndicator(el, composerForm, statusCfg);
1405
+ applyStatusIndicatorState(el, statusCfg, status);
1406
+ }
1376
1407
  // A composer lock reason owns the status region until the lock clears.
1377
1408
  if (el.hasAttribute(COMPOSER_REASON_ATTR)) return;
1378
1409
  if (status === "idle" && statusCfg.idleLink) {
@@ -1544,6 +1575,16 @@ export const createAgentExperience = (
1544
1575
  }, 400);
1545
1576
  };
1546
1577
 
1578
+ const isWelcomeFullscreen = () => {
1579
+ const win = mount.ownerDocument.defaultView ?? window;
1580
+ const mobile = (config.launcher?.mobileFullscreen ?? true) &&
1581
+ win.innerWidth <= (config.launcher?.mobileBreakpoint ?? 640) &&
1582
+ (launcherEnabled || isDockedMountMode(config));
1583
+ return mobile || (config.launcher?.fullHeight === true &&
1584
+ !config.launcher.sidebarMode && !isDockedMountMode(config));
1585
+ };
1586
+ const getWelcomeConfig = () => resolveWelcomeConfig(config, isWelcomeFullscreen());
1587
+
1547
1588
  // Derived from the session's user messages on every welcome render; the
1548
1589
  // public `data-persona-conversation-state` contract mirrors it.
1549
1590
  let conversationState: "empty" | "active" = "empty";
@@ -1578,7 +1619,11 @@ export const createAgentExperience = (
1578
1619
  // updateScrollToBottomButtonOffset reads them, never writes.
1579
1620
  const syncComposerOverlayMetrics = () => {
1580
1621
  const placement = resolveComposerPlacement(config, isComposerBar());
1581
- const resolved = resolveWelcomeConfig(config);
1622
+ const resolved = getWelcomeConfig();
1623
+ mount.setAttribute("data-persona-welcome-fullscreen", String(isWelcomeFullscreen()));
1624
+ mount.setAttribute("data-persona-welcome-layout", resolved.layout ?? "top");
1625
+ mount.setAttribute("data-persona-welcome-layout-explicit", config.welcome?.layout ?? "");
1626
+ mount.setAttribute("data-persona-welcome-anchor", resolved.anchor ?? "bottom");
1582
1627
  const footerHidden = footer.style.display === "none";
1583
1628
  const footerHeight = footerHidden ? 0 : footer.offsetHeight;
1584
1629
 
@@ -1587,10 +1632,25 @@ export const createAgentExperience = (
1587
1632
  ? parseAnchorFraction(resolved.anchorComposerTop) ??
1588
1633
  parseAnchorFraction(DEFAULT_ANCHOR_COMPOSER_TOP)!
1589
1634
  : null;
1635
+ // The implicit fullscreen layout centers the complete greeting/composer
1636
+ // group. Explicit anchoring retains its percentage-based geometry.
1637
+ const centerGroup = fraction !== null && resolved.layout === "centered" &&
1638
+ config.welcome?.anchor === undefined &&
1639
+ config.welcome?.anchorComposerTop === undefined;
1640
+ const measuredBodyStyle = centerGroup ? getComputedStyle(body) : null;
1641
+ const previousOverlayHeight = parseFloat(mount.style.getPropertyValue("--persona-composer-overlay-height")) || 0;
1642
+ const previousOverlayLift = parseFloat(mount.style.getPropertyValue("--persona-composer-lift")) || 0;
1643
+ const groupGap = Math.max(0, (parseFloat(measuredBodyStyle?.paddingBottom ?? "") || 0) -
1644
+ (placement === "overlay" ? previousOverlayHeight + previousOverlayLift : 0));
1645
+ const columnTop = centerGroup ? body.getBoundingClientRect().top - container.getBoundingClientRect().top : 0;
1646
+ const greetingHeight = !centerGroup || welcomeHost.hidden ? 0 :
1647
+ (welcomeHost.querySelector<HTMLElement>("[data-persona-welcome-plugin]") ?? welcomeHost).offsetHeight;
1590
1648
  const lift =
1591
1649
  fraction === null || isComposerBar()
1592
1650
  ? 0
1593
- : computeComposerLift({
1651
+ : centerGroup
1652
+ ? Math.max(0, Math.round((container.clientHeight - columnTop - greetingHeight - groupGap - footerHeight) / 2))
1653
+ : computeComposerLift({
1594
1654
  columnHeight: container.clientHeight,
1595
1655
  footerHeight,
1596
1656
  fraction,
@@ -2506,9 +2566,6 @@ export const createAgentExperience = (
2506
2566
  pruneComposerModes(state.activeModeIds, composerChipModes()).length > 0
2507
2567
  );
2508
2568
  };
2509
- // Bound once `voiceState` exists (further down this closure); the compact
2510
- // sweep can run before then only via a microtask, which is later still.
2511
- let isDictationActive: () => boolean = () => false;
2512
2569
  function syncComposerCompact(): void {
2513
2570
  const state = composerStore.getState();
2514
2571
  const wrapped = composerCompactLatch.observe(
@@ -2524,7 +2581,6 @@ export const createAgentExperience = (
2524
2581
  hasChips: hasComposerChips(),
2525
2582
  hasQuote: state.quote !== undefined,
2526
2583
  hasPendingSubmission: state.pendingSubmission !== undefined,
2527
- dictationActive: isDictationActive(),
2528
2584
  })
2529
2585
  );
2530
2586
  }
@@ -2667,6 +2723,16 @@ export const createAgentExperience = (
2667
2723
  // Render custom slots
2668
2724
  renderSlots();
2669
2725
 
2726
+ messagesWrapper.addEventListener("click", (event) => {
2727
+ const target = event.target;
2728
+ if (!(target instanceof Element)) return;
2729
+ const button = target.closest<HTMLButtonElement>("button[data-persona-copy-tool-detail]");
2730
+ if (!button) return;
2731
+ event.preventDefault();
2732
+ event.stopPropagation();
2733
+ void copyToolDetail(button);
2734
+ });
2735
+
2670
2736
  // Add event delegation for reasoning and tool bubble expansion
2671
2737
  // This handles clicks even after idiomorph morphs the DOM
2672
2738
  const handleBubbleExpansion = (event: Event) => {
@@ -2684,7 +2750,17 @@ export const createAgentExperience = (
2684
2750
  const messageId = bubble.getAttribute('data-message-id');
2685
2751
  if (!messageId) return;
2686
2752
 
2753
+ // Disclosure is reading intent, not new streamed content. Pause before the
2754
+ // DOM grows so the resize observer cannot pull the opened details away.
2755
+ markReaderEngaged();
2756
+ pauseAutoScroll();
2687
2757
  const bubbleType = headerButton.getAttribute('data-bubble-type');
2758
+ if (bubble.classList.contains("persona-activity-row")) {
2759
+ activityLifecycle.manual(messageId);
2760
+ const group = bubble.parentElement?.closest<HTMLElement>("[data-persona-tool-group]");
2761
+ if (group?.dataset.messageId) activityLifecycle.manual(group.dataset.messageId);
2762
+ }
2763
+
2688
2764
 
2689
2765
  // Toggle expansion state
2690
2766
  if (bubbleType === 'reasoning') {
@@ -2693,22 +2769,22 @@ export const createAgentExperience = (
2693
2769
  } else {
2694
2770
  reasoningExpansionState.add(messageId);
2695
2771
  }
2696
- updateReasoningBubbleUI(messageId, bubble);
2772
+ updateReasoningBubbleUI(messageId, bubble, reasoningExpansionState);
2697
2773
  } else if (bubbleType === 'tool') {
2698
2774
  if (toolExpansionState.has(messageId)) {
2699
2775
  toolExpansionState.delete(messageId);
2700
2776
  } else {
2701
2777
  toolExpansionState.add(messageId);
2702
2778
  }
2703
- updateToolBubbleUI(messageId, bubble, config);
2779
+ updateToolBubbleUI(messageId, bubble, toolExpansionState, config);
2704
2780
  } else if (bubbleType === 'approval' && approvalUi) {
2705
2781
  // approvalUi is always set here: approval bubbles only exist after the
2706
2782
  // chunk was adopted. The guard keeps a stray click on a stub harmless.
2707
2783
  const approvalConfig = config.approval !== false ? config.approval : undefined;
2708
2784
  const defaultExpanded = (approvalConfig?.detailsDisplay ?? 'collapsed') === 'expanded';
2709
- const expanded = approvalUi.approvalDetailsExpansionState.get(messageId) ?? defaultExpanded;
2710
- approvalUi.approvalDetailsExpansionState.set(messageId, !expanded);
2711
- approvalUi.updateApprovalDetailsUI(messageId, bubble, config);
2785
+ const expanded = approvalDetailsExpansionState.get(messageId) ?? defaultExpanded;
2786
+ approvalDetailsExpansionState.set(messageId, !expanded);
2787
+ approvalUi.updateApprovalDetailsUI(messageId, bubble, config, approvalDetailsExpansionState);
2712
2788
  }
2713
2789
  // Invalidate cached wrapper so next render rebuilds with current expansion state
2714
2790
  messageCache.delete(messageId);
@@ -2723,6 +2799,11 @@ export const createAgentExperience = (
2723
2799
  }
2724
2800
  });
2725
2801
 
2802
+ messagesWrapper.addEventListener('click', (event) => {
2803
+ // Assistive technologies activate buttons with a click rather than a pointer.
2804
+ if (event.detail === 0 && (event.target as HTMLElement).closest('.persona-activity-row')) handleBubbleExpansion(event);
2805
+ });
2806
+
2726
2807
  messagesWrapper.addEventListener('keydown', (event) => {
2727
2808
  const target = event.target as HTMLElement;
2728
2809
  if ((event.key === 'Enter' || event.key === ' ') && target.closest('button[data-expand-header="true"]')) {
@@ -3984,7 +4065,7 @@ export const createAgentExperience = (
3984
4065
  if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
3985
4066
  if (!shouldExpandLauncherForArtifacts(config, launcherEnabled)) return;
3986
4067
 
3987
- const base = config.launcher?.width ?? config.launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
4068
+ const base = resolvePanelGeometry(config).width;
3988
4069
  const expanded =
3989
4070
  config.features?.artifacts?.layout?.expandedPanelWidth ??
3990
4071
  "min(720px, calc(100vw - 24px))";
@@ -4035,6 +4116,13 @@ export const createAgentExperience = (
4035
4116
  // Apply full-height and sidebar styles if enabled
4036
4117
  // This ensures the widget fills its container height with proper flex layout
4037
4118
  const applyFullHeightStyles = () => {
4119
+ const fullScreenTranscript = isMobileFullscreenActive() ||
4120
+ (isComposerBar() && config.launcher?.composerBar?.expandedSize === "fullscreen") ||
4121
+ (config.launcher?.fullHeight === true && !config.launcher?.sidebarMode && !isDockedMountMode(config));
4122
+ messagesWrapper.style.gap = fullScreenTranscript
4123
+ ? "var(--persona-components-message-fullscreenGap, var(--persona-components-message-gap, 12px))"
4124
+ : "var(--persona-components-message-gap, 12px)";
4125
+ messagesWrapper.style.setProperty("--persona-transcript-gap", messagesWrapper.style.gap);
4038
4126
  // Composer-bar mode owns its own sizing/chrome. Geometry comes from
4039
4127
  // `applyComposerBarGeometry()` (per-state inline on the wrapper), the
4040
4128
  // pill carries its own chrome via `.persona-pill-composer`, and the
@@ -4083,7 +4171,14 @@ export const createAgentExperience = (
4083
4171
  const isInlineEmbed = config.launcher?.enabled === false;
4084
4172
  /** Detached appearance: inset card over a canvas instead of flush chrome. */
4085
4173
  const isDetached = config.launcher?.detachedPanel === true;
4086
- const panelPartial = config.theme?.components?.panel;
4174
+ const lightPanelPartial = config.theme?.components?.panel;
4175
+ const darkPanelPartial = config.darkTheme?.components?.panel;
4176
+ const panelPartial: typeof lightPanelPartial = getColorScheme(config) === "dark"
4177
+ ? deepMerge(
4178
+ (lightPanelPartial ?? {}) as Record<string, unknown>,
4179
+ (darkPanelPartial ?? {}) as Record<string, unknown>
4180
+ ) as typeof lightPanelPartial
4181
+ : lightPanelPartial;
4087
4182
  const activeTheme = getActiveTheme(config);
4088
4183
  const resolvePanelChrome = (raw: string | undefined, fallback: string): string => {
4089
4184
  if (raw == null || raw === "") return fallback;
@@ -4110,11 +4205,38 @@ export const createAgentExperience = (
4110
4205
  // Card chrome defaults (floating look): reused to restore detached chrome.
4111
4206
  // Defaults chain through the aliases themeToCssVariables emits so explicit
4112
4207
  // theme.components.panel overrides and these defaults never diverge.
4113
- const cardBorder = 'var(--persona-panel-border, 1px solid var(--persona-border))';
4114
- const cardShadow = 'var(--persona-panel-shadow, var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25)))';
4115
- const cardRadius = 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))';
4208
+ const modeChrome = (
4209
+ mode: "floating" | "inline" | "sidebar" | "docked" | "mobile",
4210
+ field: "border" | "shadow" | "borderRadius",
4211
+ fallback: string
4212
+ ): string => `var(--persona-components-panel-modes-${mode}-${field}, ${fallback})`;
4213
+ const configuredFloatingPanel = panelPartial?.modes?.floating;
4214
+ const cardBorder = resolvePanelChrome(
4215
+ configuredFloatingPanel?.border ?? panelPartial?.border,
4216
+ modeChrome("floating", "border", 'var(--persona-panel-border, 1px solid var(--persona-border))')
4217
+ );
4218
+ const cardShadow = resolvePanelChrome(
4219
+ configuredFloatingPanel?.shadow ?? panelPartial?.shadow,
4220
+ modeChrome("floating", "shadow", 'var(--persona-panel-shadow, var(--persona-palette-shadows-xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25)))')
4221
+ );
4222
+ const cardRadius = resolvePanelChrome(
4223
+ configuredFloatingPanel?.borderRadius ?? panelPartial?.borderRadius,
4224
+ modeChrome("floating", "borderRadius", 'var(--persona-panel-radius, var(--persona-radius-xl, 0.75rem))')
4225
+ );
4116
4226
  /** Detached restores card chrome except when a host layout goes fullscreen. */
4117
4227
  const detachedCard = isDetached && !shouldGoFullscreen && !dockedHostFullscreen;
4228
+ const panelMode = shouldGoFullscreen || dockedHostFullscreen
4229
+ ? "mobile"
4230
+ : detachedCard
4231
+ ? "floating"
4232
+ : dockedMode
4233
+ ? "docked"
4234
+ : sidebarMode
4235
+ ? "sidebar"
4236
+ : isInlineEmbed
4237
+ ? "inline"
4238
+ : "floating";
4239
+ const configuredModePanel = panelPartial?.modes?.[panelMode];
4118
4240
  // Stamp reflects rendered chrome: cleared when a fullscreen host layout
4119
4241
  // suppresses the card, so the attribute never lies to the detached CSS.
4120
4242
  if (detachedCard) {
@@ -4132,7 +4254,7 @@ export const createAgentExperience = (
4132
4254
  : shouldGoFullscreen
4133
4255
  ? 'none'
4134
4256
  : sidebarMode
4135
- ? (isLeftSidebar ? 'var(--persona-palette-shadows-sidebar-left, 2px 0 12px rgba(0, 0, 0, 0.08))' : 'var(--persona-palette-shadows-sidebar-right, -2px 0 12px rgba(0, 0, 0, 0.08))')
4257
+ ? (isLeftSidebar ? '2px 0 12px rgba(0, 0, 0, 0.08)' : '-2px 0 12px rgba(0, 0, 0, 0.08)')
4136
4258
  // Flush inline embeds fill their container: no elevation by default
4137
4259
  // (detachedPanel or components.panel.shadow opts back in).
4138
4260
  : isInlineEmbed ? 'none' : cardShadow;
@@ -4146,9 +4268,18 @@ export const createAgentExperience = (
4146
4268
  : (sidebarMode || shouldGoFullscreen) ? '0' : cardRadius;
4147
4269
 
4148
4270
  // Apply theme overrides or defaults (components.panel.*)
4149
- const panelBorder = resolvePanelChrome(panelPartial?.border, defaultPanelBorder);
4150
- const panelShadow = resolvePanelChrome(panelPartial?.shadow, defaultPanelShadow);
4151
- const panelBorderRadius = resolvePanelChrome(panelPartial?.borderRadius, defaultPanelBorderRadius);
4271
+ const panelBorder = resolvePanelChrome(
4272
+ configuredModePanel?.border ?? panelPartial?.border,
4273
+ modeChrome(panelMode, "border", defaultPanelBorder)
4274
+ );
4275
+ const panelShadow = resolvePanelChrome(
4276
+ configuredModePanel?.shadow ?? panelPartial?.shadow,
4277
+ modeChrome(panelMode, "shadow", defaultPanelShadow)
4278
+ );
4279
+ const panelBorderRadius = resolvePanelChrome(
4280
+ configuredModePanel?.borderRadius ?? panelPartial?.borderRadius,
4281
+ modeChrome(panelMode, "borderRadius", defaultPanelBorderRadius)
4282
+ );
4152
4283
 
4153
4284
  // Split chrome: 'welded' folds the card border onto the outer panel so it
4154
4285
  // wraps both columns as one card (shadow/radius already on the panel);
@@ -4192,7 +4323,8 @@ export const createAgentExperience = (
4192
4323
  // Flush fills the container flush, so the outer panel squares off by default;
4193
4324
  // the pane keeps its own rounded radius. An explicit panel.borderRadius wins.
4194
4325
  const panelRadiusExplicit =
4195
- panelPartial?.borderRadius != null && panelPartial.borderRadius !== '';
4326
+ (configuredModePanel?.borderRadius != null && configuredModePanel.borderRadius !== '') ||
4327
+ (panelPartial?.borderRadius != null && panelPartial.borderRadius !== '');
4196
4328
  const appliedPanelRadius =
4197
4329
  chatFlush && !panelRadiusExplicit ? '0' : panelBorderRadius;
4198
4330
 
@@ -4317,15 +4449,14 @@ export const createAgentExperience = (
4317
4449
  }
4318
4450
 
4319
4451
  // Re-apply panel width/maxWidth from initial setup
4320
- const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
4321
- const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
4452
+ const geometry = resolvePanelGeometry(config);
4322
4453
  if (!sidebarMode && !dockedMode) {
4323
4454
  if (isInlineEmbed && fullHeight) {
4324
4455
  panel.style.width = "100%";
4325
4456
  panel.style.maxWidth = "100%";
4326
4457
  } else {
4327
- panel.style.width = width;
4328
- panel.style.maxWidth = width;
4458
+ panel.style.width = geometry.width;
4459
+ panel.style.maxWidth = geometry.maxWidth;
4329
4460
  }
4330
4461
  } else if (dockedMode) {
4331
4462
  const dockReveal = resolveDockConfig(config).reveal;
@@ -4375,7 +4506,9 @@ export const createAgentExperience = (
4375
4506
  weldedOuterRadius = null;
4376
4507
  }
4377
4508
 
4378
- if (dockedMode && !shouldGoFullscreen && !detachedCard && !detachedSplitActive && !weldedSplitActive && panelPartial?.border === undefined) {
4509
+ const panelBorderExplicit =
4510
+ configuredModePanel?.border != null || panelPartial?.border != null;
4511
+ if (dockedMode && !shouldGoFullscreen && !detachedCard && !detachedSplitActive && !weldedSplitActive && !panelBorderExplicit) {
4379
4512
  container.style.border = 'none';
4380
4513
  const dockSide = resolveDockConfig(config).side;
4381
4514
  if (dockSide === 'right') {
@@ -4389,7 +4522,7 @@ export const createAgentExperience = (
4389
4522
  // mode resolves its border to none, so add the dock-facing hairline there so
4390
4523
  // the split still separates from the host page. Mirrors the flush block's
4391
4524
  // side choice (right dock => left edge faces the page).
4392
- if (dockedMode && !shouldGoFullscreen && weldedSplitActive && panelPartial?.border === undefined) {
4525
+ if (dockedMode && !shouldGoFullscreen && weldedSplitActive && !panelBorderExplicit) {
4393
4526
  const dockSide = resolveDockConfig(config).side;
4394
4527
  if (dockSide === 'right') {
4395
4528
  panel.style.borderLeft = '1px solid var(--persona-border)';
@@ -4478,7 +4611,9 @@ export const createAgentExperience = (
4478
4611
 
4479
4612
  // Apply sidebar-specific styles
4480
4613
  if (sidebarMode) {
4481
- const sidebarWidth = config.launcher?.sidebarWidth ?? '420px';
4614
+ const sidebarWidth = getPanelAliasProvenance(config).sidebarWidth
4615
+ ? config.launcher?.sidebarWidth ?? "420px"
4616
+ : resolveTokenValue(getActiveTheme(config), "components.panel.width") ?? "420px";
4482
4617
 
4483
4618
  // Wrapper - fixed position. Detached insets the card off the edges and
4484
4619
  // shrinks its height by the inset on both ends; flush hugs the edges.
@@ -4592,7 +4727,12 @@ export const createAgentExperience = (
4592
4727
  let warnedComposerBarPlacement = false;
4593
4728
  // mount.style.cssText is wiped by applyFullHeightStyles, so every
4594
4729
  // mount-level var re-stamps here, next to applyContentMaxWidthVar.
4730
+ function syncTranscriptTopFade() {
4731
+ if (config.layout?.topFade && body.scrollTop > 1) body.setAttribute("data-persona-top-fade", "true");
4732
+ else body.removeAttribute("data-persona-top-fade");
4733
+ }
4595
4734
  const applyComposerPlacement = () => {
4735
+ syncTranscriptTopFade();
4596
4736
  const placement = resolveComposerPlacement(config, isComposerBar());
4597
4737
  if (
4598
4738
  isComposerBar() &&
@@ -4605,7 +4745,7 @@ export const createAgentExperience = (
4605
4745
  "[persona] composer.placement is ignored in composer-bar mount mode."
4606
4746
  );
4607
4747
  }
4608
- const welcome = resolveWelcomeConfig(config);
4748
+ const welcome = getWelcomeConfig();
4609
4749
  mount.setAttribute("data-persona-composer-placement", placement);
4610
4750
  mount.setAttribute("data-persona-conversation-state", conversationState);
4611
4751
  // Root-level mirror of the welcome host's own anchor attribute, so the
@@ -4632,12 +4772,9 @@ export const createAgentExperience = (
4632
4772
  const mobileFullscreen = config.launcher?.mobileFullscreen ?? true;
4633
4773
  const mobileBreakpoint = config.launcher?.mobileBreakpoint ?? 640;
4634
4774
  if (mobileFullscreen && ownerWindow.innerWidth <= mobileBreakpoint) return;
4635
- const viewportHeight = ownerWindow.innerHeight;
4636
- const verticalMargin = 64; // leave space for launcher's offset
4637
- const heightOffset = config.launcher?.heightOffset ?? 0;
4638
- const available = Math.max(200, viewportHeight - verticalMargin);
4639
- const clamped = Math.min(640, available);
4640
- panel.style.height = `${Math.max(200, clamped - heightOffset)}px`;
4775
+ const geometry = resolvePanelGeometry(config);
4776
+ panel.style.height = geometry.height;
4777
+ panel.style.maxHeight = geometry.maxHeight;
4641
4778
  };
4642
4779
  applyFullHeightStyles();
4643
4780
  // Apply theme variables after applyFullHeightStyles since it resets mount.style.cssText
@@ -4981,12 +5118,14 @@ export const createAgentExperience = (
4981
5118
  return;
4982
5119
  }
4983
5120
 
4984
- const starters = config.suggestions?.starters;
5121
+ const v5 = config.future?.v5Defaults === true;
5122
+ const fullscreen = isWelcomeFullscreen();
5123
+ const starters = config.suggestions?.starters ?? (v5 ? {} : undefined);
4985
5124
  if (starters) {
4986
5125
  // The starter host lives inside the welcome surface, so a pinned
4987
5126
  // "welcome" placement has nowhere to render when that surface is hidden.
4988
5127
  const welcomeCardVisible = isWelcomeVisible(
4989
- resolveWelcomeConfig(config),
5128
+ getWelcomeConfig(),
4990
5129
  current
4991
5130
  );
4992
5131
  const requestedPlacement = starters.placement ?? "auto";
@@ -5014,10 +5153,10 @@ export const createAgentExperience = (
5014
5153
  config.suggestionChipsConfig,
5015
5154
  {
5016
5155
  surface: "starter",
5017
- variant: starters.variant ?? "card",
5156
+ variant: starters.variant ?? (v5 && !fullscreen ? "chip" : "card"),
5018
5157
  behavior: starters.behavior ?? "send",
5019
5158
  overflow: starters.overflow ?? "wrap",
5020
- maxItems: starters.maxItems ?? 4,
5159
+ maxItems: starters.maxItems ?? (v5 && !fullscreen ? 3 : 4),
5021
5160
  config,
5022
5161
  plugins,
5023
5162
  submitPrompt: submitSuggestionPrompt,
@@ -5038,6 +5177,7 @@ export const createAgentExperience = (
5038
5177
  variant: "chip",
5039
5178
  behavior: "send",
5040
5179
  overflow: "wrap",
5180
+ maxItems: 4,
5041
5181
  config,
5042
5182
  plugins,
5043
5183
  submitPrompt: submitSuggestionPrompt,
@@ -5220,7 +5360,6 @@ export const createAgentExperience = (
5220
5360
  lastUserMessageWasVoice: false,
5221
5361
  lastUserMessageId: null as string | null
5222
5362
  };
5223
- isDictationActive = () => voiceState.active;
5224
5363
  // First stamp: an untouched composer emits no store change, so the attribute
5225
5364
  // would otherwise not appear until the first keystroke.
5226
5365
  syncComposerCompact();
@@ -6163,7 +6302,9 @@ export const createAgentExperience = (
6163
6302
  // Only an explicit config value is stamped inline; the 85%/100% defaults
6164
6303
  // live in widget.css so `components.message.<role>.maxWidth` can win.
6165
6304
  const maxWidth =
6166
- roleLayout?.maxWidth ?? (width === "full" ? "100%" : undefined);
6305
+ roleLayout?.maxWidth ?? (width === "full"
6306
+ ? `var(--persona-message-${sizingRole}-max-width, 100%)`
6307
+ : undefined);
6167
6308
 
6168
6309
  wrapper.classList.add("persona-message-row");
6169
6310
  wrapper.classList.remove(
@@ -6201,6 +6342,15 @@ export const createAgentExperience = (
6201
6342
  messages: AgentWidgetMessage[],
6202
6343
  transform: MessageTransform
6203
6344
  ) => {
6345
+ const activityIds = new Set<string>();
6346
+ for (const message of messages) {
6347
+ const kind = message.variant === "tool" ? "tool" : message.variant === "reasoning" ? "reasoning" : null;
6348
+ if (!kind || activityVariant(config, kind) !== "row") continue;
6349
+ const display = kind === "tool" ? config.features?.toolCallDisplay : config.features?.reasoningDisplay;
6350
+ if (display?.expandable === false) continue;
6351
+ activityIds.add(message.id);
6352
+ activityLifecycle.observe(message, kind, kind === "tool" ? toolExpansionState : reasoningExpansionState, display);
6353
+ }
6204
6354
  // Build new content in a temporary container for morphing
6205
6355
  const tempContainer = document.createElement("div");
6206
6356
 
@@ -6613,7 +6763,7 @@ export const createAgentExperience = (
6613
6763
  };
6614
6764
  liveBubble = approvalPlugin.renderApproval({
6615
6765
  message,
6616
- defaultRenderer: () => approvalMod.createApprovalBubble(message, config),
6766
+ defaultRenderer: () => approvalMod.createApprovalBubble(message, config, approvalDetailsExpansionState),
6617
6767
  config,
6618
6768
  approve: (options) => resolveDecision("approved", options),
6619
6769
  deny: (options) => resolveDecision("denied", options)
@@ -6629,7 +6779,7 @@ export const createAgentExperience = (
6629
6779
  const existing = container.querySelector<HTMLElement>(`#wrapper-${message.id}`);
6630
6780
  existing?.removeAttribute("data-preserve-runtime");
6631
6781
  lastApprovalBubbleFingerprint.delete(message.id);
6632
- bubble = approvalMod.createApprovalBubble(message, config);
6782
+ bubble = approvalMod.createApprovalBubble(message, config, approvalDetailsExpansionState);
6633
6783
  } else {
6634
6784
  // A fresh live bubble to hydrate (needsRebuild), or fingerprint
6635
6785
  // unchanged so we reuse the preserved live wrapper (`bubble: null`).
@@ -6649,14 +6799,14 @@ export const createAgentExperience = (
6649
6799
  if (!showReasoning) return;
6650
6800
  bubble = matchingPlugin.renderReasoning({
6651
6801
  message,
6652
- defaultRenderer: () => createReasoningBubble(message, config),
6802
+ defaultRenderer: () => createReasoningBubble(message, config, reasoningExpansionState),
6653
6803
  config
6654
6804
  });
6655
6805
  } else if (message.variant === "tool" && message.toolCall && matchingPlugin.renderToolCall) {
6656
6806
  if (!showToolCalls) return;
6657
6807
  bubble = matchingPlugin.renderToolCall({
6658
6808
  message,
6659
- defaultRenderer: () => createToolBubble(message, config),
6809
+ defaultRenderer: () => createToolBubble(message, config, toolExpansionState),
6660
6810
  config
6661
6811
  });
6662
6812
  } else if (matchingPlugin.renderMessage) {
@@ -6824,15 +6974,15 @@ export const createAgentExperience = (
6824
6974
  if (!bubble) {
6825
6975
  if (message.variant === "reasoning" && message.reasoning) {
6826
6976
  if (!showReasoning) return;
6827
- bubble = createReasoningBubble(message, config);
6977
+ bubble = createReasoningBubble(message, config, reasoningExpansionState);
6828
6978
  } else if (message.variant === "tool" && message.toolCall) {
6829
6979
  if (!showToolCalls) return;
6830
- bubble = createToolBubble(message, config);
6980
+ bubble = createToolBubble(message, config, toolExpansionState);
6831
6981
  } else if (message.variant === "approval" && message.approval) {
6832
6982
  if (config.approval === false) return;
6833
6983
  const approvalMod = ensureApprovalUi();
6834
6984
  if (!approvalMod) return;
6835
- bubble = approvalMod.createApprovalBubble(message, config);
6985
+ bubble = approvalMod.createApprovalBubble(message, config, approvalDetailsExpansionState);
6836
6986
  } else {
6837
6987
  // Check for custom message renderers in layout config
6838
6988
  const messageLayoutConfig = config.layout?.messages;
@@ -6950,6 +7100,32 @@ export const createAgentExperience = (
6950
7100
  );
6951
7101
  groupWrapper.setAttribute("data-persona-tool-group-row", "true");
6952
7102
 
7103
+ if (config.features?.toolCallDisplay?.groupedMode === "collapsible") {
7104
+ const id = `tool-group-${group[0].id}`;
7105
+ const active = group.some(item => item.toolCall?.status !== "complete");
7106
+ const synthetic: AgentWidgetMessage = { ...group[0], id, toolCall: {
7107
+ id, name: `${group.length} tools`, status: active ? "running" : "complete",
7108
+ chunks: group.flatMap(item => item.toolCall?.chunks ?? []),
7109
+ success: !group.some(item => item.toolCall?.success === false),
7110
+ } };
7111
+ activityIds.add(id);
7112
+ activityLifecycle.observe(synthetic, "tool", toolExpansionState, config.features.toolCallDisplay);
7113
+ const label = `${active ? "Using" : "Used"} ${group.length} tools`;
7114
+ const custom = config.toolCall?.renderGroupedSummary?.({ messages: group, toolCalls: group.map(item => item.toolCall!), defaultSummary: label, config });
7115
+ const row = createActivityGroup(synthetic, config, toolExpansionState.has(id), custom ?? label);
7116
+ row.bubble.classList.add("persona-tool-group");
7117
+ row.body.dataset.personaToolGroupStack = "true";
7118
+ wrappers[0].before(groupWrapper);
7119
+ groupWrapper.appendChild(row.bubble);
7120
+ wrappers.forEach((wrapper, index) => {
7121
+ wrapper.style.setProperty("--persona-message-row-max-width", "100%");
7122
+ wrapper.style.setProperty("--persona-activity-stagger", `${index * 40}ms`);
7123
+ wrapper.classList.add("persona-activity-group-child");
7124
+ row.body.appendChild(wrapper);
7125
+ });
7126
+ return;
7127
+ }
7128
+
6953
7129
  const groupContainer = document.createElement("div");
6954
7130
  groupContainer.className =
6955
7131
  "persona-tool-group persona-flex persona-w-full persona-flex-col persona-gap-2";
@@ -7006,6 +7182,8 @@ export const createAgentExperience = (
7006
7182
  });
7007
7183
  }
7008
7184
 
7185
+ activityLifecycle.prune(activityIds);
7186
+
7009
7187
  // Remove cache entries for messages that no longer exist
7010
7188
  pruneCache(messageCache, activeMessageIds);
7011
7189
 
@@ -8018,6 +8196,7 @@ export const createAgentExperience = (
8018
8196
  statusText.setAttribute("aria-live", "polite");
8019
8197
  statusText.setAttribute(COMPOSER_REASON_ATTR, "");
8020
8198
  statusText.textContent = reason;
8199
+ applyStatusIndicatorState(statusText, config.statusIndicator ?? {}, "idle");
8021
8200
  return;
8022
8201
  }
8023
8202
  if (!statusText.hasAttribute(COMPOSER_REASON_ATTR)) return;
@@ -8176,7 +8355,8 @@ export const createAgentExperience = (
8176
8355
  composerLiftAnimation = animateComposerLiftChange(
8177
8356
  footer,
8178
8357
  next === "active" ? previousLift : readLiftPx(),
8179
- next === "active" ? "drop" : "rise"
8358
+ next === "active" ? "drop" : "rise",
8359
+ getWelcomeConfig().layout === "centered" ? 320 : 260
8180
8360
  );
8181
8361
  }
8182
8362
  // The reservation shrinks by the lift in one frame, above the anchor.
@@ -8194,8 +8374,8 @@ export const createAgentExperience = (
8194
8374
  // so the pending stand-in the overlay body class would hide stays visible.
8195
8375
  let welcomePluginSuppressed = false;
8196
8376
  const updateWelcome = (messages?: AgentWidgetMessage[]) => {
8197
- const resolved = resolveWelcomeConfig(config);
8198
- const welcomeKey = `${resolved.variant}|${resolved.dismiss}|${resolved.anchor ?? "bottom"}|${resolved.align ?? ""}|${resolved.iconPlacement}|${resolved.kicker ?? ""}|${resolved.title}|${resolved.subtitle}`;
8377
+ const resolved = getWelcomeConfig();
8378
+ const welcomeKey = `${resolved.variant}|${resolved.layout}|${resolved.dismiss}|${resolved.anchor ?? "bottom"}|${resolved.align ?? ""}|${resolved.iconPlacement}|${resolved.kicker ?? ""}|${resolved.title}|${resolved.subtitle}`;
8199
8379
  if (welcomeKey !== lastWelcomeKey || resolved.icon !== lastWelcomeIcon) {
8200
8380
  lastWelcomeKey = welcomeKey;
8201
8381
  lastWelcomeIcon = resolved.icon;
@@ -8348,8 +8528,8 @@ export const createAgentExperience = (
8348
8528
  // A plugin calling requestRender() from inside its own render would recurse.
8349
8529
  if (welcomeArbitrating) return;
8350
8530
  welcomeArbitrating = true;
8351
- const resolved = resolveWelcomeConfig(config);
8352
- lastWelcomeArbitrationKey = `${resolved.variant}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
8531
+ const resolved = getWelcomeConfig();
8532
+ lastWelcomeArbitrationKey = `${resolved.variant}|${resolved.layout}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
8353
8533
  lastWelcomeArbitrationIcon = resolved.icon;
8354
8534
  try {
8355
8535
  runWelcomeCleanups();
@@ -8398,8 +8578,8 @@ export const createAgentExperience = (
8398
8578
 
8399
8579
  /** Re-arbitrate only when the resolved welcome config actually changed. */
8400
8580
  const refreshWelcomePlugins = () => {
8401
- const resolved = resolveWelcomeConfig(config);
8402
- const key = `${resolved.variant}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
8581
+ const resolved = getWelcomeConfig();
8582
+ const key = `${resolved.variant}|${resolved.layout}|${resolved.dismiss}|${resolved.title}|${resolved.subtitle}|${resolved.message ?? ""}`;
8403
8583
  if (
8404
8584
  key === lastWelcomeArbitrationKey &&
8405
8585
  resolved.icon === lastWelcomeArbitrationIcon
@@ -8639,7 +8819,7 @@ export const createAgentExperience = (
8639
8819
  spans.forEach((span) => {
8640
8820
  const startedAt = Number(span.getAttribute("data-tool-elapsed"));
8641
8821
  if (!startedAt) return;
8642
- span.textContent = formatElapsedMs(now - startedAt);
8822
+ span.textContent = span.closest(".persona-activity-row") ? activityDuration(now - startedAt) : formatElapsedMs(now - startedAt);
8643
8823
  });
8644
8824
  }, 100);
8645
8825
  };
@@ -11413,9 +11593,15 @@ export const createAgentExperience = (
11413
11593
  historySurface = null;
11414
11594
  });
11415
11595
 
11416
- // Pre-initialize client session when in client token mode so feedback works
11417
- // before the user sends their first message (e.g. on restored/persisted messages)
11418
- if (config.clientToken) {
11596
+ // Client-token init on mount is reserved for widgets that need a session at
11597
+ // boot: visitor history (boot reconciliation) and `sessionInit: 'mount'`.
11598
+ // Everything else inits on intent (the `sessionInit` triggers below) or at
11599
+ // send, since init is no longer free server-side (it pre-allocates the
11600
+ // conversation's session owner).
11601
+ if (
11602
+ config.clientToken &&
11603
+ (config.features?.history?.enabled === true || config.sessionInit === "mount")
11604
+ ) {
11419
11605
  session
11420
11606
  .initClientSession()
11421
11607
  .then(() => {
@@ -11435,6 +11621,73 @@ export const createAgentExperience = (
11435
11621
  });
11436
11622
  }
11437
11623
 
11624
+ // Early session init on visitor intent (`sessionInit`). Delegated on the
11625
+ // mount so composer rebuilds stay covered, and read `config` at event time so
11626
+ // `update()` applies without re-wiring.
11627
+ const warmSession = () => session.warmClientSession();
11628
+ // One delegated listener. `input` counts only real typing, paste, drop or
11629
+ // IME (an InputEvent with an inputType; the widget's programmatic edits
11630
+ // dispatch a plain Event) that leaves text. In 'focus' mode a pointer-down in
11631
+ // the composer or a Tab into it also counts; `.focus()` (autofocus on
11632
+ // open/mount) produces neither.
11633
+ const handleSessionInitIntent = (event: Event) => {
11634
+ const mode = config.sessionInit ?? "input";
11635
+ const input = composerBindings?.input ?? textarea;
11636
+ const target = event.target as Node;
11637
+ if (
11638
+ input &&
11639
+ (event.type === "input"
11640
+ ? (mode === "input" || mode === "focus") &&
11641
+ target === input &&
11642
+ !!(event as InputEvent).inputType &&
11643
+ !!(input as HTMLTextAreaElement).value.trim()
11644
+ : mode === "focus" &&
11645
+ (event.type === "keyup"
11646
+ ? target === input && (event as KeyboardEvent).key === "Tab"
11647
+ : !!(composerBindings?.form ?? composerForm)?.contains(target)))
11648
+ ) {
11649
+ warmSession();
11650
+ }
11651
+ };
11652
+ const sessionInitEvents = ["input", "pointerdown", "keyup"];
11653
+ sessionInitEvents.forEach((type) => mount.addEventListener(type, handleSessionInitIntent));
11654
+ // The custom trigger (a `sessionInit` function) runs on mount and again
11655
+ // whenever `update()` swaps in a different function, cleaning up the old one.
11656
+ let sessionInitHook: AgentWidgetSessionInitHook | undefined;
11657
+ let sessionInitHookCleanup: ReturnType<AgentWidgetSessionInitHook>;
11658
+ // Host hook code may throw; neither its setup nor its cleanup may leave
11659
+ // mount, update() or destroy() half done. The send still inits either way.
11660
+ const releaseSessionInitHook = () => {
11661
+ const cleanup = sessionInitHookCleanup;
11662
+ sessionInitHookCleanup = undefined;
11663
+ try {
11664
+ if (typeof cleanup === "function") cleanup();
11665
+ } catch {
11666
+ // ignored, see above
11667
+ }
11668
+ };
11669
+ const syncSessionInitHook = () => {
11670
+ const next =
11671
+ config.clientToken && typeof config.sessionInit === "function"
11672
+ ? config.sessionInit
11673
+ : undefined;
11674
+ if (next === sessionInitHook) return;
11675
+ releaseSessionInitHook();
11676
+ sessionInitHook = next;
11677
+ try {
11678
+ sessionInitHookCleanup = next?.({ warm: warmSession, mount });
11679
+ } catch {
11680
+ // ignored, see above
11681
+ }
11682
+ };
11683
+ syncSessionInitHook();
11684
+ destroyCallbacks.push(() => {
11685
+ sessionInitEvents.forEach((type) =>
11686
+ mount.removeEventListener(type, handleSessionInitIntent)
11687
+ );
11688
+ releaseSessionInitHook();
11689
+ });
11690
+
11438
11691
  // Wire up optional SSE tap (host) + event stream buffer to capture SSE
11439
11692
  // events. `feedEventStream` stages events while the lazy chunk is in flight.
11440
11693
  if (showEventStreamToggle || config.onSSEEvent) {
@@ -11607,6 +11860,7 @@ export const createAgentExperience = (
11607
11860
  statusText.setAttribute("role", "status");
11608
11861
  statusText.setAttribute("aria-live", "polite");
11609
11862
  statusText.textContent = text;
11863
+ applyStatusIndicatorState(statusText, config.statusIndicator ?? {}, "idle");
11610
11864
  if (composerNoticeTimer) clearTimeout(composerNoticeTimer);
11611
11865
  composerNoticeTimer = setTimeout(() => {
11612
11866
  composerNoticeTimer = null;
@@ -12429,7 +12683,7 @@ export const createAgentExperience = (
12429
12683
  }
12430
12684
  emitVoiceState(source);
12431
12685
  persistVoiceMetadata();
12432
- // Live dictation is a composer occupant; the store emits nothing for it.
12686
+ // Reconcile content geometry without treating recording as extra content.
12433
12687
  syncComposerCompact();
12434
12688
  if (micButton) {
12435
12689
  // Store original styles (including icon info for restoration)
@@ -12728,6 +12982,62 @@ export const createAgentExperience = (
12728
12982
 
12729
12983
  composerVoiceBridge = handleMicButtonClick;
12730
12984
 
12985
+ // Voice prewarm on mic intent (`voiceRecognition.prewarm`). Delegated on the
12986
+ // mount so composer rebuilds stay covered, and `config` is read at event time
12987
+ // so `update()` applies without re-wiring. Default: 'hover' in client token
12988
+ // mode, off otherwise.
12989
+ const warmVoice = () => session.prewarmVoice();
12990
+ const voicePrewarmSetting = () => {
12991
+ const voice = config.voiceRecognition;
12992
+ if (voice?.enabled !== true) return false;
12993
+ return voice.prewarm ?? (config.clientToken ? "hover" : false);
12994
+ };
12995
+ // `pointerover` covers mouse hover and precedes a touch's pointer-down;
12996
+ // `focusin` covers keyboard focus.
12997
+ const handleVoicePrewarmIntent = (event: Event) => {
12998
+ if (voicePrewarmSetting() !== "hover") return;
12999
+ if (micButton?.contains(event.target as Node)) warmVoice();
13000
+ };
13001
+ const voicePrewarmEvents = ["pointerover", "pointerdown", "focusin"];
13002
+ voicePrewarmEvents.forEach((type) => mount.addEventListener(type, handleVoicePrewarmIntent));
13003
+ // A custom prewarm hook is bound to one mic button: it re-runs (after its
13004
+ // cleanup) when the button is rebuilt or `update()` swaps the function.
13005
+ let voicePrewarmHook: AgentWidgetVoicePrewarmHook | undefined;
13006
+ let voicePrewarmHookButton: HTMLButtonElement | null = null;
13007
+ let voicePrewarmHookCleanup: ReturnType<AgentWidgetVoicePrewarmHook>;
13008
+ // Host hook code may throw; neither setup nor cleanup may break the widget.
13009
+ const releaseVoicePrewarmHook = () => {
13010
+ const cleanup = voicePrewarmHookCleanup;
13011
+ voicePrewarmHookCleanup = undefined;
13012
+ try {
13013
+ if (typeof cleanup === "function") cleanup();
13014
+ } catch {
13015
+ // ignored, see above
13016
+ }
13017
+ };
13018
+ const syncVoicePrewarmHook = () => {
13019
+ const setting = voicePrewarmSetting();
13020
+ const next = typeof setting === "function" && micButton ? setting : undefined;
13021
+ const button = next ? micButton : null;
13022
+ if (next === voicePrewarmHook && button === voicePrewarmHookButton) return;
13023
+ releaseVoicePrewarmHook();
13024
+ voicePrewarmHook = next;
13025
+ voicePrewarmHookButton = button;
13026
+ if (!next || !button) return;
13027
+ try {
13028
+ voicePrewarmHookCleanup = next({ warm: warmVoice, micButton: button, mount });
13029
+ } catch {
13030
+ // ignored, see above
13031
+ }
13032
+ };
13033
+ syncVoicePrewarmHook();
13034
+ destroyCallbacks.push(() => {
13035
+ voicePrewarmEvents.forEach((type) =>
13036
+ mount.removeEventListener(type, handleVoicePrewarmIntent)
13037
+ );
13038
+ releaseVoicePrewarmHook();
13039
+ });
13040
+
12731
13041
  if (micButton) {
12732
13042
  // The click listener itself is registered by `wireComposerSurface`, so a
12733
13043
  // composer rebuild unwires it with the rest of the surface.
@@ -12846,8 +13156,10 @@ export const createAgentExperience = (
12846
13156
  footerResizeObserver.disconnect();
12847
13157
  footerResizeObserver.observe(footer);
12848
13158
  footerResizeObserver.observe(container);
13159
+ footerResizeObserver.observe(welcomeHost);
12849
13160
  };
12850
13161
 
13162
+ let lastWelcomeFullscreen = isWelcomeFullscreen();
12851
13163
  const recalcPanelHeight = () => {
12852
13164
  // Composer-bar mode lets CSS own all sizing: collapsed pill is auto-sized
12853
13165
  // by the footer; expanded fullscreen/modal are driven by CSS attribute
@@ -12909,10 +13221,9 @@ export const createAgentExperience = (
12909
13221
 
12910
13222
  // In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
12911
13223
  if (!sidebarMode && !dockedMode) {
12912
- const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
12913
- const width = launcherWidth ?? DEFAULT_FLOATING_LAUNCHER_WIDTH;
12914
- panel.style.width = width;
12915
- panel.style.maxWidth = width;
13224
+ const geometry = resolvePanelGeometry(config);
13225
+ panel.style.width = geometry.width;
13226
+ panel.style.maxWidth = geometry.maxWidth;
12916
13227
  }
12917
13228
  applyLauncherArtifactPanelWidth();
12918
13229
 
@@ -12923,6 +13234,12 @@ export const createAgentExperience = (
12923
13234
  // overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
12924
13235
  updateScrollToBottomButtonOffset();
12925
13236
  syncComposerOverlayMetrics();
13237
+ const welcomeFullscreen = isWelcomeFullscreen();
13238
+ if (welcomeFullscreen !== lastWelcomeFullscreen) {
13239
+ lastWelcomeFullscreen = welcomeFullscreen;
13240
+ updateWelcome();
13241
+ renderSuggestions();
13242
+ }
12926
13243
  updateOpenState();
12927
13244
 
12928
13245
  // Sync scroll lock and host stacking when viewport mode changes (e.g. orientation change)
@@ -12985,6 +13302,7 @@ export const createAgentExperience = (
12985
13302
  let lastBottomOffset = getScrollBottomOffset(body);
12986
13303
 
12987
13304
  const handleScroll = () => {
13305
+ syncTranscriptTopFade();
12988
13306
  const scrollTop = body.scrollTop;
12989
13307
  // When content mutates (e.g. stream-animation plugins re-rendering text)
12990
13308
  // or the viewport grows (composer shrinking back), the maximum scroll
@@ -13203,6 +13521,8 @@ export const createAgentExperience = (
13203
13521
  }
13204
13522
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
13205
13523
  session.clearMessages();
13524
+ if (config.layout?.topFade) body.scrollTop = 0;
13525
+ body.removeAttribute("data-persona-top-fade");
13206
13526
  messageCache.clear();
13207
13527
  resumeAutoScroll();
13208
13528
 
@@ -13315,6 +13635,7 @@ export const createAgentExperience = (
13315
13635
  if (micButton) {
13316
13636
  composerBindings.addListener(micButton, "click", handleMicButtonClick);
13317
13637
  }
13638
+ syncVoicePrewarmHook();
13318
13639
  };
13319
13640
 
13320
13641
  wireComposerSurface();
@@ -13418,6 +13739,10 @@ export const createAgentExperience = (
13418
13739
  updateCopy();
13419
13740
  renderSuggestions();
13420
13741
  setComposerDisabled(session.isStreaming());
13742
+ if (config.statusIndicator?.mode === "transient" && !isComposerBar()) {
13743
+ const status = session.getStatus();
13744
+ applyStatusToElement(statusText, _getStatusText(status), config.statusIndicator, status);
13745
+ }
13421
13746
  // The rebuilt mic is stamped idle by its builder. A rebuild mid-recording
13422
13747
  // has to restore the live state, which also re-arms the level loop
13423
13748
  // against the new footer and wrapper.
@@ -13589,6 +13914,7 @@ export const createAgentExperience = (
13589
13914
 
13590
13915
  const controller: Controller = {
13591
13916
  update(nextConfig: AgentWidgetConfigPatch) {
13917
+ const previousDefaultsVersion = config.future?.v5Defaults === true;
13592
13918
  const previousToolCallConfig = config.toolCall;
13593
13919
  const previousMessageActions = config.messageActions;
13594
13920
  const previousLayoutMessages = config.layout?.messages;
@@ -13862,7 +14188,8 @@ export const createAgentExperience = (
13862
14188
  || (config.features?.showToolCalls ?? true) !== (previousShowToolCalls ?? true)
13863
14189
  || JSON.stringify(config.features?.toolCallDisplay) !== JSON.stringify(previousToolCallDisplay)
13864
14190
  || JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay);
13865
- const messagesConfigChanged = toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged
14191
+ const messagesConfigChanged = (config.future?.v5Defaults === true) !== previousDefaultsVersion
14192
+ || toolCallConfigChanged || messageActionsChanged || layoutMessagesChanged
13866
14193
  || loadingIndicatorChanged || iterationDisplayChanged || featuresChanged;
13867
14194
  if (messagesConfigChanged && session) {
13868
14195
  configVersion++;
@@ -13899,7 +14226,7 @@ export const createAgentExperience = (
13899
14226
  // Hide icon if either headerIconHidden is true OR layout.header.showIcon is false
13900
14227
  const shouldHideIcon = headerIconHidden || layoutShowIcon === false;
13901
14228
  const headerIconName = launcher.headerIconName;
13902
- const headerIconSize = launcher.headerIconSize ?? "48px";
14229
+ const headerIconSize = launcher.headerIconSize ?? DEFAULT_HEADER_ICON_SIZE;
13903
14230
 
13904
14231
  if (iconHolder) {
13905
14232
  const headerEl = header;
@@ -13940,6 +14267,8 @@ export const createAgentExperience = (
13940
14267
  const iconSize = parseFloat(headerIconSize) || 24;
13941
14268
  const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "currentColor", 1);
13942
14269
  if (iconSvg) {
14270
+ iconSvg.style.width = `calc(${iconSize}px * var(--persona-components-header-iconScale, 0.6))`;
14271
+ iconSvg.style.height = `calc(${iconSize}px * var(--persona-components-header-iconScale, 0.6))`;
13943
14272
  iconHolder.replaceChildren(iconSvg);
13944
14273
  } else {
13945
14274
  // Fallback to agentIconText if Lucide icon fails
@@ -14306,6 +14635,7 @@ export const createAgentExperience = (
14306
14635
  // Re-key the visitor store before any client rebuild sees the new config.
14307
14636
  syncVisitorStore();
14308
14637
  session.updateConfig(config);
14638
+ syncSessionInitHook();
14309
14639
  if (
14310
14640
  (config.features?.history?.enabled === true) !== previousHistoryEnabled ||
14311
14641
  config.features?.history?.provider !== previousHistoryProvider ||
@@ -14492,6 +14822,7 @@ export const createAgentExperience = (
14492
14822
  }
14493
14823
  }
14494
14824
  }
14825
+ syncVoicePrewarmHook();
14495
14826
 
14496
14827
  // Update attachment button visibility based on attachments config
14497
14828
  const attachmentsEnabled = config.attachments?.enabled === true;
@@ -14816,6 +15147,8 @@ export const createAgentExperience = (
14816
15147
  }
14817
15148
 
14818
15149
  syncComposerLayout();
15150
+ // Placement may change through a live option or defaults-version switch.
15151
+ applyComposerPlacement();
14819
15152
 
14820
15153
  // Modes can be added or removed live: drop selections whose mode is gone
14821
15154
  // before anything reads `activeModeIds`, and mount/unmount the mode chips.
@@ -14900,8 +15233,8 @@ export const createAgentExperience = (
14900
15233
 
14901
15234
  // Update status indicator visibility and text
14902
15235
  const statusIndicatorConfig = config.statusIndicator ?? {};
14903
- const isVisible = statusIndicatorConfig.visible ?? true;
14904
- statusText.style.display = isVisible ? "" : "none";
15236
+ placeStatusIndicator(statusText, composerForm, statusIndicatorConfig);
15237
+ applyStatusIndicatorState(statusText, statusIndicatorConfig, session?.getStatus() ?? "idle");
14905
15238
 
14906
15239
  // Update status text if status is currently set
14907
15240
  if (session) {
@@ -14911,6 +15244,8 @@ export const createAgentExperience = (
14911
15244
  if (s === "connecting") return statusIndicatorConfig.connectingText ?? statusCopy.connecting;
14912
15245
  if (s === "connected") return statusIndicatorConfig.connectedText ?? statusCopy.connected;
14913
15246
  if (s === "error") return statusIndicatorConfig.errorText ?? statusCopy.error;
15247
+ if (s === "paused") return statusIndicatorConfig.pausedText ?? statusCopy.paused;
15248
+ if (s === "resuming") return statusIndicatorConfig.resumingText ?? statusCopy.resuming;
14914
15249
  return statusCopy[s];
14915
15250
  };
14916
15251
  applyStatusToElement(statusText, getCurrentStatusText(currentStatus), statusIndicatorConfig, currentStatus);
@@ -14951,7 +15286,13 @@ export const createAgentExperience = (
14951
15286
  clearChat() {
14952
15287
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
14953
15288
  artifactsPaneUserHidden = false;
15289
+ activityLifecycle.clear();
15290
+ toolExpansionState.clear();
15291
+ reasoningExpansionState.clear();
15292
+ approvalDetailsExpansionState.clear();
14954
15293
  session.clearMessages();
15294
+ if (config.layout?.topFade) body.scrollTop = 0;
15295
+ body.removeAttribute("data-persona-top-fade");
14955
15296
  messageCache.clear();
14956
15297
  resumeAutoScroll();
14957
15298
 
@@ -15207,6 +15548,9 @@ export const createAgentExperience = (
15207
15548
  startNewConversation(opts?: { focus?: boolean }): Promise<void> {
15208
15549
  return startNewConversation({ focusComposer: opts?.focus === true });
15209
15550
  },
15551
+ warmSession(): void {
15552
+ warmSession();
15553
+ },
15210
15554
  deleteConversation(conversationId: string): Promise<void> {
15211
15555
  return deleteHistoryConversation(conversationId);
15212
15556
  },
@@ -15475,6 +15819,10 @@ export const createAgentExperience = (
15475
15819
  clearInterval(toolElapsedTimerId);
15476
15820
  toolElapsedTimerId = null;
15477
15821
  }
15822
+ activityLifecycle.clear();
15823
+ toolExpansionState.clear();
15824
+ reasoningExpansionState.clear();
15825
+ approvalDetailsExpansionState.clear();
15478
15826
  destroyCallbacks.forEach((cb) => cb());
15479
15827
  wrapper.remove();
15480
15828
  pillRoot?.remove();