@runtypelabs/persona 4.23.1 → 4.24.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 (158) 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-9Vxm-PPz.d.cts → types-Db61aI9J.d.cts} +25 -2
  4. package/dist/animations/{types-9Vxm-PPz.d.ts → types-Db61aI9J.d.ts} +25 -2
  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 +25 -2
  8. package/dist/animations-extra.d.ts +25 -2
  9. package/dist/approval-ui-3NMYNXBD.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +261 -40
  12. package/dist/approval-ui.d.ts +261 -40
  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 +256 -28
  16. package/dist/artifacts-ui.d.ts +256 -28
  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 +256 -28
  21. package/dist/context-mentions.d.ts +256 -28
  22. package/dist/event-stream-view.d.cts +256 -28
  23. package/dist/event-stream-view.d.ts +256 -28
  24. package/dist/forms-ui.d.cts +291 -30
  25. package/dist/forms-ui.d.ts +291 -30
  26. package/dist/history-view.d.cts +25 -2
  27. package/dist/history-view.d.ts +25 -2
  28. package/dist/index.cjs +70 -69
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +337 -51
  31. package/dist/index.d.ts +337 -51
  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 +256 -28
  41. package/dist/session-reconnect.d.ts +256 -28
  42. package/dist/smart-dom-reader.d.cts +256 -28
  43. package/dist/smart-dom-reader.d.ts +256 -28
  44. package/dist/theme-editor-preview.cjs +74 -73
  45. package/dist/theme-editor-preview.d.cts +263 -28
  46. package/dist/theme-editor-preview.d.ts +263 -28
  47. package/dist/theme-editor-preview.js +51 -50
  48. package/dist/theme-editor.cjs +10 -10
  49. package/dist/theme-editor.d.cts +276 -30
  50. package/dist/theme-editor.d.ts +276 -30
  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/widget.css +1 -1
  57. package/package.json +3 -2
  58. package/src/__fixtures__/defaults-parity.v4.json +776 -0
  59. package/src/approval-ui-entry.ts +0 -1
  60. package/src/approval-ui-loader.ts +5 -4
  61. package/src/approval-ui.ts +0 -1
  62. package/src/client-identity-proof.test.ts +193 -0
  63. package/src/client-session-warm.test.ts +216 -0
  64. package/src/client-visitor-history.test.ts +6 -0
  65. package/src/client.tool-pair-replay.test.ts +325 -0
  66. package/src/client.ts +154 -61
  67. package/src/codegen.test.ts +24 -0
  68. package/src/components/activity-row.test.ts +100 -0
  69. package/src/components/activity-row.ts +136 -0
  70. package/src/components/approval-actions.test.ts +8 -5
  71. package/src/components/approval-actions.ts +19 -6
  72. package/src/components/approval-bubble.test.ts +56 -9
  73. package/src/components/approval-bubble.ts +36 -41
  74. package/src/components/composer-builder.ts +5 -2
  75. package/src/components/composer-parts.ts +10 -2
  76. package/src/components/header-builder.test.ts +32 -0
  77. package/src/components/header-builder.ts +7 -4
  78. package/src/components/header-layouts.test.ts +12 -0
  79. package/src/components/header-layouts.ts +3 -2
  80. package/src/components/launcher.ts +11 -2
  81. package/src/components/message-bubble.test.ts +22 -0
  82. package/src/components/message-bubble.ts +7 -3
  83. package/src/components/panel.test.ts +52 -1
  84. package/src/components/panel.ts +47 -8
  85. package/src/components/pill-composer-builder.test.ts +2 -0
  86. package/src/components/pill-composer-builder.ts +0 -1
  87. package/src/components/reasoning-bubble.ts +29 -17
  88. package/src/components/suggestions.test.ts +1 -0
  89. package/src/components/suggestions.ts +0 -16
  90. package/src/components/tool-bubble.ts +131 -114
  91. package/src/components/tool-details.test.ts +87 -0
  92. package/src/components/tool-details.ts +102 -0
  93. package/src/components/welcome.test.ts +8 -0
  94. package/src/components/welcome.ts +4 -2
  95. package/src/defaults-parity.test.ts +158 -0
  96. package/src/defaults.test.ts +69 -0
  97. package/src/defaults.ts +128 -49
  98. package/src/generated/runtype-openapi-contract.ts +28 -13
  99. package/src/index-core.ts +2 -0
  100. package/src/install.test.ts +24 -0
  101. package/src/install.ts +3 -1
  102. package/src/launcher-global.ts +2 -1
  103. package/src/launcher-variants.test.ts +52 -0
  104. package/src/runtime/init-update-reset.test.ts +21 -0
  105. package/src/session.ts +61 -9
  106. package/src/styles/widget-styles.test.ts +19 -0
  107. package/src/styles/widget.css +295 -23
  108. package/src/theme-editor/index.ts +2 -0
  109. package/src/theme-editor/preview-utils.test.ts +8 -0
  110. package/src/theme-editor/preview-utils.ts +7 -3
  111. package/src/theme-editor/sections.test.ts +31 -0
  112. package/src/theme-editor/sections.ts +55 -1
  113. package/src/theme-editor/state-preferences.test.ts +49 -0
  114. package/src/theme-editor/state.ts +73 -15
  115. package/src/theme-reference.test.ts +20 -0
  116. package/src/theme-reference.ts +11 -5
  117. package/src/tool-call-display-defaults.test.ts +33 -1
  118. package/src/types/theme.ts +80 -3
  119. package/src/types.ts +179 -23
  120. package/src/ui.activity-row.test.ts +91 -0
  121. package/src/ui.approval-plugin.test.ts +63 -0
  122. package/src/ui.composer-compact.test.ts +24 -2
  123. package/src/ui.composer-placement.test.ts +23 -0
  124. package/src/ui.detached-panel.test.ts +33 -6
  125. package/src/ui.docked.test.ts +19 -0
  126. package/src/ui.header-update-stability.test.ts +25 -0
  127. package/src/ui.identity-proof.test.ts +58 -0
  128. package/src/ui.launcher-teaser.test.ts +25 -0
  129. package/src/ui.message-width.test.ts +17 -2
  130. package/src/ui.reasoning-display.test.ts +92 -0
  131. package/src/ui.rebuild-composer.test.ts +20 -3
  132. package/src/ui.scroll.test.ts +32 -0
  133. package/src/ui.session-init.test.ts +312 -0
  134. package/src/ui.status-indicator.test.ts +82 -0
  135. package/src/ui.suggest-replies.test.ts +11 -0
  136. package/src/ui.ts +363 -74
  137. package/src/ui.v5-defaults.test.ts +144 -0
  138. package/src/ui.welcome.test.ts +92 -3
  139. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
  140. package/src/utils/code-generators.ts +14 -0
  141. package/src/utils/composer-compact.test.ts +0 -2
  142. package/src/utils/composer-compact.ts +2 -4
  143. package/src/utils/composer-placement.ts +2 -3
  144. package/src/utils/config-merge.test.ts +81 -5
  145. package/src/utils/config-merge.ts +13 -3
  146. package/src/utils/defaults-provenance.ts +67 -0
  147. package/src/utils/dock.ts +7 -1
  148. package/src/utils/launcher-variant.ts +16 -0
  149. package/src/utils/panel-config.ts +97 -0
  150. package/src/utils/status-indicator.ts +39 -0
  151. package/src/utils/theme.test.ts +182 -0
  152. package/src/utils/theme.ts +131 -18
  153. package/src/utils/tokens.test.ts +122 -1
  154. package/src/utils/tokens.ts +177 -17
  155. package/src/utils/tool-pair-replay.ts +100 -0
  156. package/src/welcome.test.ts +23 -0
  157. package/src/welcome.ts +16 -9
  158. package/dist/approval-ui-K7R5Q5OC.js +0 -1
