@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +278 -37
- package/dist/approval-ui.d.ts +278 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +273 -25
- package/dist/artifacts-ui.d.ts +273 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +273 -25
- package/dist/context-mentions.d.ts +273 -25
- package/dist/event-stream-view.d.cts +273 -25
- package/dist/event-stream-view.d.ts +273 -25
- package/dist/forms-ui.d.cts +319 -27
- package/dist/forms-ui.d.ts +319 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -39
- package/dist/index.d.ts +344 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +273 -25
- package/dist/session-reconnect.d.ts +273 -25
- package/dist/smart-dom-reader.d.cts +273 -25
- package/dist/smart-dom-reader.d.ts +273 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +280 -25
- package/dist/theme-editor-preview.d.ts +280 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +293 -27
- package/dist/theme-editor.d.ts +293 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime-7DA7MJH4.js +1 -0
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +33 -0
- package/dist/voice-runtime.d.ts +33 -0
- package/dist/voice-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +4 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +59 -7
- package/src/session.voice.test.ts +72 -1
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +207 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +422 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/voice/runtype-voice-provider.ts +238 -27
- package/src/voice/voice.test.ts +272 -2
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
- 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`.
|
|
3082
|
-
*
|
|
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"
|
|
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
|
|
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.
|
|
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
|
|
5522
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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"
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
7144
|
-
*
|
|
7145
|
-
* until resetHistoryIdentity()
|
|
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
|
});
|