@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/types.ts CHANGED
@@ -2254,6 +2254,14 @@ export type AgentWidgetToolCallLoadingAnimation =
2254
2254
  | "rainbow";
2255
2255
 
2256
2256
  export type AgentWidgetToolCallDisplayFeature = {
2257
+ /** Activity chrome. Defaults to card in V4 and row with future.v5Defaults. */
2258
+ variant?: "card" | "row";
2259
+ /** Row status icons: always, active/attention-needed only, or never. V5 defaults to active; V4 to always. */
2260
+ iconVisibility?: "always" | "active" | "never";
2261
+ /** Row variant: open when the first content chunk arrives. Default false in V5; true in V4. */
2262
+ autoExpand?: boolean;
2263
+ /** Row variant: collapse once after completion, in ms; false disables. Default 1000. */
2264
+ autoCollapseDelay?: number | false;
2257
2265
  /**
2258
2266
  * Controls what collapsed tool call rows show in their header/summary area.
2259
2267
  * @default "tool-call"
@@ -2284,9 +2292,10 @@ export type AgentWidgetToolCallDisplayFeature = {
2284
2292
  * Controls how a grouped tool sequence is rendered.
2285
2293
  * - `"stack"`: show the group summary and each child tool row.
2286
2294
  * - `"summary"`: show one consolidated summary row only.
2295
+ * - `"collapsible"`: expandable activity group (V5 default).
2287
2296
  * @default "stack"
2288
2297
  */
2289
- groupedMode?: "stack" | "summary";
2298
+ groupedMode?: "stack" | "summary" | "collapsible";
2290
2299
  /**
2291
2300
  * What happens to a tool call row once it completes.
2292
2301
  *
@@ -2319,6 +2328,14 @@ export type AgentWidgetToolCallDisplayFeature = {
2319
2328
  export type AgentWidgetToolCallCompletedVisibility = "kept" | "removed";
2320
2329
 
2321
2330
  export type AgentWidgetReasoningDisplayFeature = {
2331
+ /** Activity chrome. Defaults to card in V4 and row with future.v5Defaults. */
2332
+ variant?: "card" | "row";
2333
+ /** Row status icons: always, active/attention-needed only, or never. V5 defaults to active; V4 to always. */
2334
+ iconVisibility?: "always" | "active" | "never";
2335
+ /** Row variant: open when the first content chunk arrives. Default false in V5; true in V4. */
2336
+ autoExpand?: boolean;
2337
+ /** Row variant: collapse once after completion, in ms; false disables. Default 1000. */
2338
+ autoCollapseDelay?: number | false;
2322
2339
  /**
2323
2340
  * When true, active collapsed reasoning rows can render a lightweight preview block.
2324
2341
  * @default false
@@ -3078,8 +3095,8 @@ export interface AgentWidgetHistoryFeature {
3078
3095
  * otherwise `"browser"`) and narrows it to what the provider advertises, so a
3079
3096
  * browser-only provider is never asked for a scope it cannot serve. An
3080
3097
  * explicit `features.history.scope` is a hard request and still fails closed
3081
- * with `unsupported_scope`. `getIdentityProof` is never called for a supplied
3082
- * provider, and `HistoryOperationContext` carries only the resolved scope;
3098
+ * with `unsupported_scope`. History operations on a supplied provider never
3099
+ * call `getIdentityProof`, and `HistoryOperationContext` carries only the resolved scope;
3083
3100
  * report your own identity through `getIdentityStatus()` /
3084
3101
  * `subscribeIdentityStatus()`, which is what the scope banner and its retry
3085
3102
  * affordance read.
@@ -3483,7 +3500,7 @@ export type EventStreamConfig = {
3483
3500
  /**
3484
3501
  * Fields to extract from event payloads for description text.
3485
3502
  * The first matching field value is displayed after the badge.
3486
- * @default ["flowName", "stepName", "name", "tool", "toolName"]
3503
+ * @default ["flowName", "stepName", "reasoningText", "text", "name", "tool", "toolName"]
3487
3504
  */
3488
3505
  descriptionFields?: string[];
3489
3506
  /**
@@ -3702,6 +3719,8 @@ export type AgentWidgetComposerBarPeekConfig = {
3702
3719
  };
3703
3720
 
3704
3721
  export type AgentWidgetLauncherConfig = {
3722
+ /** Collapsed launcher shape. V4 defaults to pill; V5 defaults to circle. */
3723
+ variant?: "pill" | "circle";
3705
3724
  enabled?: boolean;
3706
3725
  title?: string;
3707
3726
  subtitle?: string;
@@ -4496,9 +4515,9 @@ export type AgentWidgetComposerConfig = {
4496
4515
  /**
4497
4516
  * Where the composer footer sits relative to the transcript.
4498
4517
  *
4499
- * - `"block"` (default): the footer is a flex sibling below the scroll body.
4518
+ * - `"block"` (V4 default): the footer is a flex sibling below the scroll body.
4500
4519
  * Nothing scrolls behind it.
4501
- * - `"overlay"`: the footer is absolutely overlaid on the scroll body, so the
4520
+ * - `"overlay"` (V5 default): the footer is absolutely overlaid on the scroll body, so the
4502
4521
  * transcript scrolls behind it (the chatgpt.com / claude.ai model, where the
4503
4522
  * composer is sticky inside the scroller). The widget reserves the footer's
4504
4523
  * live height as bottom padding on the scroll body and on a plugin welcome
@@ -4510,7 +4529,7 @@ export type AgentWidgetComposerConfig = {
4510
4529
  * included) and `theme.components.input.backdropFilter`.
4511
4530
  *
4512
4531
  * Ignored in composer-bar mount mode, which owns its own geometry.
4513
- * @default "block"
4532
+ * @default "block" (V4), "overlay" (V5)
4514
4533
  */
4515
4534
  placement?: ComposerPlacement;
4516
4535
  /**
@@ -4524,8 +4543,9 @@ export type AgentWidgetComposerConfig = {
4524
4543
  *
4525
4544
  * `"single-row"` describes the IDLE composer only. It composes with the
4526
4545
  * compact state, so a wrapped draft, chips, attachment previews, a quote, a
4527
- * pending card, or live dictation all fall back to the stacked card, exactly
4528
- * as `data-persona-composer-compact` does today.
4546
+ * pending card all fall back to the stacked card, exactly
4547
+ * as `data-persona-composer-compact` does today. Recording alone does not
4548
+ * expand the composer; dictated text expands it when it wraps.
4529
4549
  *
4530
4550
  * Ignored in composer-bar mount mode, whose pill is already one row.
4531
4551
  * @default "stacked"
@@ -4572,7 +4592,14 @@ export type AgentWidgetClearChatConfig = {
4572
4592
  };
4573
4593
 
4574
4594
  export type AgentWidgetStatusIndicatorConfig = {
4595
+ /** Show the indicator. Explicit false also hides transient connection states. */
4575
4596
  visible?: boolean;
4597
+ /**
4598
+ * "always" shows status beneath the composer; "transient" shows connecting,
4599
+ * reconnecting, and error status above it, hiding normal idle/streaming status.
4600
+ * Available in both defaults states. Default: "always" in v4, "transient" in v5.
4601
+ */
4602
+ mode?: "always" | "transient";
4576
4603
  /** Text alignment. Default: 'right'. */
4577
4604
  align?: 'left' | 'center' | 'right';
4578
4605
  idleText?: string;
@@ -4587,6 +4614,24 @@ export type AgentWidgetStatusIndicatorConfig = {
4587
4614
  resumingText?: string;
4588
4615
  };
4589
4616
 
4617
+ /** Built-in `voiceRecognition.prewarm` triggers. See {@link AgentWidgetVoiceRecognitionConfig.prewarm}. */
4618
+ export type AgentWidgetVoicePrewarmTrigger = 'hover';
4619
+
4620
+ /**
4621
+ * Custom `voiceRecognition.prewarm` trigger. Runs while the composer has a mic
4622
+ * button, and again when the button is rebuilt or `update()` swaps in a
4623
+ * different function. Call `warm()` whenever the visitor shows intent to talk
4624
+ * (it is safe to call repeatedly). Return a cleanup to run on replacement.
4625
+ */
4626
+ export type AgentWidgetVoicePrewarmHook = (context: {
4627
+ /** Fire-and-forget voice warm-up; latched and error-swallowing. */
4628
+ warm: () => void;
4629
+ /** The composer's current mic button. */
4630
+ micButton: HTMLButtonElement;
4631
+ /** The widget's mount element. */
4632
+ mount: HTMLElement;
4633
+ }) => void | (() => void);
4634
+
4590
4635
  export type AgentWidgetVoiceRecognitionConfig = {
4591
4636
  /**
4592
4637
  * Enable the mic button and voice input. The mic still only renders when the
@@ -4658,6 +4703,30 @@ export type AgentWidgetVoiceRecognitionConfig = {
4658
4703
  */
4659
4704
  onMetrics?: (metrics: VoiceMetrics) => void;
4660
4705
 
4706
+ /**
4707
+ * Warm the voice path when the visitor shows intent to talk, before the mic
4708
+ * click, so the call starts faster.
4709
+ *
4710
+ * - `'hover'`: when the pointer enters the mic button, it receives focus, or
4711
+ * (on touch) the first pointer-down on it.
4712
+ * - `false`: never.
4713
+ * - A function: custom trigger. Called with `{ warm, micButton, mount }`
4714
+ * while the mic button exists (and again when the button is rebuilt or
4715
+ * `update()` swaps in a different function); call `warm()` whenever you
4716
+ * decide, e.g. when the panel opens. May return a cleanup that runs on
4717
+ * destroy, rebuild, or replacement. Replaces the built-in trigger.
4718
+ *
4719
+ * What `warm()` does depends on the provider: `runtype` fires its prewarm
4720
+ * (see `provider.runtype.prewarmMode`), a `custom` provider's optional
4721
+ * {@link VoiceProvider.prewarm} runs, and the browser (Web Speech) path
4722
+ * starts the client-token session init (as `sessionInit` does), since speech
4723
+ * recognition itself cannot start before the click. Warming is
4724
+ * fire-and-forget, latched, and never surfaces an error or UI state.
4725
+ *
4726
+ * @default 'hover' in client token mode, otherwise `false`
4727
+ */
4728
+ prewarm?: AgentWidgetVoicePrewarmTrigger | AgentWidgetVoicePrewarmHook | false;
4729
+
4661
4730
  // Voice provider configuration
4662
4731
  provider?: {
4663
4732
  type: 'browser' | 'runtype' | 'custom';
@@ -4680,6 +4749,23 @@ export type AgentWidgetVoiceRecognitionConfig = {
4680
4749
  * `@runtypelabs/persona/voice-worklet-player` for a jitter-buffered engine.
4681
4750
  */
4682
4751
  createPlaybackEngine?: () => VoicePlaybackEngine | Promise<VoicePlaybackEngine>;
4752
+ /**
4753
+ * How `prewarm()` warms the voice path (see `voiceRecognition.prewarm`).
4754
+ *
4755
+ * - `'request'` (default): a fire-and-forget
4756
+ * `POST {host}/v1/client/agents/{agentId}/voice/prewarm`.
4757
+ * - `'attach'`: open the voice WebSocket ahead of the click without
4758
+ * starting a call; the click then starts the call on that socket. It can
4759
+ * create server work before the visitor clicks, so it is opt-in.
4760
+ * @default 'request'
4761
+ */
4762
+ prewarmMode?: 'request' | 'attach';
4763
+ /**
4764
+ * `'attach'` mode: how long (ms) an attached socket waits for the click
4765
+ * before it closes. The server clamps it to 30000..600000.
4766
+ * @default 30000
4767
+ */
4768
+ attachIdleMs?: number;
4683
4769
  /** @deprecated No-op on the realtime path: the server's STT owns turn-taking. */
4684
4770
  pauseDuration?: number;
4685
4771
  /** @deprecated No-op on the realtime path: the server's STT owns turn-taking. */
@@ -4987,6 +5073,10 @@ export type VoiceConfig = {
4987
5073
  * `@runtypelabs/persona/voice-worklet-player` for a jitter-buffered engine.
4988
5074
  */
4989
5075
  createPlaybackEngine?: () => VoicePlaybackEngine | Promise<VoicePlaybackEngine>;
5076
+ /** See `voiceRecognition.provider.runtype.prewarmMode`. @default 'request' */
5077
+ prewarmMode?: 'request' | 'attach';
5078
+ /** See `voiceRecognition.provider.runtype.attachIdleMs`. @default 30000 */
5079
+ attachIdleMs?: number;
4990
5080
  /** @deprecated No-op on the realtime path: the server's STT owns turn-taking. */
4991
5081
  pauseDuration?: number;
4992
5082
  /** @deprecated No-op on the realtime path: the server's STT owns turn-taking. */
@@ -5058,6 +5148,14 @@ export interface VoiceProvider {
5058
5148
 
5059
5149
  /** Stop playback / cancel in-flight request without starting recording */
5060
5150
  stopPlayback?(): void;
5151
+
5152
+ /**
5153
+ * Warm the provider ahead of `startListening()` (the widget calls it on mic
5154
+ * intent, see `voiceRecognition.prewarm`). Must be fire-and-forget: never
5155
+ * throw, emit errors or status, or acquire the microphone. May be called
5156
+ * repeatedly; throttle as needed.
5157
+ */
5158
+ prewarm?(): void;
5061
5159
  }
5062
5160
 
5063
5161
  /**
@@ -5443,7 +5541,8 @@ export type AgentWidgetResolvedSuggestion = {
5443
5541
 
5444
5542
  /** Presentation keys shared by every suggestion surface. */
5445
5543
  export type AgentWidgetSuggestionSurfaceConfig = {
5446
- /** Item density. Defaults to `card` for starters and `chip` for follow-ups. */
5544
+ /** Item density. V4 starters: card. V5 starters: chip, or card in fullscreen.
5545
+ * Follow-ups default to chip. */
5447
5546
  variant?: AgentWidgetSuggestionVariant;
5448
5547
  /** Default click behavior, overridable per item. Defaults to `send`. */
5449
5548
  behavior?: AgentWidgetSuggestionBehavior;
@@ -5454,7 +5553,7 @@ export type AgentWidgetSuggestionSurfaceConfig = {
5454
5553
  * own stacking.
5455
5554
  */
5456
5555
  overflow?: "scroll" | "wrap";
5457
- /** Maximum visible items. Defaults to 4. */
5556
+ /** Maximum visible items. Defaults to 4, or 3 for V5 non-fullscreen starters. */
5458
5557
  maxItems?: number;
5459
5558
  };
5460
5559
 
@@ -5518,8 +5617,8 @@ export type AgentWidgetWelcomeVariant = "card" | "hero" | "none";
5518
5617
 
5519
5618
  /**
5520
5619
  * When the welcome surface goes away. `"never"` is the default for `"card"`;
5521
- * `"on-first-message"` is the default for `"hero"` and cannot be overridden
5522
- * there.
5620
+ * `"on-first-message"` is forced for legacy V4 `"hero"`. V5 defaults every
5621
+ * variant to `"on-first-message"`. Explicit layout opts into overridable dismissal.
5523
5622
  */
5524
5623
  export type AgentWidgetWelcomeDismiss = "never" | "on-first-message";
5525
5624
 
@@ -5571,7 +5670,10 @@ export type AgentWidgetWelcomeAlign = "start" | "center";
5571
5670
  * host-set value from a filled-in default.
5572
5671
  */
5573
5672
  export interface AgentWidgetWelcomeConfig {
5574
- /** Card or hero title. Defaults to "Hello 👋". */
5673
+ /** Vertical placement: V4 defaults to top; V5 to centered. Fullscreen centered
5674
+ * welcomes also raise the empty composer unless `anchor` is explicitly set. */
5675
+ layout?: "top" | "centered";
5676
+ /** Card or hero title. V4: "Hello 👋". V5: "What can I help with?". */
5575
5677
  title?: string;
5576
5678
  /**
5577
5679
  * Small muted line above the title, e.g. a section name. Omitted when unset
@@ -5584,19 +5686,22 @@ export interface AgentWidgetWelcomeConfig {
5584
5686
  icon?: AgentWidgetWelcomeIcon;
5585
5687
  /**
5586
5688
  * Horizontal alignment of the kicker, title, subtitle, and starter
5587
- * suggestions. Unset follows the variant: `card` starts, `hero` centers.
5689
+ * suggestions. Centered layouts default to center. Otherwise unset follows
5690
+ * the variant: `card` starts, `hero` centers.
5588
5691
  */
5589
5692
  align?: AgentWidgetWelcomeAlign;
5590
5693
  /** @default "card" */
5591
5694
  variant?: AgentWidgetWelcomeVariant;
5592
- /** @default "never" for `card`, always `"on-first-message"` for `hero`. */
5695
+ /** V4: never for card, forced on-first-message for hero. V5: on-first-message
5696
+ * for all variants; explicit values win. Explicit layout also allows overriding
5697
+ * the legacy hero dismissal in V4. */
5593
5698
  dismiss?: AgentWidgetWelcomeDismiss;
5594
5699
  /**
5595
5700
  * Vertical anchoring of the greeting-plus-composer pair in the EMPTY
5596
5701
  * conversation only. On the first message the pair drops to the bottom
5597
5702
  * (animated; see `data-persona-conversation-state`).
5598
5703
  *
5599
- * - `"bottom"` (default): the composer stays pinned at the bottom.
5704
+ * - `"bottom"`: the composer stays pinned at the bottom.
5600
5705
  * - `"center"`: greeting and composer float together so the composer's top
5601
5706
  * edge lands at `anchorComposerTop` of the panel column, and the welcome
5602
5707
  * surface is end-anchored `composerGap` above it.
@@ -5604,7 +5709,8 @@ export interface AgentWidgetWelcomeConfig {
5604
5709
  * Composes with either `composer.placement`. Ignored in composer-bar mount
5605
5710
  * mode. `renderWelcome` plugin content inherits the anchor: the widget
5606
5711
  * positions the overlay host and reserves the composer zone.
5607
- * @default "bottom"
5712
+ * Defaults to bottom, except centered fullscreen layouts, which center the
5713
+ * measured greeting/composer group when no anchor override is supplied.
5608
5714
  */
5609
5715
  anchor?: AgentWidgetWelcomeAnchor;
5610
5716
  /**
@@ -5941,6 +6047,8 @@ export interface WidgetHistoryInternals {
5941
6047
  */
5942
6048
  export type ClientChatRequest = {
5943
6049
  sessionId: string;
6050
+ /** Fresh identity proof for this execution, independent of visitor history. */
6051
+ identityProof?: RuntypeClientChatRequest['identityProof'];
5944
6052
  messages: Array<{
5945
6053
  id?: string;
5946
6054
  role: 'user' | 'assistant' | 'system';
@@ -6323,6 +6431,9 @@ export type AgentWidgetLayoutConfig = {
6323
6431
  * `launcher.composerBar.contentMaxWidth`)
6324
6432
  */
6325
6433
  contentMaxWidth?: string;
6434
+ /** Fade the transcript's top edge when content is scrolled above it.
6435
+ * Defaults to false in V4 and true with future.v5Defaults. */
6436
+ topFade?: boolean;
6326
6437
  };
6327
6438
 
6328
6439
  // ============================================================================
@@ -6951,7 +7062,32 @@ export type AgentWidgetLoadingIndicatorConfig = {
6951
7062
  renderIdle?: (context: IdleIndicatorRenderContext) => HTMLElement | null;
6952
7063
  };
6953
7064
 
7065
+ /** Built-in `sessionInit` triggers. See {@link AgentWidgetConfig.sessionInit}. */
7066
+ export type AgentWidgetSessionInitTrigger = 'input' | 'focus' | 'mount' | 'send';
7067
+
7068
+ /**
7069
+ * Custom `sessionInit` trigger. Runs when the widget mounts in client token mode,
7070
+ * and again if `update()` swaps in a different function. Call `warm()` whenever
7071
+ * the visitor shows intent (it is safe to call repeatedly). Return a cleanup to
7072
+ * run when the widget is destroyed or the hook is replaced.
7073
+ */
7074
+ export type AgentWidgetSessionInitHook = (context: {
7075
+ /** Fire-and-forget early session init; latched and error-swallowing. */
7076
+ warm: () => void;
7077
+ /** The widget's mount element. */
7078
+ mount: HTMLElement;
7079
+ }) => void | (() => void);
7080
+
6954
7081
  export type AgentWidgetConfig = {
7082
+ /** Opt in to upcoming major-version defaults without changing explicit settings. */
7083
+ future?: {
7084
+ /**
7085
+ * Use the v5 default styling as it rolls out in 4.x. Every affected setting
7086
+ * remains independently configurable. Explicit options and theme tokens win.
7087
+ * @default false
7088
+ */
7089
+ v5Defaults?: boolean;
7090
+ };
6955
7091
  apiUrl?: string;
6956
7092
  flowId?: string;
6957
7093
  /**
@@ -7106,6 +7242,46 @@ export type AgentWidgetConfig = {
7106
7242
  * ```
7107
7243
  */
7108
7244
  onSessionExpired?: () => void;
7245
+ /**
7246
+ * When the widget calls `/v1/client/init` in client token mode, ahead of the
7247
+ * first send. Ignored outside client token mode.
7248
+ *
7249
+ * - `'input'` (default): on the composer's first user keystroke that leaves
7250
+ * non-empty text. Programmatic value changes (restored drafts,
7251
+ * `setMessage()`, history recall) do not count.
7252
+ * - `'focus'`: when the visitor focuses the composer by pointer or Tab (and
7253
+ * on first input, as a fallback). Programmatic focus, such as
7254
+ * `autoFocusInput` on open or mount, does not count.
7255
+ * - `'mount'`: as soon as the widget mounts (the behavior before this option
7256
+ * existed). Use it when a server-configured welcome message, which arrives
7257
+ * with the init response, must show before the visitor types.
7258
+ * - `'send'`: no early init; the first send initializes the session.
7259
+ * - A function: custom trigger. Called on mount (and when `update()` swaps
7260
+ * in a different function) with a `warm` callback to call whenever you
7261
+ * decide (e.g. on hover); may return a cleanup that runs on destroy or
7262
+ * replacement. Replaces the built-in triggers.
7263
+ *
7264
+ * The early init is fire-and-forget: it runs at most once per session
7265
+ * lifetime (re-armed after expiry or a new conversation), sends exactly the
7266
+ * same request the send would, and swallows errors. A failure resurfaces
7267
+ * when the send retries init. A send during an in-flight early init reuses
7268
+ * it, so only one `/v1/client/init` is issued. When visitor history is
7269
+ * enabled the widget still initializes on mount to restore the conversation.
7270
+ * Hosts can also trigger it imperatively with `controller.warmSession()`.
7271
+ *
7272
+ * @default 'input'
7273
+ *
7274
+ * @example
7275
+ * ```typescript
7276
+ * config: {
7277
+ * clientToken: 'ct_live_...',
7278
+ * sessionInit: ({ warm, mount }) => {
7279
+ * mount.addEventListener('pointerenter', warm, { once: true });
7280
+ * }
7281
+ * }
7282
+ * ```
7283
+ */
7284
+ sessionInit?: AgentWidgetSessionInitTrigger | AgentWidgetSessionInitHook;
7109
7285
  /**
7110
7286
  * Get stored session ID for session resumption (client token mode only).
7111
7287
  * Called when initializing a new session to check if there's a previous session_id
@@ -7138,13 +7314,20 @@ export type AgentWidgetConfig = {
7138
7314
  */
7139
7315
  setStoredSessionId?: (sessionId: string) => void;
7140
7316
  /**
7141
- * Called immediately before any cross-device history request and before a
7317
+ * With identityProvider, called before every client-token chat POST, including retries, independently of history.
7318
+ * Also called immediately before any cross-device history request and before a
7142
7319
  * conversationId resume of a conversation discovered on another device.
7143
- * Return a fresh end-user access token (rt_eu_… / JWT). Null uses exact-browser
7144
- * scope only for a never-bound visitor; a previously bound visitor fails closed
7145
- * until resetHistoryIdentity() runs. Never cached by the widget.
7320
+ * Return a fresh end-user access token (rt_eu_… / JWT). Null stops identity-enabled chat.
7321
+ * For history only, null uses exact-browser scope for a never-bound visitor;
7322
+ * a bound visitor fails closed until resetHistoryIdentity(). Never cached.
7146
7323
  */
7147
7324
  getIdentityProof?: () => string | null | Promise<string | null>;
7325
+ /**
7326
+ * Provider name registered with Runtype, for example "clerk". Enables per-request
7327
+ * chat identity through getIdentityProof without enabling history. A missing or
7328
+ * failed proof stops the turn; omitting this preserves history-only callbacks.
7329
+ */
7330
+ identityProvider?: string;
7148
7331
  /**
7149
7332
  * Read the persisted active conversation id (client token mode only).
7150
7333
  * Called only after the internal stored-state bootstrap gate resolves.
@@ -7840,6 +8023,8 @@ export type AgentWidgetReasoning = {
7840
8023
  };
7841
8024
 
7842
8025
  export type AgentWidgetToolCall = {
8026
+ /** Optional approval state for activity-row status chrome. */
8027
+ approvalStatus?: "pending" | "approved" | "denied" | "timeout";
7843
8028
  id: string;
7844
8029
  name?: string;
7845
8030
  status: "pending" | "running" | "complete";
@@ -0,0 +1,91 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { createAgentExperience, type AgentWidgetController } from './ui';
4
+ import type { AgentWidgetConfig, AgentWidgetMessage } from './types';
5
+ let controller: AgentWidgetController;
6
+ let mount: HTMLElement;
7
+ beforeEach(() => { window.localStorage.clear(); window.scrollTo = vi.fn(); mount = document.createElement('div'); document.body.appendChild(mount); });
8
+ afterEach(() => { controller?.destroy(); document.body.replaceChildren(); vi.useRealTimers(); });
9
+ const inject = (id: string, status: 'running' | 'complete', chunks: string[] = []) => controller.injectTestMessage({ type: 'message', message: { id, role: 'assistant', content: '', createdAt: '2026-01-01T00:00:00Z', sequence: Number(id.slice(-1)), variant: 'tool', toolCall: { id, name: 'Search', status, chunks } } });
10
+ const header = (id: string) => mount.querySelector<HTMLButtonElement>(`[data-message-id="${id}"] > button`)!;
11
+ const start = (v5Defaults: boolean, toolCallDisplay: NonNullable<AgentWidgetConfig['features']>['toolCallDisplay']) => { controller = createAgentExperience(mount, { future: { v5Defaults }, apiUrl: '/test', persistState: false, launcher: { enabled: false }, features: { toolCallDisplay } }); };
12
+ describe.each([false, true])('activity UI (V5 %s)', v5Defaults => {
13
+ it('uses version defaults for streaming tool and reasoning disclosure', () => {
14
+ start(v5Defaults, { variant: 'row', grouped: false });
15
+ inject('tool1', 'running', ['one']);
16
+ expect(header('tool1').getAttribute('aria-expanded')).toBe(String(!v5Defaults));
17
+ if (v5Defaults) {
18
+ inject('tool1', 'running', ['one', 'two']);
19
+ expect(header('tool1').getAttribute('aria-expanded')).toBe('false');
20
+ header('tool1').click();
21
+ expect(header('tool1').getAttribute('aria-expanded')).toBe('true');
22
+ }
23
+ controller.destroy();
24
+ controller = createAgentExperience(mount, { future: { v5Defaults }, apiUrl: '/test', persistState: false, launcher: { enabled: false }, features: { reasoningDisplay: { variant: 'row' } } });
25
+ controller.injectTestMessage({ type: 'message', message: { id: 'r1', role: 'assistant', content: '', createdAt: '2026-01-01', variant: 'reasoning', reasoning: { id: 'r1', status: 'streaming', chunks: ['Thinking'] } } });
26
+ expect(header('r1').getAttribute('aria-expanded')).toBe(String(!v5Defaults));
27
+ });
28
+ it('opens on first token, collapses once, and preserves a manual decision', () => {
29
+ start(v5Defaults, { variant: 'row', grouped: false, autoExpand: true }); vi.useFakeTimers();
30
+ inject('tool1', 'running'); expect(header('tool1').getAttribute('aria-expanded')).toBe('false');
31
+ inject('tool1', 'running', ['one']); expect(header('tool1').getAttribute('aria-expanded')).toBe('true');
32
+ inject('tool1', 'complete', ['one']); vi.advanceTimersByTime(1000); expect(header('tool1').getAttribute('aria-expanded')).toBe('false');
33
+ header('tool1').click(); expect(header('tool1').getAttribute('aria-expanded')).toBe('true');
34
+ inject('tool1', 'complete', ['one']); vi.advanceTimersByTime(2000); expect(header('tool1').getAttribute('aria-expanded')).toBe('true');
35
+ });
36
+ it('cancels pending collapse on clear and supports reused message IDs', () => {
37
+ start(v5Defaults, { variant: 'row', grouped: false, autoExpand: true }); vi.useFakeTimers();
38
+ inject('tool1', 'running', ['one']); inject('tool1', 'complete', ['one']); controller.clearChat();
39
+ inject('tool1', 'running', ['new']); vi.advanceTimersByTime(1100); expect(header('tool1').getAttribute('aria-expanded')).toBe('true');
40
+ controller.destroy(); vi.advanceTimersByTime(1100);
41
+ });
42
+ it('groups consecutive calls with a collapsible label and staggered children', () => {
43
+ start(v5Defaults, { variant: 'row', grouped: true, groupedMode: 'collapsible' });
44
+ inject('tool1', 'complete', ['one']); inject('tool2', 'complete', ['two']);
45
+ const group = mount.querySelector('[data-persona-tool-group]')!;
46
+ expect(group.querySelector('button')?.textContent).toContain('Used 2 tools');
47
+ const children = group.querySelectorAll<HTMLElement>('.persona-activity-group-child'); expect(children).toHaveLength(2);
48
+ expect(children[1].style.getPropertyValue('--persona-activity-stagger')).toBe('40ms');
49
+ const chevron = () => header('tool-group-tool1').querySelector('.persona-activity-chevron svg')!.outerHTML;
50
+ const collapsedChevron = chevron();
51
+ expect(header('tool-group-tool1').querySelector('svg[stroke="currentColor"]')).not.toBeNull();
52
+ header('tool-group-tool1').click(); expect(header('tool-group-tool1').getAttribute('aria-expanded')).toBe('true');
53
+ expect(chevron()).not.toBe(collapsedChevron);
54
+ header('tool-group-tool1').click();
55
+ expect(header('tool-group-tool1').getAttribute('aria-expanded')).toBe('false');
56
+ expect(chevron()).toBe(collapsedChevron);
57
+ controller.injectTestMessage({ type: 'message', message: { id: 'text3', sequence: 3, role: 'assistant', content: 'Break', createdAt: '2026-01-01T00:00:00Z' } });
58
+ inject('tool4', 'complete', ['four']); expect(mount.querySelectorAll('[data-persona-tool-group]')).toHaveLength(1);
59
+ });
60
+ it('honors explicit static display and content renderers', () => {
61
+ start(v5Defaults, { variant: 'row', grouped: false, expandable: false });
62
+ inject('tool1', 'running', ['one']); expect(header('tool1').hasAttribute('data-expand-header')).toBe(false);
63
+ });
64
+ it('uses the same first-token lifecycle for reasoning', () => {
65
+ controller = createAgentExperience(mount, { future: { v5Defaults }, apiUrl: '/test', persistState: false, launcher: { enabled: false }, features: { reasoningDisplay: { variant: 'row', autoExpand: true, autoCollapseDelay: 50 } } });
66
+ vi.useFakeTimers();
67
+ const m: AgentWidgetMessage = { id: 'r1', role: 'assistant', content: '', createdAt: '2026-01-01', variant: 'reasoning', reasoning: { id: 'r1', status: 'streaming', chunks: ['Thinking'] } };
68
+ controller.injectTestMessage({ type: 'message', message: m }); expect(header('r1').getAttribute('aria-expanded')).toBe('true');
69
+ controller.injectTestMessage({ type: 'message', message: { ...m, reasoning: { ...m.reasoning!, status: 'complete' } } });
70
+ vi.advanceTimersByTime(50); expect(header('r1').getAttribute('aria-expanded')).toBe('false');
71
+ });
72
+ });
73
+
74
+ it('copies current tool output after a streaming DOM update', async () => {
75
+ start(true, { grouped: false });
76
+ const writeText = vi.fn().mockResolvedValue(undefined);
77
+ const original = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
78
+ Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } });
79
+ try {
80
+ inject('tool1', 'running', ['first']);
81
+ header('tool1').click();
82
+ inject('tool1', 'complete', ['updated output']);
83
+ mount.querySelector<HTMLButtonElement>('[data-persona-copy-tool-detail]')!.click();
84
+ await Promise.resolve();
85
+ expect(writeText).toHaveBeenCalledWith('updated output');
86
+ expect(header('tool1').getAttribute('aria-expanded')).toBe('true');
87
+ } finally {
88
+ if (original) Object.defineProperty(navigator, 'clipboard', original);
89
+ else Reflect.deleteProperty(navigator, 'clipboard');
90
+ }
91
+ });
@@ -20,6 +20,18 @@ const createMount = () => {
20
20
  return mount;
21
21
  };
22
22
 
23
+ const approvalDetails = (mount: HTMLElement): HTMLElement => {
24
+ const details = mount.querySelector<HTMLElement>('[data-role="params"]');
25
+ expect(details).not.toBeNull();
26
+ return details!;
27
+ };
28
+
29
+ const toggleApprovalDetails = (mount: HTMLElement): void => {
30
+ const toggle = mount.querySelector<HTMLElement>('[data-action="toggle-params"]');
31
+ expect(toggle).not.toBeNull();
32
+ toggle!.click();
33
+ };
34
+
23
35
  const injectApproval = (
24
36
  controller: ReturnType<typeof createAgentExperience>,
25
37
  {
@@ -210,4 +222,55 @@ describe("renderApproval plugin hook", () => {
210
222
  expect(mount.querySelector('[data-test-id="custom-pending"]')).toBeNull();
211
223
  expect(mount.querySelector(".persona-approval-bubble")).not.toBeNull();
212
224
  });
225
+
226
+ it("keeps approval disclosure expansion separate for widgets with matching message ids", () => {
227
+ const firstMount = createMount();
228
+ const first = createAgentExperience(firstMount, {
229
+ apiUrl: "https://api.example.com/chat",
230
+ launcher: { enabled: false },
231
+ } as unknown as Parameters<typeof createAgentExperience>[1]);
232
+ injectApproval(first, { id: "shared" });
233
+ toggleApprovalDetails(firstMount);
234
+ expect(approvalDetails(firstMount).hidden).toBe(false);
235
+
236
+ // The transcript derives DOM ids from the message id. Keep both widgets
237
+ // mounted while removing only the first rendered row ids so the fixture
238
+ // can model equal message ids without duplicate document ids.
239
+ firstMount.querySelectorAll("[id]").forEach((element) => element.removeAttribute("id"));
240
+
241
+ const secondMount = createMount();
242
+ const second = createAgentExperience(secondMount, {
243
+ apiUrl: "https://api.example.com/chat",
244
+ launcher: { enabled: false },
245
+ } as unknown as Parameters<typeof createAgentExperience>[1]);
246
+ injectApproval(second, { id: "shared" });
247
+
248
+ expect(approvalDetails(secondMount).hidden).toBe(true);
249
+ first.destroy();
250
+ second.destroy();
251
+ });
252
+
253
+ it("resets approval disclosure expansion on clearChat and destroy", () => {
254
+ const mount = createMount();
255
+ const controller = createAgentExperience(mount, {
256
+ apiUrl: "https://api.example.com/chat",
257
+ launcher: { enabled: false },
258
+ } as unknown as Parameters<typeof createAgentExperience>[1]);
259
+ injectApproval(controller, { id: "after-clear" });
260
+ toggleApprovalDetails(mount);
261
+ expect(approvalDetails(mount).hidden).toBe(false);
262
+ controller.clearChat();
263
+ injectApproval(controller, { id: "after-clear" });
264
+ expect(approvalDetails(mount).hidden).toBe(true);
265
+ controller.destroy();
266
+
267
+ const replacementMount = createMount();
268
+ const replacement = createAgentExperience(replacementMount, {
269
+ apiUrl: "https://api.example.com/chat",
270
+ launcher: { enabled: false },
271
+ } as unknown as Parameters<typeof createAgentExperience>[1]);
272
+ injectApproval(replacement, { id: "after-clear" });
273
+ expect(approvalDetails(replacementMount).hidden).toBe(true);
274
+ replacement.destroy();
275
+ });
213
276
  });