package/src/types.ts CHANGED
@@ -593,10 +593,34 @@ export type AgentWidgetContextMentionTokenRenderContext = {
593
593
  readonly: boolean;
594
594
  };
595
595
 
596
+ /** One earlier client-tool call replayed to the model, in `/v1/dispatch` shape. */
597
+ export type AgentWidgetReplayedToolCall = {
598
+ toolCallId: string;
599
+ /** Model-facing (sanitized) name, e.g. `webmcp_search`. */
600
+ toolName: string;
601
+ args: Record<string, unknown>;
602
+ };
603
+
604
+ /** The browser's answer to an {@link AgentWidgetReplayedToolCall}. */
605
+ export type AgentWidgetReplayedToolResult = {
606
+ toolCallId: string;
607
+ toolName: string;
608
+ result: unknown;
609
+ };
610
+
611
+ /**
612
+ * A message in the proxy/agent request payload. Earlier client-tool calls the
613
+ * browser answered replay as an assistant message carrying `toolCalls`,
614
+ * followed by a `tool` message carrying the matching `toolResults`; both have
615
+ * empty `content`. Client-token mode never sends them: the server replays
616
+ * from its own stored transcript there.
617
+ */
596
618
  export type AgentWidgetRequestPayloadMessage = {
597
- role: AgentWidgetMessageRole;
619
+ role: AgentWidgetMessageRole | "tool";
598
620
  content: MessageContent;
599
621
  createdAt: string;
622
+ toolCalls?: AgentWidgetReplayedToolCall[];
623
+ toolResults?: AgentWidgetReplayedToolResult[];
600
624
  };
