@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-9Vxm-PPz.d.cts → types-Db61aI9J.d.cts} +25 -2
- package/dist/animations/{types-9Vxm-PPz.d.ts → types-Db61aI9J.d.ts} +25 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +25 -2
- package/dist/animations-extra.d.ts +25 -2
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +261 -40
- package/dist/approval-ui.d.ts +261 -40
- 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 +256 -28
- package/dist/artifacts-ui.d.ts +256 -28
- 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 +256 -28
- package/dist/context-mentions.d.ts +256 -28
- package/dist/event-stream-view.d.cts +256 -28
- package/dist/event-stream-view.d.ts +256 -28
- package/dist/forms-ui.d.cts +291 -30
- package/dist/forms-ui.d.ts +291 -30
- package/dist/history-view.d.cts +25 -2
- package/dist/history-view.d.ts +25 -2
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -51
- package/dist/index.d.ts +337 -51
- 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 +256 -28
- package/dist/session-reconnect.d.ts +256 -28
- package/dist/smart-dom-reader.d.cts +256 -28
- package/dist/smart-dom-reader.d.ts +256 -28
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +263 -28
- package/dist/theme-editor-preview.d.ts +263 -28
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +276 -30
- package/dist/theme-editor.d.ts +276 -30
- 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/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.tool-pair-replay.test.ts +325 -0
- package/src/client.ts +154 -61
- 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 +28 -13
- package/src/index-core.ts +2 -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 +61 -9
- 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 +179 -23
- 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 +363 -74
- package/src/ui.v5-defaults.test.ts +144 -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/utils/tool-pair-replay.ts +100 -0
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- 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`.
|
|
3040
|
-
*
|
|
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"
|
|
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
|
|
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.
|
|
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
|
|
5480
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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"
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
7102
|
-
*
|
|
7103
|
-
* until resetHistoryIdentity()
|
|
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("
|
|
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
|
+
});
|