@runtypelabs/persona 4.32.2 → 4.34.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/activity-ui.d.cts +3 -15
- package/dist/activity-ui.d.ts +3 -15
- package/dist/animations/glyph-cycle.d.cts +1 -3
- package/dist/animations/glyph-cycle.d.ts +1 -3
- package/dist/animations/wipe.d.cts +1 -3
- package/dist/animations/wipe.d.ts +1 -3
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +3 -15
- package/dist/approval-ui.d.ts +3 -15
- package/dist/approval-ui.js +1 -1
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +3 -21
- package/dist/artifacts-ui.d.ts +3 -21
- package/dist/background-runtime.d.cts +2 -3
- package/dist/background-runtime.d.ts +2 -3
- package/dist/{chunk-BbXc58OA.js → chunk-6CUrH_jm.js} +1 -1
- package/dist/{chunk-B_H0l3u2.js → chunk-BRM3bHcJ.js} +1 -1
- package/dist/{chunk-DNdeIFJ6.js → chunk-Brdx4NCz.js} +1 -1
- package/dist/chunk-C-rux8Q9.js +1 -0
- package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
- package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
- package/dist/chunk-CEPM3FOp.js +1 -0
- package/dist/{chunk-BpjtKnGI.js → chunk-CV8EPIhT.js} +1 -1
- package/dist/{chunk-DVZ8ri1z.js → chunk-CWzHNuVK.js} +1 -1
- package/dist/{chunk-B3l7TOOv.js → chunk-CYpt7PsO.js} +1 -1
- package/dist/{chunk-2FnmZ9AA.js → chunk-Csr59G5V.js} +1 -1
- package/dist/{chunk-B9Zu-wrk.js → chunk-CxNPSY-b.js} +1 -1
- package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
- package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
- package/dist/chunk-Dqa_GOcj.js +1 -0
- package/dist/chunk-DsWv3EGF.js +1 -0
- package/dist/{chunk-C7-m0EmI.js → chunk-NUNdcnAZ.js} +1 -1
- package/dist/{chunk-D0tcIZJM.js → chunk-YMkMvAsa.js} +2 -2
- package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
- package/dist/chunk-i4qGOJAL.js +1 -0
- package/dist/chunk-kizhspe5.js +1 -0
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
- package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +4 -20
- package/dist/composer-actions-runtime.d.ts +4 -20
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-i_FiUp6J.d.cts} +1 -7
- package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-i_FiUp6J.d.ts} +1 -7
- package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-DhGNgG7Z.d.cts} +1 -3
- package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-DhGNgG7Z.d.ts} +1 -3
- package/dist/config-diagnostics.d.cts +0 -2
- package/dist/config-diagnostics.d.ts +0 -2
- package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-DL-xwGfL.d.cts} +2 -6
- package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-DL-xwGfL.d.ts} +2 -6
- package/dist/context-mentions-inline.cjs +1 -1
- package/dist/context-mentions-inline.d.cts +2 -4
- package/dist/context-mentions-inline.d.ts +2 -4
- package/dist/context-mentions-inline.js +1 -1
- package/dist/context-mentions.cjs +1 -1
- package/dist/context-mentions.d.cts +3 -9
- package/dist/context-mentions.d.ts +3 -9
- package/dist/{defaults-CjX0kKzb.d.ts → defaults-Bga2Xrsp.d.cts} +2 -5
- package/dist/{defaults-CjX0kKzb.d.cts → defaults-Bga2Xrsp.d.ts} +2 -5
- package/dist/dictation-runtime.cjs +1 -1
- package/dist/dictation-runtime.d.cts +4 -13
- package/dist/dictation-runtime.d.ts +4 -13
- package/dist/dictation-runtime.js +1 -1
- package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.cts} +0 -2
- package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.ts} +0 -2
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +3 -16
- package/dist/event-stream-view.d.ts +3 -16
- package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-kLdyPcmk.d.cts} +1 -3
- package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-kLdyPcmk.d.ts} +1 -3
- package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-CmwZGKrf.d.cts} +1 -5
- package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-CmwZGKrf.d.ts} +1 -5
- package/dist/forms-ui.d.cts +2 -4
- package/dist/forms-ui.d.ts +2 -4
- package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-i46-lPN_.d.cts} +2 -14
- package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-i46-lPN_.d.ts} +2 -14
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +8 -27
- package/dist/history-shell.d.ts +8 -27
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/icons-extra.cjs +1 -1
- package/dist/icons-extra.d.cts +0 -2
- package/dist/icons-extra.d.ts +0 -2
- package/dist/index.cjs +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- 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/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-Ccr3ETd3.d.cts} +1 -3
- package/dist/{loading-label-runtime-loader-CPxnoaoi.d.cts → loading-label-runtime-loader-Ccr3ETd3.d.ts} +1 -3
- package/dist/loading-label-runtime.cjs +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/loading-label-runtime.js +1 -1
- package/dist/markdown-parsers.js +1 -1
- package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-C6AVaH7f.d.cts} +21 -4
- package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-C6AVaH7f.d.ts} +21 -4
- package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-BVggqE3C.d.cts} +0 -2
- package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-BVggqE3C.d.ts} +0 -2
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/preferences-runtime.cjs +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/preferences-runtime.js +1 -1
- package/dist/{preview-DdUvoLb4.d.cts → preview-BMdRffo4.d.cts} +2 -6
- package/dist/{preview-DdUvoLb4.d.ts → preview-BMdRffo4.d.ts} +2 -6
- package/dist/{registry-Dz4iHNST.d.ts → registry-B16l8s1b.d.cts} +1 -8
- package/dist/{registry-Dz4iHNST.d.cts → registry-B16l8s1b.d.ts} +1 -8
- package/dist/{session-Baxp9iRK.d.cts → session-CtZHe7fc.d.cts} +10 -93
- package/dist/{session-Baxp9iRK.d.ts → session-CtZHe7fc.d.ts} +10 -93
- package/dist/session-reconnect.d.cts +2 -4
- package/dist/session-reconnect.d.ts +2 -4
- package/dist/smart-dom-reader.d.cts +2 -8
- package/dist/smart-dom-reader.d.ts +2 -8
- package/dist/testing.d.cts +0 -2
- package/dist/testing.d.ts +0 -2
- package/dist/theme-editor-preview.cjs +42 -42
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +25 -25
- package/dist/theme-editor.cjs +25 -5
- package/dist/theme-editor.d.cts +2 -22
- package/dist/theme-editor.d.ts +2 -22
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +4 -4
- package/dist/theme-reference.d.ts +4 -4
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-D2cyFvmM.d.cts} +1 -7
- package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-D2cyFvmM.d.ts} +1 -7
- package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
- package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
- package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-Do1Nd-Ow.d.cts} +2 -12
- package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-Do1Nd-Ow.d.ts} +2 -12
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-BOZHQIk0.d.cts → types-BC7MRxO5.d.cts} +167 -36
- package/dist/{types-BOZHQIk0.d.ts → types-BC7MRxO5.d.ts} +167 -36
- package/dist/ui-extras.js +3 -3
- package/dist/voice-client-tools.d.cts +1 -3
- package/dist/voice-client-tools.d.ts +1 -3
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -12
- package/dist/voice-runtime.d.ts +2 -12
- package/dist/voice-runtime.js +2 -2
- package/dist/voice-worklet-player.d.cts +2 -6
- package/dist/voice-worklet-player.d.ts +2 -6
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-B8g46vS0.d.cts} +1 -5
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-B8g46vS0.d.ts} +1 -5
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +4 -4
- package/src/client-dispatch.ts +69 -16
- package/src/client.operator-role.test.ts +217 -0
- package/src/components/event-stream-view.test.ts +5 -0
- package/src/components/message-bubble.test.ts +169 -0
- package/src/components/message-bubble.ts +123 -34
- package/src/components/scroll-to-bottom.test.ts +135 -0
- package/src/components/scroll-to-bottom.ts +50 -13
- package/src/components/user-message-collapse.ts +117 -0
- package/src/defaults-parity.test.ts +14 -0
- package/src/defaults.ts +6 -0
- package/src/index-core.ts +7 -0
- package/src/session.resubmit-from.test.ts +27 -0
- package/src/session.test.ts +10 -0
- package/src/session.ts +8 -5
- package/src/styles/widget.css +79 -1
- package/src/theme-reference.ts +5 -3
- package/src/types/theme.ts +50 -0
- package/src/types.ts +120 -11
- package/src/ui.ask-user-question-plugin.test.ts +154 -0
- package/src/ui.operator-role.test.ts +336 -0
- package/src/ui.scroll-additive.test.ts +219 -117
- package/src/ui.scroll.test.ts +176 -13
- package/src/ui.ts +267 -119
- package/src/ui.turn-activity.test.ts +7 -7
- package/src/ui.user-message-collapse.test.ts +540 -0
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
- package/src/utils/auto-follow.test.ts +0 -38
- package/src/utils/auto-follow.ts +0 -35
- package/src/utils/brand.test.ts +14 -0
- package/src/utils/brand.ts +8 -4
- package/src/utils/history-messages.test.ts +30 -0
- package/src/utils/history-messages.ts +26 -8
- package/src/utils/message-fingerprint.ts +4 -0
- package/src/utils/resubmit-turn.test.ts +13 -0
- package/src/utils/resubmit-turn.ts +12 -1
- package/src/utils/theme.test.ts +23 -0
- package/src/utils/tokens.ts +47 -0
- package/src/welcome.test.ts +16 -0
- package/src/welcome.ts +10 -2
- package/dist/chunk-BNE40cEG.js +0 -1
- package/dist/chunk-Bz9jAgba.js +0 -1
- package/dist/chunk-CVMIncEY.js +0 -1
- package/dist/chunk-CdpVP6LZ.js +0 -1
- package/dist/chunk-CjkechxR.js +0 -1
- package/dist/chunk-DgZZqy14.js +0 -1
|
@@ -2,6 +2,7 @@ import { getLoadingLabelRuntimeSync } from "../loading-label-runtime-loader";
|
|
|
2
2
|
import { createElement, createNode } from "../utils/dom";
|
|
3
3
|
import {
|
|
4
4
|
AgentWidgetMessage,
|
|
5
|
+
AgentWidgetMessageRole,
|
|
5
6
|
AgentWidgetMessageLayoutConfig,
|
|
6
7
|
AgentWidgetAvatarConfig,
|
|
7
8
|
AgentWidgetTimestampConfig,
|
|
@@ -24,6 +25,11 @@ import { createMentionTokenElement } from "../utils/mention-token";
|
|
|
24
25
|
import { createIconButton } from "../utils/buttons";
|
|
25
26
|
import { renderLucideIcon } from "../utils/icons";
|
|
26
27
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
|
|
28
|
+
import {
|
|
29
|
+
applyUserCollapseState,
|
|
30
|
+
createUserCollapseRegion,
|
|
31
|
+
type UserMessageCollapseRender,
|
|
32
|
+
} from "./user-message-collapse";
|
|
27
33
|
import {
|
|
28
34
|
applyStreamBuffer,
|
|
29
35
|
createSkeletonPlaceholder,
|
|
@@ -577,40 +583,47 @@ export const renderLoadingIndicatorWithFallback = (
|
|
|
577
583
|
*/
|
|
578
584
|
const createAvatar = (
|
|
579
585
|
avatarConfig: AgentWidgetAvatarConfig,
|
|
580
|
-
role:
|
|
586
|
+
role: AgentWidgetMessageRole
|
|
581
587
|
): HTMLElement => {
|
|
582
588
|
const avatar = createElement(
|
|
583
589
|
"div",
|
|
584
590
|
"persona-flex-shrink-0 persona-w-8 persona-h-8 persona-rounded-full persona-flex persona-items-center persona-justify-center persona-text-sm"
|
|
585
591
|
);
|
|
586
592
|
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
593
|
+
const isOperator = role === "operator";
|
|
594
|
+
const avatarContent = role === "user"
|
|
595
|
+
? avatarConfig.userAvatar
|
|
596
|
+
: isOperator
|
|
597
|
+
? avatarConfig.operatorAvatar
|
|
598
|
+
: avatarConfig.assistantAvatar;
|
|
599
|
+
|
|
600
|
+
// The operator avatar is a neutral outlined chip, apart from the solid
|
|
601
|
+
// user and assistant fills.
|
|
602
|
+
const applyFill = () => {
|
|
603
|
+
avatar.classList.add(
|
|
604
|
+
...(isOperator
|
|
605
|
+
? ["persona-bg-persona-container", "persona-text-persona-text", "persona-border"]
|
|
606
|
+
: [role === "user" ? "persona-bg-persona-accent" : "persona-bg-persona-primary", "persona-text-white"])
|
|
607
|
+
);
|
|
608
|
+
};
|
|
590
609
|
|
|
591
610
|
if (avatarContent) {
|
|
592
611
|
// Check if it's a URL or emoji/text
|
|
593
612
|
if (avatarContent.startsWith("http") || avatarContent.startsWith("/") || avatarContent.startsWith("data:")) {
|
|
594
613
|
const img = createElement("img") as HTMLImageElement;
|
|
595
614
|
img.src = avatarContent;
|
|
596
|
-
img.alt = role === "user" ? "User" : "Assistant";
|
|
615
|
+
img.alt = role === "user" ? "User" : isOperator ? "Operator" : "Assistant";
|
|
597
616
|
img.className = "persona-w-full persona-h-full persona-rounded-full persona-object-cover";
|
|
598
617
|
avatar.appendChild(img);
|
|
599
618
|
} else {
|
|
600
619
|
// Emoji or text
|
|
601
620
|
avatar.textContent = avatarContent;
|
|
602
|
-
|
|
603
|
-
role === "user" ? "persona-bg-persona-accent" : "persona-bg-persona-primary",
|
|
604
|
-
"persona-text-white"
|
|
605
|
-
);
|
|
621
|
+
applyFill();
|
|
606
622
|
}
|
|
607
623
|
} else {
|
|
608
624
|
// Default avatar
|
|
609
|
-
avatar.textContent = role === "user" ? "U" : "A";
|
|
610
|
-
|
|
611
|
-
role === "user" ? "persona-bg-persona-accent" : "persona-bg-persona-primary",
|
|
612
|
-
"persona-text-white"
|
|
613
|
-
);
|
|
625
|
+
avatar.textContent = role === "user" ? "U" : isOperator ? "O" : "A";
|
|
626
|
+
applyFill();
|
|
614
627
|
}
|
|
615
628
|
|
|
616
629
|
return avatar;
|
|
@@ -644,13 +657,32 @@ const createTimestamp = (
|
|
|
644
657
|
return timestamp;
|
|
645
658
|
};
|
|
646
659
|
|
|
660
|
+
/** Assistant color utilities an operator bubble replaces with its own tokens. */
|
|
661
|
+
const OPERATOR_DROPPED_CLASSES = new Set([
|
|
662
|
+
"persona-bg-persona-container",
|
|
663
|
+
"persona-text-persona-text",
|
|
664
|
+
"persona-border-persona-message-border",
|
|
665
|
+
]);
|
|
666
|
+
|
|
647
667
|
/**
|
|
648
668
|
* Get bubble classes based on layout preset
|
|
649
669
|
*/
|
|
650
670
|
export const getBubbleClasses = (
|
|
651
|
-
role:
|
|
671
|
+
role: AgentWidgetMessageRole,
|
|
652
672
|
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
653
673
|
): string[] => {
|
|
674
|
+
// Operator bubbles share the assistant's geometry per layout; only the role
|
|
675
|
+
// class changes, and the colors come from the operator tokens instead.
|
|
676
|
+
if (role === "operator") {
|
|
677
|
+
const classes = getBubbleClasses("assistant", layout).flatMap((name) =>
|
|
678
|
+
name === "persona-message-assistant-bubble"
|
|
679
|
+
? ["persona-message-operator-bubble"]
|
|
680
|
+
: OPERATOR_DROPPED_CLASSES.has(name) ? [] : [name]
|
|
681
|
+
);
|
|
682
|
+
// Flat drops the assistant's fill, not the operator's: the operator keeps
|
|
683
|
+
// its background, so it needs the minimal layout's side padding.
|
|
684
|
+
return layout === "flat" ? [...classes, "persona-px-3"] : classes;
|
|
685
|
+
}
|
|
654
686
|
const baseClasses = ["persona-message-bubble"];
|
|
655
687
|
|
|
656
688
|
switch (layout) {
|
|
@@ -763,7 +795,7 @@ export const applyBubbleGeometry = (
|
|
|
763
795
|
tokens?: MessageGeometryTokens
|
|
764
796
|
): void => {
|
|
765
797
|
if (!tokens) return;
|
|
766
|
-
const key = role === "user"
|
|
798
|
+
const key = role === "user" || role === "operator" ? role : "assistant";
|
|
767
799
|
const preset = BUBBLE_GEOMETRY_DEFAULTS[layout ?? "bubble"] ?? BUBBLE_GEOMETRY_DEFAULTS.bubble;
|
|
768
800
|
if (tokens.padding) {
|
|
769
801
|
bubble.style.padding = `var(--persona-message-${key}-padding, ${preset.padding})`;
|
|
@@ -787,13 +819,14 @@ export const customMessageActionDataAction = (id: string): string =>
|
|
|
787
819
|
|
|
788
820
|
/**
|
|
789
821
|
* Host-contributed actions this message's role qualifies for. Assistant-only by
|
|
790
|
-
* default, matching every built-in except edit and quote
|
|
822
|
+
* default, matching every built-in except edit and quote; an action reaches
|
|
823
|
+
* operator messages only when its `roles` lists `"operator"`.
|
|
791
824
|
*/
|
|
792
825
|
export const customMessageActionsForRole = (
|
|
793
826
|
actionsConfig: AgentWidgetMessageActionsConfig | undefined,
|
|
794
827
|
role: AgentWidgetMessage["role"]
|
|
795
828
|
): AgentWidgetCustomMessageAction[] => {
|
|
796
|
-
if (role !== "user" && role !== "assistant") return [];
|
|
829
|
+
if (role !== "user" && role !== "assistant" && role !== "operator") return [];
|
|
797
830
|
return (actionsConfig?.custom ?? []).filter((action) =>
|
|
798
831
|
(action.roles ?? ["assistant"]).includes(role)
|
|
799
832
|
);
|
|
@@ -970,6 +1003,11 @@ export type CreateStandardBubbleOptions = {
|
|
|
970
1003
|
* Full widget config (needed for loading indicator context)
|
|
971
1004
|
*/
|
|
972
1005
|
widgetConfig?: import("../types").AgentWidgetConfig;
|
|
1006
|
+
/**
|
|
1007
|
+
* Long user message collapse (`features.userMessageCollapse`). When set on
|
|
1008
|
+
* a user bubble, its content is wrapped in a clip region with the toggle.
|
|
1009
|
+
*/
|
|
1010
|
+
userCollapse?: UserMessageCollapseRender;
|
|
973
1011
|
};
|
|
974
1012
|
|
|
975
1013
|
/**
|
|
@@ -1000,21 +1038,33 @@ export const createStandardBubble = (
|
|
|
1000
1038
|
bubble.id = `bubble-${message.id}`;
|
|
1001
1039
|
bubble.setAttribute("data-message-id", message.id);
|
|
1002
1040
|
|
|
1003
|
-
|
|
1041
|
+
const isOperator = message.role === "operator";
|
|
1042
|
+
// The V5 operator is outlined, not filled, so every layout needs the border
|
|
1043
|
+
// style that only the bubble layout's classes carry.
|
|
1044
|
+
if (isOperator && options?.widgetConfig?.future?.v5Defaults === true) bubble.classList.add("persona-border");
|
|
1045
|
+
bubble.setAttribute(
|
|
1046
|
+
"data-persona-theme-zone",
|
|
1047
|
+
message.role === "user" ? "user-message" : isOperator ? "operator-message" : "assistant-message"
|
|
1048
|
+
);
|
|
1004
1049
|
|
|
1005
1050
|
const defaultMessageTokens = resolveThemeDefaults(options?.widgetConfig?.future).components.message;
|
|
1006
1051
|
const messageTokens = options?.widgetConfig?.theme?.components?.message;
|
|
1052
|
+
const darkMessageTokens = options?.widgetConfig?.darkTheme?.components?.message;
|
|
1007
1053
|
applyBubbleGeometry(
|
|
1008
1054
|
bubble,
|
|
1009
1055
|
message.role,
|
|
1010
1056
|
layout,
|
|
1011
1057
|
message.role === "user"
|
|
1012
|
-
? { ...defaultMessageTokens.user, ...messageTokens?.user, ...
|
|
1013
|
-
:
|
|
1058
|
+
? { ...defaultMessageTokens.user, ...messageTokens?.user, ...darkMessageTokens?.user }
|
|
1059
|
+
: isOperator
|
|
1060
|
+
? { ...defaultMessageTokens.operator, ...messageTokens?.operator, ...darkMessageTokens?.operator }
|
|
1061
|
+
: { ...defaultMessageTokens.assistant, ...messageTokens?.assistant, ...darkMessageTokens?.assistant }
|
|
1014
1062
|
);
|
|
1015
1063
|
|
|
1016
|
-
// Apply component-level color overrides via CSS variables
|
|
1017
|
-
|
|
1064
|
+
// Apply component-level color overrides via CSS variables. Flat clears the
|
|
1065
|
+
// user and assistant fills; an operator keeps its own background token so it
|
|
1066
|
+
// stays apart from the assistant prose on the same side.
|
|
1067
|
+
if (layout === "flat" && !isOperator) {
|
|
1018
1068
|
bubble.style.backgroundColor = "transparent";
|
|
1019
1069
|
bubble.style.color =
|
|
1020
1070
|
message.role === "user"
|
|
@@ -1026,6 +1076,23 @@ export const createStandardBubble = (
|
|
|
1026
1076
|
} else if (message.role === "assistant") {
|
|
1027
1077
|
bubble.style.backgroundColor = 'var(--persona-message-assistant-bg, var(--persona-container))';
|
|
1028
1078
|
bubble.style.color = 'var(--persona-message-assistant-text, var(--persona-text))';
|
|
1079
|
+
} else if (isOperator) {
|
|
1080
|
+
bubble.style.backgroundColor = 'var(--persona-components-message-operator-background, var(--persona-container))';
|
|
1081
|
+
bubble.style.color = 'var(--persona-components-message-operator-text, var(--persona-text))';
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (isOperator) {
|
|
1085
|
+
const authorLabel = createElement("div", "persona-message-author persona-text-xs persona-font-medium persona-mb-1");
|
|
1086
|
+
const configuredLabel = options?.widgetConfig?.copy?.operatorLabel;
|
|
1087
|
+
const operatorLabel = typeof configuredLabel === "string" && configuredLabel.trim()
|
|
1088
|
+
? configuredLabel.trim() : "Human operator";
|
|
1089
|
+
const displayName = typeof message.author?.displayName === "string"
|
|
1090
|
+
? message.author.displayName.trim() : "";
|
|
1091
|
+
authorLabel.textContent = displayName ? `${displayName} · ${operatorLabel}` : operatorLabel;
|
|
1092
|
+
// A visible name is also the accessible name for the human-authored group.
|
|
1093
|
+
bubble.setAttribute("role", "group");
|
|
1094
|
+
bubble.setAttribute("aria-label", authorLabel.textContent);
|
|
1095
|
+
bubble.appendChild(authorLabel);
|
|
1029
1096
|
}
|
|
1030
1097
|
|
|
1031
1098
|
const imageParts = getMessageImageParts(message);
|
|
@@ -1181,6 +1248,12 @@ export const createStandardBubble = (
|
|
|
1181
1248
|
}
|
|
1182
1249
|
}
|
|
1183
1250
|
|
|
1251
|
+
// Everything a long user message can spill (attachments, chips, text) goes
|
|
1252
|
+
// in the collapse region; the timestamp and actions stay outside the clip.
|
|
1253
|
+
const userCollapse = message.role === "user" ? options?.userCollapse : undefined;
|
|
1254
|
+
const collapseRegion = userCollapse ? createUserCollapseRegion(message.id, userCollapse.maxHeight) : null;
|
|
1255
|
+
const contentParent = collapseRegion ?? bubble;
|
|
1256
|
+
|
|
1184
1257
|
if (imageParts.length > 0) {
|
|
1185
1258
|
const imagePreviews = createMessageImagePreviews(
|
|
1186
1259
|
imageParts,
|
|
@@ -1193,7 +1266,7 @@ export const createStandardBubble = (
|
|
|
1193
1266
|
);
|
|
1194
1267
|
|
|
1195
1268
|
if (imagePreviews) {
|
|
1196
|
-
|
|
1269
|
+
contentParent.appendChild(imagePreviews);
|
|
1197
1270
|
} else if (shouldHideTextUntilPreviewFails && textContentDiv) {
|
|
1198
1271
|
textContentDiv.style.display = "";
|
|
1199
1272
|
}
|
|
@@ -1203,7 +1276,7 @@ export const createStandardBubble = (
|
|
|
1203
1276
|
if (audioParts.length > 0) {
|
|
1204
1277
|
const audioPreviews = createMessageAudioPreviews(audioParts);
|
|
1205
1278
|
if (audioPreviews) {
|
|
1206
|
-
|
|
1279
|
+
contentParent.appendChild(audioPreviews);
|
|
1207
1280
|
}
|
|
1208
1281
|
}
|
|
1209
1282
|
|
|
@@ -1211,7 +1284,7 @@ export const createStandardBubble = (
|
|
|
1211
1284
|
if (videoParts.length > 0) {
|
|
1212
1285
|
const videoPreviews = createMessageVideoPreviews(videoParts);
|
|
1213
1286
|
if (videoPreviews) {
|
|
1214
|
-
|
|
1287
|
+
contentParent.appendChild(videoPreviews);
|
|
1215
1288
|
}
|
|
1216
1289
|
}
|
|
1217
1290
|
|
|
@@ -1219,7 +1292,7 @@ export const createStandardBubble = (
|
|
|
1219
1292
|
if (fileParts.length > 0) {
|
|
1220
1293
|
const filePreviews = createMessageFilePreviews(fileParts);
|
|
1221
1294
|
if (filePreviews) {
|
|
1222
|
-
|
|
1295
|
+
contentParent.appendChild(filePreviews);
|
|
1223
1296
|
}
|
|
1224
1297
|
}
|
|
1225
1298
|
|
|
@@ -1229,10 +1302,14 @@ export const createStandardBubble = (
|
|
|
1229
1302
|
? null
|
|
1230
1303
|
: createMessageMentionChips(message.contextMentions);
|
|
1231
1304
|
if (mentionChips) {
|
|
1232
|
-
|
|
1305
|
+
contentParent.appendChild(mentionChips);
|
|
1233
1306
|
}
|
|
1234
1307
|
|
|
1235
|
-
|
|
1308
|
+
contentParent.appendChild(contentDiv);
|
|
1309
|
+
if (collapseRegion && userCollapse) {
|
|
1310
|
+
bubble.appendChild(collapseRegion);
|
|
1311
|
+
applyUserCollapseState(bubble, collapseRegion, message.id, userCollapse);
|
|
1312
|
+
}
|
|
1236
1313
|
|
|
1237
1314
|
// Add timestamp below if configured
|
|
1238
1315
|
if (showTimestamp && timestampPosition === "below" && message.createdAt) {
|
|
@@ -1293,14 +1370,15 @@ export const createStandardBubble = (
|
|
|
1293
1370
|
bubble.appendChild(createStopReasonNotice(message.stopReason, stopReasonNoticeText));
|
|
1294
1371
|
}
|
|
1295
1372
|
|
|
1296
|
-
// Add message actions for assistant messages (only when not
|
|
1373
|
+
// Add message actions for assistant and operator messages (only when not
|
|
1374
|
+
// streaming and has content). Operator rows get copy and opted-in custom
|
|
1375
|
+
// actions only: feedback, read-aloud and regenerate are about model output.
|
|
1297
1376
|
const shouldShowActions =
|
|
1298
|
-
message.role === "assistant" &&
|
|
1377
|
+
(message.role === "assistant" || isOperator) &&
|
|
1299
1378
|
!message.streaming &&
|
|
1300
1379
|
// A voice caption (the voice model's own speech) is display-only.
|
|
1301
1380
|
!message.voiceCaption &&
|
|
1302
|
-
|
|
1303
|
-
message.content.trim() &&
|
|
1381
|
+
(messageContentText.length > 0 || (isOperator && customMessageActionsForRole(actionsConfig, "operator").length > 0)) &&
|
|
1304
1382
|
actionsConfig?.enabled !== false;
|
|
1305
1383
|
|
|
1306
1384
|
// User bubbles carry actions only when this message is edit- or quote-eligible;
|
|
@@ -1321,7 +1399,18 @@ export const createStandardBubble = (
|
|
|
1321
1399
|
shouldShowUserActions
|
|
1322
1400
|
? // A user bubble never hosts the assistant-only controls.
|
|
1323
1401
|
{ ...actionsConfig, showCopy: false, showUpvote: false, showDownvote: false, showReadAloud: false }
|
|
1324
|
-
:
|
|
1402
|
+
: isOperator
|
|
1403
|
+
? {
|
|
1404
|
+
...actionsConfig,
|
|
1405
|
+
showCopy: messageContentText.length > 0 && actionsConfig.showCopy !== false,
|
|
1406
|
+
showUpvote: false,
|
|
1407
|
+
showDownvote: false,
|
|
1408
|
+
showReadAloud: false,
|
|
1409
|
+
showRegenerate: false,
|
|
1410
|
+
showEdit: false,
|
|
1411
|
+
showQuote: false,
|
|
1412
|
+
}
|
|
1413
|
+
: actionsConfig,
|
|
1325
1414
|
actionCallbacks,
|
|
1326
1415
|
eligibility,
|
|
1327
1416
|
options?.widgetConfig?.future?.v5Defaults === true && actionsConfig.visibility === undefined
|
|
@@ -40,6 +40,49 @@ describe("scroll-to-bottom chrome", () => {
|
|
|
40
40
|
control.destroy();
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it("names a pending approval below with a labelled shield pill, then reverts", () => {
|
|
44
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
45
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
46
|
+
control.update(config, true, false, 2, true);
|
|
47
|
+
const el = control.element;
|
|
48
|
+
expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
|
|
49
|
+
expect(el.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("true");
|
|
50
|
+
expect(el.textContent).toBe("Approval needed");
|
|
51
|
+
expect(el.querySelector("svg")?.getAttribute("data-icon")).toBe("shield-check");
|
|
52
|
+
// The label replaces the unread count; assistive tech hears where it leads.
|
|
53
|
+
expect(el.querySelector<HTMLElement>("[data-persona-scroll-to-bottom-count]")?.style.display).toBe("none");
|
|
54
|
+
expect(el.getAttribute("aria-label")).toBe("Approval needed below. Jump to latest");
|
|
55
|
+
|
|
56
|
+
// Hidden, the approval state never shows.
|
|
57
|
+
control.update(config, false, false, 0, true);
|
|
58
|
+
expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(false);
|
|
59
|
+
|
|
60
|
+
control.update(config, true, false, 2, false);
|
|
61
|
+
expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(false);
|
|
62
|
+
expect(el.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("false");
|
|
63
|
+
expect(el.querySelector("svg")?.getAttribute("data-icon")).toBe("arrow-down");
|
|
64
|
+
expect(el.getAttribute("aria-label")).toBe("Jump to latest (2 new)");
|
|
65
|
+
control.destroy();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses a custom approval label, opts out with an empty one, and tells custom glyphs", () => {
|
|
69
|
+
const states: string[] = [];
|
|
70
|
+
const renderIcon = vi.fn((_context: AgentWidgetScrollToBottomIconContext) => ({
|
|
71
|
+
element: document.createElement("span"),
|
|
72
|
+
update: (next: AgentWidgetScrollToBottomIconContext) => { states.push(next.state.activity); },
|
|
73
|
+
}));
|
|
74
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
75
|
+
control.update({ features: { scrollToBottom: { approvalLabel: "Review request", renderIcon } } }, true, false, 0, true);
|
|
76
|
+
expect(control.element.textContent).toBe("Review request");
|
|
77
|
+
expect(renderIcon.mock.calls[0][0].state.activity).toBe("approval-below");
|
|
78
|
+
control.update({ features: { scrollToBottom: { approvalLabel: "", renderIcon } } }, true, false, 1, true);
|
|
79
|
+
expect(control.element.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("false");
|
|
80
|
+
expect(control.element.getAttribute("aria-label")).toBe("Approval needed below. Jump to latest");
|
|
81
|
+
control.update({ features: { scrollToBottom: { renderIcon } } }, true, false, 0, false);
|
|
82
|
+
expect(states.at(-1)).toBe("idle");
|
|
83
|
+
control.destroy();
|
|
84
|
+
});
|
|
85
|
+
|
|
43
86
|
it("makes fading controls inert immediately and returns focus only when owned, including Shadow DOM", () => {
|
|
44
87
|
const host = document.createElement("div"); document.body.appendChild(host);
|
|
45
88
|
const root = host.attachShadow({ mode: "open" });
|
|
@@ -122,6 +165,98 @@ describe("scroll-to-bottom chrome", () => {
|
|
|
122
165
|
control.destroy(); warning.mockRestore();
|
|
123
166
|
});
|
|
124
167
|
|
|
168
|
+
describe("approval morph", () => {
|
|
169
|
+
const stubAnimate = () => {
|
|
170
|
+
const calls: { target: Element; keyframes: Keyframe[]; options: KeyframeAnimationOptions; animation: { cancel: ReturnType<typeof vi.fn> } }[] = [];
|
|
171
|
+
const original = (HTMLElement.prototype as { animate?: unknown }).animate;
|
|
172
|
+
(HTMLElement.prototype as { animate?: unknown }).animate = function (this: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions) {
|
|
173
|
+
const animation = { cancel: vi.fn(), finished: new Promise<void>(() => {}) };
|
|
174
|
+
calls.push({ target: this, keyframes, options, animation });
|
|
175
|
+
return animation;
|
|
176
|
+
};
|
|
177
|
+
return { calls, restore: () => { (HTMLElement.prototype as { animate?: unknown }).animate = original; } };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
it("morphs a visible arrow into the pill on the theme's motion tokens, and back", () => {
|
|
181
|
+
const { calls, restore } = stubAnimate();
|
|
182
|
+
try {
|
|
183
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
184
|
+
document.body.appendChild(control.element);
|
|
185
|
+
control.element.style.setProperty("--persona-motion-duration-base", "0.3s");
|
|
186
|
+
control.element.style.setProperty("--persona-motion-easing", "cubic-bezier(0.2, 0, 0, 1)");
|
|
187
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
188
|
+
control.update(config, true);
|
|
189
|
+
expect(calls).toHaveLength(0);
|
|
190
|
+
|
|
191
|
+
control.update(config, true, false, 0, true);
|
|
192
|
+
const [own, labelFade] = calls;
|
|
193
|
+
expect(own.target).toBe(control.element);
|
|
194
|
+
expect(own.keyframes).toHaveLength(2);
|
|
195
|
+
expect(own.keyframes[0]).toMatchObject({ overflow: "hidden" });
|
|
196
|
+
expect(own.keyframes[0]).toHaveProperty("width");
|
|
197
|
+
expect(own.keyframes[1]).toHaveProperty("backgroundColor");
|
|
198
|
+
expect(own.options).toEqual({ duration: 300, easing: "cubic-bezier(0.2, 0, 0, 1)" });
|
|
199
|
+
// The label fades in as the clipped pill widens.
|
|
200
|
+
expect(labelFade.target.textContent).toBe("Approval needed");
|
|
201
|
+
expect(calls).toHaveLength(2);
|
|
202
|
+
|
|
203
|
+
// Ordinary syncs in the same state leave a running morph alone.
|
|
204
|
+
control.update(config, true, false, 0, true);
|
|
205
|
+
expect(calls).toHaveLength(2);
|
|
206
|
+
expect(calls.every((call) => call.animation.cancel.mock.calls.length === 0)).toBe(true);
|
|
207
|
+
|
|
208
|
+
// Back to the arrow: the running morph is replaced, unclipped (the
|
|
209
|
+
// count badge hangs outside the button).
|
|
210
|
+
control.update(config, true, false, 2, false);
|
|
211
|
+
expect(calls.slice(0, 2).every((call) => call.animation.cancel.mock.calls.length === 1)).toBe(true);
|
|
212
|
+
expect(calls).toHaveLength(3);
|
|
213
|
+
expect(calls[2].keyframes[0]).not.toHaveProperty("overflow");
|
|
214
|
+
control.destroy();
|
|
215
|
+
} finally { restore(); }
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("swaps instantly when the theme turns motion off, and falls back when the duration is missing", () => {
|
|
219
|
+
const { calls, restore } = stubAnimate();
|
|
220
|
+
try {
|
|
221
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
222
|
+
const off = createScrollToBottomControl(icon, "transcript");
|
|
223
|
+
off.element.style.setProperty("--persona-motion-duration-base", "0ms");
|
|
224
|
+
off.update(config, true);
|
|
225
|
+
off.update(config, true, false, 0, true);
|
|
226
|
+
expect(calls).toHaveLength(0);
|
|
227
|
+
expect(off.element.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
|
|
228
|
+
off.destroy();
|
|
229
|
+
|
|
230
|
+
const unset = createScrollToBottomControl(icon, "transcript");
|
|
231
|
+
unset.update(config, true);
|
|
232
|
+
unset.update(config, true, false, 0, true);
|
|
233
|
+
expect(calls[0].options.duration).toBe(200);
|
|
234
|
+
unset.destroy();
|
|
235
|
+
} finally { restore(); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("does not morph a control that appears, hides, or prefers reduced motion", () => {
|
|
239
|
+
const { calls, restore } = stubAnimate();
|
|
240
|
+
try {
|
|
241
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
242
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
243
|
+
control.update(config, false);
|
|
244
|
+
control.update(config, true, false, 0, true); // appears already as the pill
|
|
245
|
+
control.update(config, false, false, 0, true); // hides
|
|
246
|
+
expect(calls).toHaveLength(0);
|
|
247
|
+
control.destroy();
|
|
248
|
+
|
|
249
|
+
vi.stubGlobal("matchMedia", () => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }));
|
|
250
|
+
const reduced = createScrollToBottomControl(icon, "transcript");
|
|
251
|
+
reduced.update(config, true);
|
|
252
|
+
reduced.update(config, true, false, 0, true);
|
|
253
|
+
expect(calls).toHaveLength(0);
|
|
254
|
+
expect(reduced.element.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
|
|
255
|
+
reduced.destroy();
|
|
256
|
+
} finally { restore(); }
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
125
260
|
it("provides live reduced-motion changes and releases its media listener", () => {
|
|
126
261
|
let change: (() => void) | undefined;
|
|
127
262
|
const media = { matches: true, addEventListener: vi.fn((_event, callback) => { change = callback; }), removeEventListener: vi.fn() };
|
|
@@ -25,9 +25,7 @@ export function createScrollToBottomControl(
|
|
|
25
25
|
const label = createElement("span", "");
|
|
26
26
|
const badge = createElement("span", "");
|
|
27
27
|
badge.setAttribute("data-persona-scroll-to-bottom-count", "");
|
|
28
|
-
element.
|
|
29
|
-
element.appendChild(label);
|
|
30
|
-
element.appendChild(badge);
|
|
28
|
+
element.append(glyph, label, badge);
|
|
31
29
|
let config: AgentWidgetConfig = {};
|
|
32
30
|
let state: AgentWidgetScrollToBottomIconState = { activity: "idle", visible: false, hovered: false, focused: false, reducedMotion: false };
|
|
33
31
|
// Keep renderer identity separate from its handle, including null fallbacks.
|
|
@@ -36,6 +34,26 @@ export function createScrollToBottomControl(
|
|
|
36
34
|
let abort: AbortController | undefined;
|
|
37
35
|
let builtin = "";
|
|
38
36
|
const media = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
|
|
37
|
+
// Arrow <-> "Approval needed" pill: one button changing shape, so morph its
|
|
38
|
+
// width and colors on the theme's motion tokens while the new label fades in.
|
|
39
|
+
let morphs: Animation[] = [];
|
|
40
|
+
const paint = () => {
|
|
41
|
+
const style = getComputedStyle(element);
|
|
42
|
+
return { width: `${element.offsetWidth}px`, backgroundColor: style.backgroundColor, borderColor: style.borderColor, color: style.color };
|
|
43
|
+
};
|
|
44
|
+
const morph = (from: Keyframe) => {
|
|
45
|
+
const style = getComputedStyle(element);
|
|
46
|
+
const value = style.getPropertyValue("--persona-motion-duration-base");
|
|
47
|
+
const ms = parseFloat(value) * (value.includes("ms") ? 1 : 1000);
|
|
48
|
+
// "0ms" turns motion off; only a missing or invalid value falls back.
|
|
49
|
+
if (ms === 0) return;
|
|
50
|
+
const timing = { duration: ms > 0 ? ms : 200, easing: style.getPropertyValue("--persona-motion-easing").trim() || "ease-out" };
|
|
51
|
+
// Clip only while widening into the pill: the arrow's count badge hangs
|
|
52
|
+
// outside the button.
|
|
53
|
+
const clip = label.style.display === "none" ? null : { overflow: "hidden" };
|
|
54
|
+
morphs = [element.animate([{ ...from, ...clip }, { ...paint(), ...clip }], timing)];
|
|
55
|
+
if (clip) morphs.push(label.animate([{ opacity: 0 }, { opacity: 1 }], timing));
|
|
56
|
+
};
|
|
39
57
|
state.reducedMotion = media?.matches ?? false;
|
|
40
58
|
const warn = (error: unknown) => console.warn("Persona scroll icon failed", error);
|
|
41
59
|
const context = (): AgentWidgetScrollToBottomIconContext => ({ config, state: { ...state }, view, signal: abort!.signal });
|
|
@@ -51,7 +69,9 @@ export function createScrollToBottomControl(
|
|
|
51
69
|
if (handle?.element) return;
|
|
52
70
|
const feature = config.features?.scrollToBottom;
|
|
53
71
|
const dots = state.activity === "generating-below" && (feature?.streamingIndicator ?? (config.future?.v5Defaults ? "dots" : "arrow")) === "dots";
|
|
54
|
-
|
|
72
|
+
// Same glyph as the approval card it points at, so the two read as one thing.
|
|
73
|
+
const iconName = state.activity === "approval-below" ? "shield-check" : feature?.iconName ?? "arrow-down";
|
|
74
|
+
const key = dots ? "dots" : `arrow:${iconName}`;
|
|
55
75
|
if (builtin === key) return;
|
|
56
76
|
builtin = key;
|
|
57
77
|
glyph.innerHTML = "";
|
|
@@ -64,7 +84,7 @@ export function createScrollToBottomControl(
|
|
|
64
84
|
}
|
|
65
85
|
glyph.appendChild(dotGroup);
|
|
66
86
|
} else {
|
|
67
|
-
const icon = renderIcon(
|
|
87
|
+
const icon = renderIcon(iconName, "14px", "currentColor", 2);
|
|
68
88
|
if (icon) glyph.appendChild(icon);
|
|
69
89
|
}
|
|
70
90
|
};
|
|
@@ -106,11 +126,12 @@ export function createScrollToBottomControl(
|
|
|
106
126
|
|
|
107
127
|
return {
|
|
108
128
|
element,
|
|
109
|
-
|
|
129
|
+
/** `approval` marks a pending approval below the fold: the control names it. */
|
|
130
|
+
update(nextConfig: AgentWidgetConfig, visible: boolean, active = false, count = 0, approval = false) {
|
|
110
131
|
const previous = config.features?.scrollToBottom;
|
|
111
132
|
const next = nextConfig.features?.scrollToBottom;
|
|
112
133
|
const configChanged = config.future?.v5Defaults !== nextConfig.future?.v5Defaults ||
|
|
113
|
-
(["enabled", "iconName", "label", "streamingIndicator", "transition", "scrollBehavior", "scrollDurationMs"] as (keyof AgentWidgetScrollToBottomFeature)[])
|
|
134
|
+
(["enabled", "iconName", "label", "approvalLabel", "streamingIndicator", "transition", "scrollBehavior", "scrollDurationMs"] as (keyof AgentWidgetScrollToBottomFeature)[])
|
|
114
135
|
.some((key) => previous?.[key] !== next?.[key]);
|
|
115
136
|
config = nextConfig;
|
|
116
137
|
const feature = config.features?.scrollToBottom;
|
|
@@ -132,21 +153,37 @@ export function createScrollToBottomControl(
|
|
|
132
153
|
focusTarget.focus({ preventScroll: true });
|
|
133
154
|
}
|
|
134
155
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
// A waiting approval outranks streaming and the unread count: the turn is
|
|
157
|
+
// blocked on the reader, so the control says so in words.
|
|
158
|
+
const waiting = approval && visible;
|
|
159
|
+
// Morph only a visible control changing shape, never its fade in or out.
|
|
160
|
+
const reshape = state.visible && visible && (state.activity === "approval-below") !== waiting;
|
|
161
|
+
if (reshape || !visible) morphs.forEach((animation) => animation.cancel());
|
|
162
|
+
const from = reshape && !state.reducedMotion && typeof element.animate === "function" ? paint() : null;
|
|
163
|
+
const approvalText = feature?.approvalLabel ?? "Approval needed";
|
|
164
|
+
const text = waiting && approvalText ? approvalText : feature?.label ?? "";
|
|
165
|
+
const base = feature?.label || "Jump to latest";
|
|
166
|
+
element.setAttribute("aria-label", waiting
|
|
167
|
+
? `${approvalText || "Approval needed"} below. ${base}`
|
|
168
|
+
: `${base}${count > 0 ? ` (${count} new)` : ""}${active && visible ? " (response streaming below)" : ""}`);
|
|
138
169
|
element.title = text;
|
|
139
170
|
element.setAttribute("data-persona-scroll-to-bottom-has-label", String(Boolean(text)));
|
|
171
|
+
if (waiting) element.setAttribute("data-persona-scroll-to-bottom-approval", "");
|
|
172
|
+
else element.removeAttribute("data-persona-scroll-to-bottom-approval");
|
|
140
173
|
label.textContent = text;
|
|
141
174
|
label.style.display = text ? "" : "none";
|
|
142
|
-
|
|
143
|
-
badge.
|
|
144
|
-
|
|
175
|
+
const shownCount = waiting ? 0 : count;
|
|
176
|
+
badge.textContent = shownCount > 0 ? String(shownCount) : "";
|
|
177
|
+
badge.style.display = shownCount > 0 ? "" : "none";
|
|
178
|
+
const activity = waiting ? "approval-below" as const : active && visible ? "generating-below" as const : "idle" as const;
|
|
179
|
+
const nextState = { ...state, visible, hovered: visible && state.hovered, focused: visible && state.focused, activity };
|
|
145
180
|
const changed = configChanged || stateChanged(nextState);
|
|
146
181
|
state = nextState;
|
|
147
182
|
updateRenderer(changed);
|
|
183
|
+
if (from) morph(from);
|
|
148
184
|
},
|
|
149
185
|
destroy() {
|
|
186
|
+
morphs.forEach((animation) => animation.cancel());
|
|
150
187
|
dispose();
|
|
151
188
|
lifetime.abort();
|
|
152
189
|
media?.removeEventListener?.("change", motion);
|