601
625
 
602
626
  export type AgentWidgetRequestPayload = {
@@ -1032,6 +1056,24 @@ export type AgentMessageMetadata = {
1032
1056
  * never re-resume the call.
1033
1057
  */
1034
1058
  suggestRepliesResolved?: boolean;
1059
+ /**
1060
+ * The client-tool answer the server accepted through `/resume`, kept so
1061
+ * later proxy/agent turns can replay the call and its result to the model.
1062
+ * `toolName` is the widget's internal name (`webmcp:<name>` or a bare
1063
+ * built-in). Set only after the resume succeeds; preserved across
1064
+ * re-emissions of the tool message.
1065
+ */
1066
+ clientToolAnswer?: {
1067
+ toolCallId: string;
1068
+ toolName: string;
1069
+ args: unknown;
1070
+ result: unknown;
1071
+ /**
1072
+ * Shared by the answers posted in one `/resume`, i.e. one assistant
1073
+ * turn's parallel calls; a chained call gets a new batch.
1074
+ */
1075
+ batch?: string;
1076
+ };
1035
1077
  };
1036
1078
 
1037
1079
  export type AgentWidgetRequestMiddlewareContext = {
@@ -2212,6 +2254,14 @@ export type AgentWidgetToolCallLoadingAnimation =
2212
2254
  | "rainbow";
2213
2255
 
2214
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;
2215
2265
  /**
2216
2266
  * Controls what collapsed tool call rows show in their header/summary area.
2217
2267
  * @default "tool-call"
@@ -2242,9 +2292,10 @@ export type AgentWidgetToolCallDisplayFeature = {
2242
2292
  * Controls how a grouped tool sequence is rendered.
2243
2293
  * - `"stack"`: show the group summary and each child tool row.
2244
2294
  * - `"summary"`: show one consolidated summary row only.
2295
+ * - `"collapsible"`: expandable activity group (V5 default).
2245
2296
  * @default "stack"
2246
2297
  */
2247
- groupedMode?: "stack" | "summary";
2298
+ groupedMode?: "stack" | "summary" | "collapsible";
2248
2299
  /**
2249
2300
  * What happens to a tool call row once it completes.
2250
2301
  *
@@ -2277,6 +2328,14 @@ export type AgentWidgetToolCallDisplayFeature = {
2277
2328
  export type AgentWidgetToolCallCompletedVisibility = "kept" | "removed";
2278
2329
 
2279
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;
2280
2339
  /**
2281
2340
  * When true, active collapsed reasoning rows can render a lightweight preview block.
2282
2341
  * @default false
@@ -3036,8 +3095,8 @@ export interface AgentWidgetHistoryFeature {
3036
3095
  * otherwise `"browser"`) and narrows it to what the provider advertises, so a
3037
3096
  * browser-only provider is never asked for a scope it cannot serve. An
3038
3097
  * explicit `features.history.scope` is a hard request and still fails closed
3039
- * with `unsupported_scope`. `getIdentityProof` is never called for a supplied
3040
- * 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;
3041
3100
  * report your own identity through `getIdentityStatus()` /
3042
3101
  * `subscribeIdentityStatus()`, which is what the scope banner and its retry
3043
3102
  * affordance read.
@@ -3441,7 +3500,7 @@ export type EventStreamConfig = {
3441
3500
  /**
3442
3501
  * Fields to extract from event payloads for description text.
3443
3502
  * The first matching field value is displayed after the badge.
3444
- * @default ["flowName", "stepName", "name", "tool", "toolName"]
3503
+ * @default ["flowName", "stepName", "reasoningText", "text", "name", "tool", "toolName"]
3445
3504
  */
3446
3505
  descriptionFields?: string[];
3447
3506
  /**
@@ -3660,6 +3719,8 @@ export type AgentWidgetComposerBarPeekConfig = {
3660
3719
  };
3661
3720
 
3662
3721
  export type AgentWidgetLauncherConfig = {
3722
+ /** Collapsed launcher shape. V4 defaults to pill; V5 defaults to circle. */
3723
+ variant?: "pill" | "circle";
3663
3724
  enabled?: boolean;
3664
3725
  title?: string;
3665
3726
  subtitle?: string;
@@ -4454,9 +4515,9 @@ export type AgentWidgetComposerConfig = {
4454
4515
  /**
4455
4516
  * Where the composer footer sits relative to the transcript.
4456
4517
  *
4457
- * - `"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.
4458
4519
  * Nothing scrolls behind it.
4459
- * - `"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
4460
4521
  * transcript scrolls behind it (the chatgpt.com / claude.ai model, where the
4461
4522
  * composer is sticky inside the scroller). The widget reserves the footer's
4462
4523
  * live height as bottom padding on the scroll body and on a plugin welcome
@@ -4468,7 +4529,7 @@ export type AgentWidgetComposerConfig = {
4468
4529
  * included) and `theme.components.input.backdropFilter`.
4469
4530
  *
4470
4531
  * Ignored in composer-bar mount mode, which owns its own geometry.
4471
- * @default "block"
4532
+ * @default "block" (V4), "overlay" (V5)
4472
4533
  */
4473
4534
  placement?: ComposerPlacement;
4474
4535
  /**
@@ -4482,8 +4543,9 @@ export type AgentWidgetComposerConfig = {
4482
4543
  *
4483
4544
  * `"single-row"` describes the IDLE composer only. It composes with the
4484
4545
  * compact state, so a wrapped draft, chips, attachment previews, a quote, a
4485
- * pending card, or live dictation all fall back to the stacked card, exactly
4486
- * 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.
4487
4549
  *
4488
4550
  * Ignored in composer-bar mount mode, whose pill is already one row.
4489
4551
  * @default "stacked"
@@ -4530,7 +4592,14 @@ export type AgentWidgetClearChatConfig = {
4530
4592
  };
4531
4593
 
4532
4594
  export type AgentWidgetStatusIndicatorConfig = {
4595
+ /** Show the indicator. Explicit false also hides transient connection states. */
4533
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";
4534
4603
  /** Text alignment. Default: 'right'. */
4535
4604
  align?: 'left' | 'center' | 'right';
4536
4605
  idleText?: string;
@@ -5401,7 +5470,8 @@ export type AgentWidgetResolvedSuggestion = {
5401
5470
 
5402
5471
  /** Presentation keys shared by every suggestion surface. */
5403
5472
  export type AgentWidgetSuggestionSurfaceConfig = {
5404
- /** Item density. Defaults to `card` for starters and `chip` for follow-ups. */
5473
+ /** Item density. V4 starters: card. V5 starters: chip, or card in fullscreen.
5474
+ * Follow-ups default to chip. */
5405
5475
  variant?: AgentWidgetSuggestionVariant;
5406
5476
  /** Default click behavior, overridable per item. Defaults to `send`. */
5407
5477
  behavior?: AgentWidgetSuggestionBehavior;
@@ -5412,7 +5482,7 @@ export type AgentWidgetSuggestionSurfaceConfig = {
5412
5482
  * own stacking.
5413
5483
  */
5414
5484
  overflow?: "scroll" | "wrap";
5415
- /** Maximum visible items. Defaults to 4. */
5485
+ /** Maximum visible items. Defaults to 4, or 3 for V5 non-fullscreen starters. */
5416
5486
  maxItems?: number;
5417
5487
  };
5418
5488
 
@@ -5476,8 +5546,8 @@ export type AgentWidgetWelcomeVariant = "card" | "hero" | "none";
5476
5546
 
5477
5547
  /**
5478
5548
  * When the welcome surface goes away. `"never"` is the default for `"card"`;
5479
- * `"on-first-message"` is the default for `"hero"` and cannot be overridden
5480
- * there.
5549
+ * `"on-first-message"` is forced for legacy V4 `"hero"`. V5 defaults every
5550
+ * variant to `"on-first-message"`. Explicit layout opts into overridable dismissal.
5481
5551
  */
5482
5552
  export type AgentWidgetWelcomeDismiss = "never" | "on-first-message";
5483
5553
 
@@ -5529,7 +5599,10 @@ export type AgentWidgetWelcomeAlign = "start" | "center";
5529
5599
  * host-set value from a filled-in default.
5530
5600
  */
5531
5601
  export interface AgentWidgetWelcomeConfig {
5532
- /** Card or hero title. Defaults to "Hello 👋". */
5602
+ /** Vertical placement: V4 defaults to top; V5 to centered. Fullscreen centered
5603
+ * welcomes also raise the empty composer unless `anchor` is explicitly set. */
5604
+ layout?: "top" | "centered";
5605
+ /** Card or hero title. V4: "Hello 👋". V5: "What can I help with?". */
5533
5606
  title?: string;
5534
5607
  /**
5535
5608
  * Small muted line above the title, e.g. a section name. Omitted when unset
@@ -5542,19 +5615,22 @@ export interface AgentWidgetWelcomeConfig {
5542
5615
  icon?: AgentWidgetWelcomeIcon;
5543
5616
  /**
5544
5617
  * Horizontal alignment of the kicker, title, subtitle, and starter
5545
- * suggestions. Unset follows the variant: `card` starts, `hero` centers.
5618
+ * suggestions. Centered layouts default to center. Otherwise unset follows
5619
+ * the variant: `card` starts, `hero` centers.
5546
5620
  */
5547
5621
  align?: AgentWidgetWelcomeAlign;
5548
5622
  /** @default "card" */
5549
5623
  variant?: AgentWidgetWelcomeVariant;
5550
- /** @default "never" for `card`, always `"on-first-message"` for `hero`. */
5624
+ /** V4: never for card, forced on-first-message for hero. V5: on-first-message
5625
+ * for all variants; explicit values win. Explicit layout also allows overriding
5626
+ * the legacy hero dismissal in V4. */
5551
5627
  dismiss?: AgentWidgetWelcomeDismiss;
5552
5628
  /**
5553
5629
  * Vertical anchoring of the greeting-plus-composer pair in the EMPTY
5554
5630
  * conversation only. On the first message the pair drops to the bottom
5555
5631
  * (animated; see `data-persona-conversation-state`).
5556
5632
  *
5557
- * - `"bottom"` (default): the composer stays pinned at the bottom.
5633
+ * - `"bottom"`: the composer stays pinned at the bottom.
5558
5634
  * - `"center"`: greeting and composer float together so the composer's top
5559
5635
  * edge lands at `anchorComposerTop` of the panel column, and the welcome
5560
5636
  * surface is end-anchored `composerGap` above it.
@@ -5562,7 +5638,8 @@ export interface AgentWidgetWelcomeConfig {
5562
5638
  * Composes with either `composer.placement`. Ignored in composer-bar mount
5563
5639
  * mode. `renderWelcome` plugin content inherits the anchor: the widget
5564
5640
  * positions the overlay host and reserves the composer zone.
5565
- * @default "bottom"
5641
+ * Defaults to bottom, except centered fullscreen layouts, which center the
5642
+ * measured greeting/composer group when no anchor override is supplied.
5566
5643
  */
5567
5644
  anchor?: AgentWidgetWelcomeAnchor;
5568
5645
  /**
@@ -5899,6 +5976,8 @@ export interface WidgetHistoryInternals {
5899
5976
  */
5900
5977
  export type ClientChatRequest = {
5901
5978
  sessionId: string;
5979
+ /** Fresh identity proof for this execution, independent of visitor history. */
5980
+ identityProof?: RuntypeClientChatRequest['identityProof'];
5902
5981
  messages: Array<{
5903
5982
  id?: string;
5904
5983
  role: 'user' | 'assistant' | 'system';
@@ -6281,6 +6360,9 @@ export type AgentWidgetLayoutConfig = {
6281
6360
  * `launcher.composerBar.contentMaxWidth`)
6282
6361
  */
6283
6362
  contentMaxWidth?: string;
6363
+ /** Fade the transcript's top edge when content is scrolled above it.
6364
+ * Defaults to false in V4 and true with future.v5Defaults. */
6365
+ topFade?: boolean;
6284
6366
  };
6285
6367
 
6286
6368
  // ============================================================================
@@ -6909,7 +6991,32 @@ export type AgentWidgetLoadingIndicatorConfig = {
6909
6991
  renderIdle?: (context: IdleIndicatorRenderContext) => HTMLElement | null;
6910
6992
  };
6911
6993
 
6994
+ /** Built-in `sessionInit` triggers. See {@link AgentWidgetConfig.sessionInit}. */
6995
+ export type AgentWidgetSessionInitTrigger = 'input' | 'focus' | 'mount' | 'send';
6996
+
6997
+ /**
6998
+ * Custom `sessionInit` trigger. Runs when the widget mounts in client token mode,
6999
+ * and again if `update()` swaps in a different function. Call `warm()` whenever
7000
+ * the visitor shows intent (it is safe to call repeatedly). Return a cleanup to
7001
+ * run when the widget is destroyed or the hook is replaced.
7002
+ */
7003
+ export type AgentWidgetSessionInitHook = (context: {
7004
+ /** Fire-and-forget early session init; latched and error-swallowing. */
7005
+ warm: () => void;
7006
+ /** The widget's mount element. */
7007
+ mount: HTMLElement;
7008
+ }) => void | (() => void);
7009
+
6912
7010
  export type AgentWidgetConfig = {
7011
+ /** Opt in to upcoming major-version defaults without changing explicit settings. */
7012
+ future?: {
7013
+ /**
7014
+ * Use the v5 default styling as it rolls out in 4.x. Every affected setting
7015
+ * remains independently configurable. Explicit options and theme tokens win.
7016
+ * @default false
7017
+ */
7018
+ v5Defaults?: boolean;
7019
+ };
6913
7020
  apiUrl?: string;
6914
7021
  flowId?: string;
6915
7022
  /**
@@ -7064,6 +7171,46 @@ export type AgentWidgetConfig = {
7064
7171
  * ```
7065
7172
  */
7066
7173
  onSessionExpired?: () => void;
7174
+ /**
7175
+ * When the widget calls `/v1/client/init` in client token mode, ahead of the
7176
+ * first send. Ignored outside client token mode.
7177
+ *
7178
+ * - `'input'` (default): on the composer's first user keystroke that leaves
7179
+ * non-empty text. Programmatic value changes (restored drafts,
7180
+ * `setMessage()`, history recall) do not count.
7181
+ * - `'focus'`: when the visitor focuses the composer by pointer or Tab (and
7182
+ * on first input, as a fallback). Programmatic focus, such as
7183
+ * `autoFocusInput` on open or mount, does not count.
7184
+ * - `'mount'`: as soon as the widget mounts (the behavior before this option
7185
+ * existed). Use it when a server-configured welcome message, which arrives
7186
+ * with the init response, must show before the visitor types.
7187
+ * - `'send'`: no early init; the first send initializes the session.
7188
+ * - A function: custom trigger. Called on mount (and when `update()` swaps
7189
+ * in a different function) with a `warm` callback to call whenever you
7190
+ * decide (e.g. on hover); may return a cleanup that runs on destroy or
7191
+ * replacement. Replaces the built-in triggers.
7192
+ *
7193
+ * The early init is fire-and-forget: it runs at most once per session
7194
+ * lifetime (re-armed after expiry or a new conversation), sends exactly the
7195
+ * same request the send would, and swallows errors. A failure resurfaces
7196
+ * when the send retries init. A send during an in-flight early init reuses
7197
+ * it, so only one `/v1/client/init` is issued. When visitor history is
7198
+ * enabled the widget still initializes on mount to restore the conversation.
7199
+ * Hosts can also trigger it imperatively with `controller.warmSession()`.
7200
+ *
7201
+ * @default 'input'
7202
+ *
7203
+ * @example
7204
+ * ```typescript
7205
+ * config: {
7206
+ * clientToken: 'ct_live_...',
7207
+ * sessionInit: ({ warm, mount }) => {
7208
+ * mount.addEventListener('pointerenter', warm, { once: true });
7209
+ * }
7210
+ * }
7211
+ * ```
7212
+ */
7213
+ sessionInit?: AgentWidgetSessionInitTrigger | AgentWidgetSessionInitHook;
7067
7214
  /**
7068
7215
  * Get stored session ID for session resumption (client token mode only).
7069
7216
  * Called when initializing a new session to check if there's a previous session_id
@@ -7096,13 +7243,20 @@ export type AgentWidgetConfig = {
7096
7243
  */
7097
7244
  setStoredSessionId?: (sessionId: string) => void;
7098
7245
  /**
7099
- * Called immediately before any cross-device history request and before a
7246
+ * With identityProvider, called before every client-token chat POST, including retries, independently of history.
7247
+ * Also called immediately before any cross-device history request and before a
7100
7248
  * conversationId resume of a conversation discovered on another device.
7101
- * Return a fresh end-user access token (rt_eu_… / JWT). Null uses exact-browser
7102
- * scope only for a never-bound visitor; a previously bound visitor fails closed
7103
- * until resetHistoryIdentity() runs. Never cached by the widget.
7249
+ * Return a fresh end-user access token (rt_eu_… / JWT). Null stops identity-enabled chat.
7250
+ * For history only, null uses exact-browser scope for a never-bound visitor;
7251
+ * a bound visitor fails closed until resetHistoryIdentity(). Never cached.
7104
7252
  */
7105
7253
  getIdentityProof?: () => string | null | Promise<string | null>;
7254
+ /**
7255
+ * Provider name registered with Runtype, for example "clerk". Enables per-request
7256
+ * chat identity through getIdentityProof without enabling history. A missing or
7257
+ * failed proof stops the turn; omitting this preserves history-only callbacks.
7258
+ */
7259
+ identityProvider?: string;
7106
7260
  /**
7107
7261
  * Read the persisted active conversation id (client token mode only).
7108
7262
  * Called only after the internal stored-state bootstrap gate resolves.
@@ -7798,6 +7952,8 @@ export type AgentWidgetReasoning = {
7798
7952
  };
7799
7953
 
7800
7954
  export type AgentWidgetToolCall = {
7955
+ /** Optional approval state for activity-row status chrome. */
7956
+ approvalStatus?: "pending" | "approved" | "denied" | "timeout";
7801
7957
  id: string;
7802
7958
  name?: string;
7803
7959
  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
  });
@@ -7,6 +7,8 @@ import { createAgentExperience } from "./ui";
7
7
  const mounts: HTMLElement[] = [];
8
8
  const controllers: ReturnType<typeof createAgentExperience>[] = [];
9
9
 
10
+ let selectedDefaults = false;
11
+
10
12
  const makeController = (config: Record<string, unknown> = {}) => {
11
13
  const mount = document.createElement("div");
12
14
  document.body.appendChild(mount);
@@ -15,6 +17,7 @@ const makeController = (config: Record<string, unknown> = {}) => {
15
17
  apiUrl: "https://api.example.com/chat",
16
18
  launcher: { enabled: false },
17
19
  persistState: false,
20
+ future: { v5Defaults: selectedDefaults },
18
21
  suggestionChips: [],
19
22
  ...config,
20
23
  } as unknown as Parameters<typeof createAgentExperience>[1]);
@@ -68,12 +71,17 @@ const capturingFetch = () =>
68
71
  }),
69
72
  }) as unknown as Response;
70
73
 
71
- describe("composer compact state", () => {
74
+ describe.each([false, true])("composer compact state (v5Defaults: %s)", (v5Defaults) => {
72
75
  beforeEach(() => {
76
+ selectedDefaults = v5Defaults;
73
77
  window.scrollTo = vi.fn();
74
78
  });
75
79
 
76
80
  afterEach(() => {
81
+ if (v5Defaults) for (const mount of mounts) {
82
+ expect(mount.style.getPropertyValue("--persona-input-radius")).toBe("9999px");
83
+ expect(mount.style.getPropertyValue("--persona-composer-control-size")).toBe("32px");
84
+ }
77
85
  controllers.splice(0).forEach((controller) => {
78
86
  try {
79
87
  controller.destroy();
@@ -149,7 +157,7 @@ describe("composer compact state", () => {
149
157
  expect(isCompact(mount)).toBe(true);
150
158
  });
151
159
 
152
- it("expands while dictation is active", async () => {
160
+ it("keeps recording compact until its text needs more space", async () => {
153
161
  class FakeRecognition {
154
162
  continuous = false;
155
163
  interimResults = false;
@@ -163,12 +171,26 @@ describe("composer compact state", () => {
163
171
  vi.stubGlobal("SpeechRecognition", FakeRecognition);
164
172
  const { mount, controller } = makeController({
165
173
  voiceRecognition: { enabled: true },
174
+ composer: { layout: "single-row" },
166
175
  });
167
176
  await flush();
168
177
  expect(isCompact(mount)).toBe(true);
169
178
  controller.startVoiceRecognition();
170
179
  await type(mount, "");
180
+ expect(isCompact(mount)).toBe(true);
181
+ const mic = mount.querySelector<HTMLElement>('[data-persona-composer-mic]')!;
182
+ expect(mic.dataset.state).toBe('recording');
183
+ expect(mic.getAttribute('aria-label')).toBe('Stop voice recognition');
184
+ await type(mount, "Short dictated text");
185
+ expect(isCompact(mount)).toBe(true);
186
+ await type(mount, "First line\nSecond line");
171
187
  expect(isCompact(mount)).toBe(false);
188
+ expect(mic.dataset.state).toBe('recording');
189
+ controller.stopVoiceRecognition();
190
+ expect(isCompact(mount)).toBe(false);
191
+ expect(mic.dataset.state).toBe('idle');
192
+ await type(mount, "");
193
+ expect(isCompact(mount)).toBe(true);
172
194
  vi.unstubAllGlobals();
173
195
  });
174
196
  });
@@ -175,3 +175,26 @@ describe("observer lifecycle", () => {
175
175
  expect(disconnect).toHaveBeenCalled();
176
176
  });
177
177
  });
178
+
179
+ describe.each([false, true])('transcript top fade (V5 %s)', v5Defaults => {
180
+ it('tracks scroll position, explicit overrides, and clear', () => {
181
+ const { mount, controller } = makeController({ future: { v5Defaults } });
182
+ const body = mount.querySelector<HTMLElement>('.persona-widget-body')!;
183
+ expect(body.hasAttribute('data-persona-top-fade')).toBe(false);
184
+ body.scrollTop = 80;
185
+ body.dispatchEvent(new Event('scroll'));
186
+ expect(body.hasAttribute('data-persona-top-fade')).toBe(v5Defaults);
187
+ controller.update({ layout: { topFade: true } });
188
+ expect(body.hasAttribute('data-persona-top-fade')).toBe(true);
189
+ controller.update({ layout: { topFade: false } });
190
+ expect(body.hasAttribute('data-persona-top-fade')).toBe(false);
191
+ controller.update({ layout: { topFade: true } });
192
+ body.scrollTop = 0;
193
+ body.dispatchEvent(new Event('scroll'));
194
+ expect(body.hasAttribute('data-persona-top-fade')).toBe(false);
195
+ body.scrollTop = 80;
196
+ body.dispatchEvent(new Event('scroll'));
197
+ controller.clearChat();
198
+ expect(body.hasAttribute('data-persona-top-fade')).toBe(false);
199
+ });
200
+ });