@runtypelabs/persona 4.32.1 → 4.33.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 -3
- package/dist/activity-ui.d.ts +3 -3
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.d.cts +2 -2
- package/dist/approval-ui.d.ts +2 -2
- package/dist/artifacts-ui.d.cts +3 -3
- package/dist/artifacts-ui.d.ts +3 -3
- package/dist/background-runtime.d.cts +1 -1
- package/dist/background-runtime.d.ts +1 -1
- 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-CEPM3FOp.js +1 -0
- package/dist/{chunk-CumoXK3u.js → chunk-CXQJG7pb.js} +1 -1
- package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
- package/dist/{chunk-BpjtKnGI.js → chunk-D6U0E82p.js} +1 -1
- package/dist/{chunk-2FnmZ9AA.js → chunk-DOZmcve8.js} +1 -1
- package/dist/chunk-DVgzoVCB.js +1 -0
- package/dist/{chunk-C9uOhfk5.js → chunk-DW7Vg_l2.js} +1 -1
- package/dist/{chunk-B3l7TOOv.js → chunk-DbGLjU2P.js} +1 -1
- package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
- package/dist/{chunk-DgZZqy14.js → chunk-d54BJdKd.js} +1 -1
- package/dist/chunk-dLJWKfOb.js +1 -0
- package/dist/{chunk-DNdeIFJ6.js → chunk-gu34lr-P.js} +1 -1
- package/dist/chunk-i4qGOJAL.js +1 -0
- package/dist/{chunk-DVZ8ri1z.js → chunk-ql5N2I_t.js} +1 -1
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.d.cts +2 -2
- package/dist/composer-actions-runtime.d.ts +2 -2
- package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-DnqGCXZz.d.cts} +1 -1
- package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-DnqGCXZz.d.ts} +1 -1
- package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-6Iwy6WLI.d.cts} +1 -1
- package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-6Iwy6WLI.d.ts} +1 -1
- package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-B-rHF98n.d.cts} +2 -2
- package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-B-rHF98n.d.ts} +2 -2
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/{defaults-CjX0kKzb.d.ts → defaults-BuOjqUAG.d.cts} +2 -1
- package/dist/{defaults-CjX0kKzb.d.cts → defaults-BuOjqUAG.d.ts} +2 -1
- package/dist/dictation-runtime.d.cts +2 -2
- package/dist/dictation-runtime.d.ts +2 -2
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-SqEfkGda.d.cts} +1 -1
- package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-SqEfkGda.d.ts} +1 -1
- package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-DqAmje-2.d.cts} +1 -1
- package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-DqAmje-2.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-B3L7QNcO.d.cts} +1 -1
- package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-B3L7QNcO.d.ts} +1 -1
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +6 -6
- package/dist/history-shell.d.ts +6 -6
- 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/index.cjs +38 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +52 -52
- 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.cts → loading-label-runtime-loader-JecjOfJS.d.cts} +1 -1
- package/dist/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-JecjOfJS.d.ts} +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-CBYtpDsT.d.cts} +2 -2
- package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-CBYtpDsT.d.ts} +2 -2
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/{preview-DdUvoLb4.d.cts → preview-_UJwwt2L.d.cts} +2 -2
- package/dist/{preview-DdUvoLb4.d.ts → preview-_UJwwt2L.d.ts} +2 -2
- package/dist/{registry-Dz4iHNST.d.cts → registry-B5ZV3K-h.d.cts} +1 -1
- package/dist/{registry-Dz4iHNST.d.ts → registry-B5ZV3K-h.d.ts} +1 -1
- package/dist/{session-Baxp9iRK.d.cts → session-Bys8_XuB.d.cts} +10 -9
- package/dist/{session-Baxp9iRK.d.ts → session-Bys8_XuB.d.ts} +10 -9
- package/dist/session-reconnect.d.cts +2 -2
- package/dist/session-reconnect.d.ts +2 -2
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +31 -31
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +19 -19
- package/dist/theme-editor.cjs +1 -1
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +4 -2
- package/dist/theme-reference.d.ts +4 -2
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-d5qAQcwv.d.cts} +1 -1
- package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-d5qAQcwv.d.ts} +1 -1
- package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-q6fHVnx-.d.cts} +2 -2
- package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-q6fHVnx-.d.ts} +2 -2
- 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-B_mj21jg.d.cts} +91 -7
- package/dist/{types-BOZHQIk0.d.ts → types-B_mj21jg.d.ts} +91 -7
- package/dist/voice-client-tools.d.cts +1 -1
- package/dist/voice-client-tools.d.ts +1 -1
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -2
- package/dist/voice-runtime.d.ts +2 -2
- package/dist/voice-runtime.js +1 -1
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-ClEO3qdI.d.cts} +1 -1
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-ClEO3qdI.d.ts} +1 -1
- 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 +1 -1
- package/src/client-dispatch.ts +69 -16
- package/src/client.operator-role.test.ts +217 -0
- package/src/components/message-bubble.test.ts +169 -0
- package/src/components/message-bubble.ts +97 -28
- package/src/defaults-parity.test.ts +14 -0
- package/src/index-core.ts +7 -0
- package/src/index-global.ts +2 -1
- package/src/install.test.ts +6 -5
- package/src/install.ts +2 -2
- package/src/launcher-global.ts +2 -1
- package/src/session.resubmit-from.test.ts +27 -0
- package/src/session.test.ts +51 -0
- package/src/session.ts +22 -12
- package/src/styles/widget.css +23 -1
- package/src/theme-reference.ts +3 -1
- package/src/types/theme.ts +21 -0
- package/src/types.ts +71 -6
- package/src/ui.ask-user-question-plugin.test.ts +154 -0
- package/src/ui.operator-role.test.ts +336 -0
- package/src/ui.ts +37 -12
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
- package/src/utils/brand.test.ts +14 -0
- package/src/utils/brand.ts +8 -4
- package/src/utils/cdn-release.test.ts +20 -0
- package/src/utils/cdn-release.ts +14 -0
- 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/tokens.ts +31 -0
- package/src/welcome.test.ts +16 -0
- package/src/welcome.ts +10 -2
- package/dist/chunk-BkOYJ11B.js +0 -1
- package/dist/chunk-Bz9jAgba.js +0 -1
- package/dist/chunk-CdpVP6LZ.js +0 -1
- package/dist/chunk-DM-OJt5q.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,
|
|
@@ -577,40 +578,47 @@ export const renderLoadingIndicatorWithFallback = (
|
|
|
577
578
|
*/
|
|
578
579
|
const createAvatar = (
|
|
579
580
|
avatarConfig: AgentWidgetAvatarConfig,
|
|
580
|
-
role:
|
|
581
|
+
role: AgentWidgetMessageRole
|
|
581
582
|
): HTMLElement => {
|
|
582
583
|
const avatar = createElement(
|
|
583
584
|
"div",
|
|
584
585
|
"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
586
|
);
|
|
586
587
|
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
588
|
+
const isOperator = role === "operator";
|
|
589
|
+
const avatarContent = role === "user"
|
|
590
|
+
? avatarConfig.userAvatar
|
|
591
|
+
: isOperator
|
|
592
|
+
? avatarConfig.operatorAvatar
|
|
593
|
+
: avatarConfig.assistantAvatar;
|
|
594
|
+
|
|
595
|
+
// The operator avatar is a neutral outlined chip, apart from the solid
|
|
596
|
+
// user and assistant fills.
|
|
597
|
+
const applyFill = () => {
|
|
598
|
+
avatar.classList.add(
|
|
599
|
+
...(isOperator
|
|
600
|
+
? ["persona-bg-persona-container", "persona-text-persona-text", "persona-border"]
|
|
601
|
+
: [role === "user" ? "persona-bg-persona-accent" : "persona-bg-persona-primary", "persona-text-white"])
|
|
602
|
+
);
|
|
603
|
+
};
|
|
590
604
|
|
|
591
605
|
if (avatarContent) {
|
|
592
606
|
// Check if it's a URL or emoji/text
|
|
593
607
|
if (avatarContent.startsWith("http") || avatarContent.startsWith("/") || avatarContent.startsWith("data:")) {
|
|
594
608
|
const img = createElement("img") as HTMLImageElement;
|
|
595
609
|
img.src = avatarContent;
|
|
596
|
-
img.alt = role === "user" ? "User" : "Assistant";
|
|
610
|
+
img.alt = role === "user" ? "User" : isOperator ? "Operator" : "Assistant";
|
|
597
611
|
img.className = "persona-w-full persona-h-full persona-rounded-full persona-object-cover";
|
|
598
612
|
avatar.appendChild(img);
|
|
599
613
|
} else {
|
|
600
614
|
// Emoji or text
|
|
601
615
|
avatar.textContent = avatarContent;
|
|
602
|
-
|
|
603
|
-
role === "user" ? "persona-bg-persona-accent" : "persona-bg-persona-primary",
|
|
604
|
-
"persona-text-white"
|
|
605
|
-
);
|
|
616
|
+
applyFill();
|
|
606
617
|
}
|
|
607
618
|
} else {
|
|
608
619
|
// 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
|
-
);
|
|
620
|
+
avatar.textContent = role === "user" ? "U" : isOperator ? "O" : "A";
|
|
621
|
+
applyFill();
|
|
614
622
|
}
|
|
615
623
|
|
|
616
624
|
return avatar;
|
|
@@ -644,13 +652,32 @@ const createTimestamp = (
|
|
|
644
652
|
return timestamp;
|
|
645
653
|
};
|
|
646
654
|
|
|
655
|
+
/** Assistant color utilities an operator bubble replaces with its own tokens. */
|
|
656
|
+
const OPERATOR_DROPPED_CLASSES = new Set([
|
|
657
|
+
"persona-bg-persona-container",
|
|
658
|
+
"persona-text-persona-text",
|
|
659
|
+
"persona-border-persona-message-border",
|
|
660
|
+
]);
|
|
661
|
+
|
|
647
662
|
/**
|
|
648
663
|
* Get bubble classes based on layout preset
|
|
649
664
|
*/
|
|
650
665
|
export const getBubbleClasses = (
|
|
651
|
-
role:
|
|
666
|
+
role: AgentWidgetMessageRole,
|
|
652
667
|
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
653
668
|
): string[] => {
|
|
669
|
+
// Operator bubbles share the assistant's geometry per layout; only the role
|
|
670
|
+
// class changes, and the colors come from the operator tokens instead.
|
|
671
|
+
if (role === "operator") {
|
|
672
|
+
const classes = getBubbleClasses("assistant", layout).flatMap((name) =>
|
|
673
|
+
name === "persona-message-assistant-bubble"
|
|
674
|
+
? ["persona-message-operator-bubble"]
|
|
675
|
+
: OPERATOR_DROPPED_CLASSES.has(name) ? [] : [name]
|
|
676
|
+
);
|
|
677
|
+
// Flat drops the assistant's fill, not the operator's: the operator keeps
|
|
678
|
+
// its background, so it needs the minimal layout's side padding.
|
|
679
|
+
return layout === "flat" ? [...classes, "persona-px-3"] : classes;
|
|
680
|
+
}
|
|
654
681
|
const baseClasses = ["persona-message-bubble"];
|
|
655
682
|
|
|
656
683
|
switch (layout) {
|
|
@@ -763,7 +790,7 @@ export const applyBubbleGeometry = (
|
|
|
763
790
|
tokens?: MessageGeometryTokens
|
|
764
791
|
): void => {
|
|
765
792
|
if (!tokens) return;
|
|
766
|
-
const key = role === "user"
|
|
793
|
+
const key = role === "user" || role === "operator" ? role : "assistant";
|
|
767
794
|
const preset = BUBBLE_GEOMETRY_DEFAULTS[layout ?? "bubble"] ?? BUBBLE_GEOMETRY_DEFAULTS.bubble;
|
|
768
795
|
if (tokens.padding) {
|
|
769
796
|
bubble.style.padding = `var(--persona-message-${key}-padding, ${preset.padding})`;
|
|
@@ -787,13 +814,14 @@ export const customMessageActionDataAction = (id: string): string =>
|
|
|
787
814
|
|
|
788
815
|
/**
|
|
789
816
|
* Host-contributed actions this message's role qualifies for. Assistant-only by
|
|
790
|
-
* default, matching every built-in except edit and quote
|
|
817
|
+
* default, matching every built-in except edit and quote; an action reaches
|
|
818
|
+
* operator messages only when its `roles` lists `"operator"`.
|
|
791
819
|
*/
|
|
792
820
|
export const customMessageActionsForRole = (
|
|
793
821
|
actionsConfig: AgentWidgetMessageActionsConfig | undefined,
|
|
794
822
|
role: AgentWidgetMessage["role"]
|
|
795
823
|
): AgentWidgetCustomMessageAction[] => {
|
|
796
|
-
if (role !== "user" && role !== "assistant") return [];
|
|
824
|
+
if (role !== "user" && role !== "assistant" && role !== "operator") return [];
|
|
797
825
|
return (actionsConfig?.custom ?? []).filter((action) =>
|
|
798
826
|
(action.roles ?? ["assistant"]).includes(role)
|
|
799
827
|
);
|
|
@@ -1000,21 +1028,33 @@ export const createStandardBubble = (
|
|
|
1000
1028
|
bubble.id = `bubble-${message.id}`;
|
|
1001
1029
|
bubble.setAttribute("data-message-id", message.id);
|
|
1002
1030
|
|
|
1003
|
-
|
|
1031
|
+
const isOperator = message.role === "operator";
|
|
1032
|
+
// The V5 operator is outlined, not filled, so every layout needs the border
|
|
1033
|
+
// style that only the bubble layout's classes carry.
|
|
1034
|
+
if (isOperator && options?.widgetConfig?.future?.v5Defaults === true) bubble.classList.add("persona-border");
|
|
1035
|
+
bubble.setAttribute(
|
|
1036
|
+
"data-persona-theme-zone",
|
|
1037
|
+
message.role === "user" ? "user-message" : isOperator ? "operator-message" : "assistant-message"
|
|
1038
|
+
);
|
|
1004
1039
|
|
|
1005
1040
|
const defaultMessageTokens = resolveThemeDefaults(options?.widgetConfig?.future).components.message;
|
|
1006
1041
|
const messageTokens = options?.widgetConfig?.theme?.components?.message;
|
|
1042
|
+
const darkMessageTokens = options?.widgetConfig?.darkTheme?.components?.message;
|
|
1007
1043
|
applyBubbleGeometry(
|
|
1008
1044
|
bubble,
|
|
1009
1045
|
message.role,
|
|
1010
1046
|
layout,
|
|
1011
1047
|
message.role === "user"
|
|
1012
|
-
? { ...defaultMessageTokens.user, ...messageTokens?.user, ...
|
|
1013
|
-
:
|
|
1048
|
+
? { ...defaultMessageTokens.user, ...messageTokens?.user, ...darkMessageTokens?.user }
|
|
1049
|
+
: isOperator
|
|
1050
|
+
? { ...defaultMessageTokens.operator, ...messageTokens?.operator, ...darkMessageTokens?.operator }
|
|
1051
|
+
: { ...defaultMessageTokens.assistant, ...messageTokens?.assistant, ...darkMessageTokens?.assistant }
|
|
1014
1052
|
);
|
|
1015
1053
|
|
|
1016
|
-
// Apply component-level color overrides via CSS variables
|
|
1017
|
-
|
|
1054
|
+
// Apply component-level color overrides via CSS variables. Flat clears the
|
|
1055
|
+
// user and assistant fills; an operator keeps its own background token so it
|
|
1056
|
+
// stays apart from the assistant prose on the same side.
|
|
1057
|
+
if (layout === "flat" && !isOperator) {
|
|
1018
1058
|
bubble.style.backgroundColor = "transparent";
|
|
1019
1059
|
bubble.style.color =
|
|
1020
1060
|
message.role === "user"
|
|
@@ -1026,6 +1066,23 @@ export const createStandardBubble = (
|
|
|
1026
1066
|
} else if (message.role === "assistant") {
|
|
1027
1067
|
bubble.style.backgroundColor = 'var(--persona-message-assistant-bg, var(--persona-container))';
|
|
1028
1068
|
bubble.style.color = 'var(--persona-message-assistant-text, var(--persona-text))';
|
|
1069
|
+
} else if (isOperator) {
|
|
1070
|
+
bubble.style.backgroundColor = 'var(--persona-components-message-operator-background, var(--persona-container))';
|
|
1071
|
+
bubble.style.color = 'var(--persona-components-message-operator-text, var(--persona-text))';
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (isOperator) {
|
|
1075
|
+
const authorLabel = createElement("div", "persona-message-author persona-text-xs persona-font-medium persona-mb-1");
|
|
1076
|
+
const configuredLabel = options?.widgetConfig?.copy?.operatorLabel;
|
|
1077
|
+
const operatorLabel = typeof configuredLabel === "string" && configuredLabel.trim()
|
|
1078
|
+
? configuredLabel.trim() : "Human operator";
|
|
1079
|
+
const displayName = typeof message.author?.displayName === "string"
|
|
1080
|
+
? message.author.displayName.trim() : "";
|
|
1081
|
+
authorLabel.textContent = displayName ? `${displayName} · ${operatorLabel}` : operatorLabel;
|
|
1082
|
+
// A visible name is also the accessible name for the human-authored group.
|
|
1083
|
+
bubble.setAttribute("role", "group");
|
|
1084
|
+
bubble.setAttribute("aria-label", authorLabel.textContent);
|
|
1085
|
+
bubble.appendChild(authorLabel);
|
|
1029
1086
|
}
|
|
1030
1087
|
|
|
1031
1088
|
const imageParts = getMessageImageParts(message);
|
|
@@ -1293,14 +1350,15 @@ export const createStandardBubble = (
|
|
|
1293
1350
|
bubble.appendChild(createStopReasonNotice(message.stopReason, stopReasonNoticeText));
|
|
1294
1351
|
}
|
|
1295
1352
|
|
|
1296
|
-
// Add message actions for assistant messages (only when not
|
|
1353
|
+
// Add message actions for assistant and operator messages (only when not
|
|
1354
|
+
// streaming and has content). Operator rows get copy and opted-in custom
|
|
1355
|
+
// actions only: feedback, read-aloud and regenerate are about model output.
|
|
1297
1356
|
const shouldShowActions =
|
|
1298
|
-
message.role === "assistant" &&
|
|
1357
|
+
(message.role === "assistant" || isOperator) &&
|
|
1299
1358
|
!message.streaming &&
|
|
1300
1359
|
// A voice caption (the voice model's own speech) is display-only.
|
|
1301
1360
|
!message.voiceCaption &&
|
|
1302
|
-
|
|
1303
|
-
message.content.trim() &&
|
|
1361
|
+
(messageContentText.length > 0 || (isOperator && customMessageActionsForRole(actionsConfig, "operator").length > 0)) &&
|
|
1304
1362
|
actionsConfig?.enabled !== false;
|
|
1305
1363
|
|
|
1306
1364
|
// User bubbles carry actions only when this message is edit- or quote-eligible;
|
|
@@ -1321,7 +1379,18 @@ export const createStandardBubble = (
|
|
|
1321
1379
|
shouldShowUserActions
|
|
1322
1380
|
? // A user bubble never hosts the assistant-only controls.
|
|
1323
1381
|
{ ...actionsConfig, showCopy: false, showUpvote: false, showDownvote: false, showReadAloud: false }
|
|
1324
|
-
:
|
|
1382
|
+
: isOperator
|
|
1383
|
+
? {
|
|
1384
|
+
...actionsConfig,
|
|
1385
|
+
showCopy: messageContentText.length > 0 && actionsConfig.showCopy !== false,
|
|
1386
|
+
showUpvote: false,
|
|
1387
|
+
showDownvote: false,
|
|
1388
|
+
showReadAloud: false,
|
|
1389
|
+
showRegenerate: false,
|
|
1390
|
+
showEdit: false,
|
|
1391
|
+
showQuote: false,
|
|
1392
|
+
}
|
|
1393
|
+
: actionsConfig,
|
|
1325
1394
|
actionCallbacks,
|
|
1326
1395
|
eligibility,
|
|
1327
1396
|
options?.widgetConfig?.future?.v5Defaults === true && actionsConfig.visibility === undefined
|
|
@@ -160,6 +160,20 @@ describe("4.22.0 defaults parity (flag off)", () => {
|
|
|
160
160
|
expect(actual.themeCssVariables[key], key).toBe(value);
|
|
161
161
|
delete resolvedActual.themeCssVariables[key];
|
|
162
162
|
}
|
|
163
|
+
// Operator bubbles only render for messages that use the operator role,
|
|
164
|
+
// so every token they emit is additive and none may exist in the baseline.
|
|
165
|
+
const additivePrefixes = [
|
|
166
|
+
"--persona-components-message-operator-",
|
|
167
|
+
"--persona-message-operator-",
|
|
168
|
+
];
|
|
169
|
+
expect(actual.themeCssVariables["--persona-components-message-operator-background"]).toBe(
|
|
170
|
+
"color-mix(in srgb, var(--persona-primary) 6%, var(--persona-container))"
|
|
171
|
+
);
|
|
172
|
+
for (const key of Object.keys(resolvedActual.themeCssVariables)) {
|
|
173
|
+
if (!additivePrefixes.some((prefix) => key.startsWith(prefix))) continue;
|
|
174
|
+
expect(baseline.themeCssVariables).not.toHaveProperty(key);
|
|
175
|
+
delete resolvedActual.themeCssVariables[key];
|
|
176
|
+
}
|
|
163
177
|
// Deliberately lengthen hover timing in both V4 and V5 without rewriting
|
|
164
178
|
// the historical fixture. Keyboard focus and the scan window are unchanged.
|
|
165
179
|
expect(baseline.mergedDefaultConfig.tooltip.delayMs).toBe(200);
|
package/src/index-core.ts
CHANGED
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
} from "./runtime/init";
|
|
6
6
|
|
|
7
7
|
export type {
|
|
8
|
+
AgentWidgetMessageAuthor,
|
|
9
|
+
AgentWidgetModelMessageTransform,
|
|
10
|
+
AgentWidgetModelMessageProjection,
|
|
8
11
|
AgentWidgetDictationConfig,
|
|
9
12
|
DictationProviderConfig,
|
|
10
13
|
DictationPhase,
|
|
@@ -46,6 +49,8 @@ export type {
|
|
|
46
49
|
ArtifactConfigPayload,
|
|
47
50
|
AgentWidgetInitOptions,
|
|
48
51
|
AgentWidgetMessage,
|
|
52
|
+
AgentWidgetMessageRole,
|
|
53
|
+
AgentWidgetModelMessageRole,
|
|
49
54
|
AgentWidgetTurnActivity,
|
|
50
55
|
AgentWidgetTurnActivityConfig,
|
|
51
56
|
AgentWidgetLauncherConfig,
|
|
@@ -594,6 +599,8 @@ export type {
|
|
|
594
599
|
SemanticSpacing,
|
|
595
600
|
SemanticTypography,
|
|
596
601
|
ComponentTokens,
|
|
602
|
+
MessageTokens,
|
|
603
|
+
MessageGeometryTokens,
|
|
597
604
|
ComposerOverflowMenuTokens,
|
|
598
605
|
TextStyleTokens,
|
|
599
606
|
AttachmentTokens,
|
package/src/index-global.ts
CHANGED
|
@@ -37,12 +37,13 @@ export type { StreamAnimationPlugin, StreamAnimationContext } from "./types";
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
39
|
import { setWebMcpPolyfillLoader } from "./webmcp-bridge";
|
|
40
|
+
import { pinCdnRelease } from "./utils/cdn-release";
|
|
40
41
|
|
|
41
42
|
// Capture at module-evaluation time: `document.currentScript` is null once
|
|
42
43
|
// execution leaves the script's initial synchronous run.
|
|
43
44
|
const widgetScriptSrc: string | null =
|
|
44
45
|
typeof document !== "undefined"
|
|
45
|
-
? ((document.currentScript as HTMLScriptElement | null)?.src ?? null
|
|
46
|
+
? pinCdnRelease((document.currentScript as HTMLScriptElement | null)?.src ?? "") || null
|
|
46
47
|
: null;
|
|
47
48
|
|
|
48
49
|
/**
|
package/src/install.test.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// `vi.runAllTimersAsync()` to drive it forward deterministically.
|
|
11
11
|
|
|
12
12
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { version } from "../package.json";
|
|
13
14
|
|
|
14
15
|
const PERSONA_EVENTS = [
|
|
15
16
|
"persona:script-load",
|
|
@@ -464,11 +465,11 @@ describe("install.ts: CDN base derivation", () => {
|
|
|
464
465
|
return { cssHref: css?.href ?? null, scripts };
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
it("
|
|
468
|
+
it("pins first-party latest installer assets to the embedded release", async () => {
|
|
468
469
|
setCurrentScript("https://cdn.runtype.com/persona/latest/install.global.js");
|
|
469
470
|
const { cssHref, scripts } = await installedUrls({});
|
|
470
|
-
expect(cssHref).toBe(
|
|
471
|
-
expect(scripts).toContain(
|
|
471
|
+
expect(cssHref).toBe(`https://cdn.runtype.com/persona/${version}/widget.css`);
|
|
472
|
+
expect(scripts).toContain(`https://cdn.runtype.com/persona/${version}/launcher.global.js`);
|
|
472
473
|
});
|
|
473
474
|
|
|
474
475
|
it("keeps npm-CDN siblings on the installer's own pinned range (no @latest skew)", async () => {
|
|
@@ -478,13 +479,13 @@ describe("install.ts: CDN base derivation", () => {
|
|
|
478
479
|
expect(scripts).toContain("https://cdn.jsdelivr.net/npm/@runtypelabs/persona@4/dist/launcher.global.js");
|
|
479
480
|
});
|
|
480
481
|
|
|
481
|
-
it("the full bundle
|
|
482
|
+
it("pins the full bundle on the eager first-party latest path", async () => {
|
|
482
483
|
setCurrentScript("https://cdn.runtype.com/persona/latest/install.global.js");
|
|
483
484
|
markCssLoaded();
|
|
484
485
|
await install({ config: { apiUrl: "/api", launcher: { enabled: false } } });
|
|
485
486
|
await flush();
|
|
486
487
|
const scripts = Array.from(document.head.querySelectorAll("script")).map((s) => s.src);
|
|
487
|
-
expect(scripts).toContain(
|
|
488
|
+
expect(scripts).toContain(`https://cdn.runtype.com/persona/${version}/index.global.js`);
|
|
488
489
|
});
|
|
489
490
|
|
|
490
491
|
it("falls back to jsDelivr@latest when currentScript is unavailable", async () => {
|
package/src/install.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* if configuration is provided via window.siteAgentConfig
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { pinCdnRelease } from "./utils/cdn-release";
|
|
8
8
|
|
|
9
9
|
import type { BrandConfig } from "./types/theme";
|
|
10
10
|
import { delayInstallLoading } from "./utils/install-loading";
|
|
@@ -201,7 +201,7 @@ declare global {
|
|
|
201
201
|
const getInstallerBase = (): string | null => {
|
|
202
202
|
if (!installerSrc) return null;
|
|
203
203
|
try {
|
|
204
|
-
const url = new URL(installerSrc, document.baseURI);
|
|
204
|
+
const url = new URL(pinCdnRelease(installerSrc), document.baseURI);
|
|
205
205
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
206
206
|
return url.origin + url.pathname.slice(0, url.pathname.lastIndexOf("/"));
|
|
207
207
|
} catch {
|
package/src/launcher-global.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { createLauncherSurface } from "./components/launcher";
|
|
|
24
24
|
import { applyLauncherThemeVariables } from "./utils/theme";
|
|
25
25
|
import { DEFAULT_LAUNCHER_CONFIG, DEFAULT_LAUNCHER_V5 } from "./defaults";
|
|
26
26
|
import { setIconsExtraLoader } from "./icons-extra-loader";
|
|
27
|
+
import { pinCdnRelease } from "./utils/cdn-release";
|
|
27
28
|
import type { AgentWidgetConfig } from "./types";
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
@@ -33,7 +34,7 @@ import type { AgentWidgetConfig } from "./types";
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
const launcherScriptSrc: string | null =
|
|
35
36
|
typeof document !== "undefined"
|
|
36
|
-
? ((document.currentScript as HTMLScriptElement | null)?.src ?? null
|
|
37
|
+
? pinCdnRelease((document.currentScript as HTMLScriptElement | null)?.src ?? "") || null
|
|
37
38
|
: null;
|
|
38
39
|
|
|
39
40
|
setIconsExtraLoader(() => {
|
|
@@ -327,3 +327,30 @@ describe("resubmitFrom retry payload fidelity per transport", () => {
|
|
|
327
327
|
expect(bodies[0].submitMode).toBeUndefined();
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
describe("human operator resubmission boundary", () => {
|
|
332
|
+
beforeEach(() => {
|
|
333
|
+
vi.restoreAllMocks();
|
|
334
|
+
global.fetch = vi.fn(async () => streamResponse()) as unknown as typeof fetch;
|
|
335
|
+
});
|
|
336
|
+
it.each(["retry", "edit"] as const)("refuses %s before a human contribution without cancelling or truncating", reason => {
|
|
337
|
+
const { session, emits } = makeSession();
|
|
338
|
+
seedTurn(session);
|
|
339
|
+
session.injectMessage({ id: "human", role: "operator", content: "Refund confirmed", author: { id: "sam", displayName: "Sam", kind: "human" } });
|
|
340
|
+
const snapshot = session.getMessages();
|
|
341
|
+
const cancel = vi.spyOn(session, "cancel");
|
|
342
|
+
emits.length = 0;
|
|
343
|
+
expect(session.resubmitFrom("u1", { reason, ...(reason === "edit" && { replacement: { text: "Changed", mentionRefs: [], options: {} } }) })).toBe(false);
|
|
344
|
+
expect(session.getMessages()).toEqual(snapshot);
|
|
345
|
+
expect(cancel).not.toHaveBeenCalled();
|
|
346
|
+
expect(emits).toHaveLength(0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("allows a fresh user turn after the human contribution", () => {
|
|
350
|
+
const { session } = makeSession();
|
|
351
|
+
session.injectMessage({ id: "human", role: "operator", content: "Refund confirmed" });
|
|
352
|
+
session.injectUserMessage({ id: "after", content: "When will it arrive?" });
|
|
353
|
+
expect(session.resubmitFrom("after", { reason: "retry" })).toBe(true);
|
|
354
|
+
expect(session.getMessages().some(message => message.id === "human")).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
});
|
package/src/session.test.ts
CHANGED
|
@@ -182,6 +182,16 @@ describe('AgentWidgetSession - Message Injection', () => {
|
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
describe('operator role', () => {
|
|
186
|
+
it('injects an operator message with its own id prefix', () => {
|
|
187
|
+
const result = session.injectMessage({ role: 'operator', content: 'Sam from support here.' });
|
|
188
|
+
|
|
189
|
+
expect(result.role).toBe('operator');
|
|
190
|
+
expect(result.id).toMatch(/^operator-/);
|
|
191
|
+
expect(messages.map((message) => message.role)).toEqual(['operator']);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
185
195
|
describe('message ordering', () => {
|
|
186
196
|
it('should maintain chronological order', () => {
|
|
187
197
|
session.injectMessage({
|
|
@@ -1070,6 +1080,47 @@ describe('AgentWidgetSession - dispatch error fallback', () => {
|
|
|
1070
1080
|
expect(session.getStatus()).toBe('idle');
|
|
1071
1081
|
});
|
|
1072
1082
|
|
|
1083
|
+
it('does not blame the backend for an internal stream TypeError after a successful response', async () => {
|
|
1084
|
+
const response = new Response('data: {}\n\n', { headers: { 'Content-Type': 'text/event-stream' } });
|
|
1085
|
+
const fault = new TypeError('t.streamGeneration is not a function');
|
|
1086
|
+
vi.spyOn(response.body!, 'getReader').mockImplementation(() => { throw fault; });
|
|
1087
|
+
global.fetch = vi.fn().mockResolvedValue(response);
|
|
1088
|
+
const errors: Error[] = [];
|
|
1089
|
+
const session = new AgentWidgetSession({ apiUrl: 'http://example.invalid/chat' }, {
|
|
1090
|
+
onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {},
|
|
1091
|
+
onError: error => errors.push(error),
|
|
1092
|
+
});
|
|
1093
|
+
await session.sendMessage('Hello');
|
|
1094
|
+
const content = lastAssistantMessage(session)?.content;
|
|
1095
|
+
expect(content).toContain('the chat encountered an unexpected error');
|
|
1096
|
+
expect(content).toContain('t.streamGeneration is not a function');
|
|
1097
|
+
expect(content).not.toContain('backend');
|
|
1098
|
+
expect(content).not.toContain("couldn't reach");
|
|
1099
|
+
expect(errors).toContain(fault);
|
|
1100
|
+
expect(session.isStreaming()).toBe(false);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it.each(['Failed to fetch', 'Load failed', 'NetworkError when attempting to fetch resource.', 'fetch failed'])(
|
|
1104
|
+
'recognizes the browser fetch TypeError: %s', async (message) => {
|
|
1105
|
+
failFetchWith(new TypeError(message));
|
|
1106
|
+
const session = new AgentWidgetSession({ apiUrl: 'http://example.invalid/chat' }, {
|
|
1107
|
+
onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {},
|
|
1108
|
+
});
|
|
1109
|
+
await session.sendMessage('Hello');
|
|
1110
|
+
expect(lastAssistantMessage(session)?.content).toContain("I couldn't reach the assistant");
|
|
1111
|
+
},
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
it('describes a server rejection without claiming the server was unreachable', async () => {
|
|
1115
|
+
global.fetch = vi.fn().mockResolvedValue(new Response('unavailable', { status: 503 }));
|
|
1116
|
+
const session = new AgentWidgetSession({ apiUrl: 'http://example.invalid/chat' }, {
|
|
1117
|
+
onMessagesChanged: () => {}, onStatusChanged: () => {}, onStreamingChanged: () => {},
|
|
1118
|
+
});
|
|
1119
|
+
await session.sendMessage('Hello');
|
|
1120
|
+
expect(lastAssistantMessage(session)?.content).toContain("chat service couldn't complete the request");
|
|
1121
|
+
expect(lastAssistantMessage(session)?.content).toContain('_Details: Chat backend request failed: 503_');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1073
1124
|
it('uses a static errorMessage override verbatim', async () => {
|
|
1074
1125
|
failFetchWith(new Error('boom'));
|
|
1075
1126
|
const session = new AgentWidgetSession(
|
package/src/session.ts
CHANGED
|
@@ -68,7 +68,7 @@ import type {
|
|
|
68
68
|
ComposerResubmitOptions
|
|
69
69
|
} from "./types";
|
|
70
70
|
import { applyQuoteToContent } from "./utils/composer-quote";
|
|
71
|
-
import { findUserTurnBoundary } from "./utils/resubmit-turn";
|
|
71
|
+
import { canResubmitUserMessage, findUserTurnBoundary } from "./utils/resubmit-turn";
|
|
72
72
|
import { extractComponentDirectiveFromMessage } from "./utils/component-middleware";
|
|
73
73
|
import type { MentionSubmitBundle } from "./utils/context-mention-manager";
|
|
74
74
|
import type { VisitorStore, VisitorStoreChange } from "./utils/visitor-store";
|
|
@@ -156,6 +156,7 @@ const CONNECTION_CONFIG_KEYS = [
|
|
|
156
156
|
"parserType",
|
|
157
157
|
"contextProviders",
|
|
158
158
|
"requestMiddleware",
|
|
159
|
+
"modelMessageTransform",
|
|
159
160
|
"customFetch",
|
|
160
161
|
"backgroundConversations",
|
|
161
162
|
"parseSSEEvent",
|
|
@@ -265,10 +266,9 @@ export class SessionHistoryError extends Error {
|
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* underlying reason to help with debugging.
|
|
269
|
+
* Dispatch can fail in transport, stream parsing, lazy modules or application
|
|
270
|
+
* callbacks. Only recognized fetch failures should suggest connectivity;
|
|
271
|
+
* unknown/internal failures must not send users to debug a healthy backend.
|
|
272
272
|
*
|
|
273
273
|
* Callers can override the copy via `config.errorMessage` (a static string or
|
|
274
274
|
* a function of the error). An override that returns an empty string yields ""
|
|
@@ -283,9 +283,17 @@ function buildDispatchErrorContent(
|
|
|
283
283
|
if (typeof override === "string") return override;
|
|
284
284
|
if (typeof override === "function") return override(err);
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
// Fetch rejects with TypeError too, so the error class cannot distinguish
|
|
287
|
+
// an offline browser from an internal fault such as a missing function.
|
|
288
|
+
const detail = err.message.trim();
|
|
289
|
+
const networkFailure = /^(Failed to fetch|fetch failed|Load failed|NetworkError when attempting to fetch resource\.?)$/i.test(detail);
|
|
290
|
+
const backendFailure = /^(Chat backend|Agent execution) request failed: \d{3}\b/.test(detail);
|
|
291
|
+
const base = networkFailure
|
|
292
|
+
? "Sorry: I couldn't reach the assistant. Please check your connection and try again."
|
|
293
|
+
: backendFailure
|
|
294
|
+
? "Sorry: the chat service couldn't complete the request. Please try again."
|
|
295
|
+
: "Sorry: the chat encountered an unexpected error. Please reload the page and try again.";
|
|
296
|
+
return detail ? `${base}\n\n_Details: ${detail}_` : base;
|
|
289
297
|
}
|
|
290
298
|
|
|
291
299
|
/**
|
|
@@ -2493,7 +2501,7 @@ export class AgentWidgetSession {
|
|
|
2493
2501
|
? generateUserMessageId()
|
|
2494
2502
|
: role === "assistant"
|
|
2495
2503
|
? generateAssistantMessageId()
|
|
2496
|
-
:
|
|
2504
|
+
: `${role === "operator" ? "operator" : "system"}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
2497
2505
|
|
|
2498
2506
|
return {
|
|
2499
2507
|
id: messageId,
|
|
@@ -2508,7 +2516,8 @@ export class AgentWidgetSession {
|
|
|
2508
2516
|
...(voiceProcessing !== undefined && { voiceProcessing }),
|
|
2509
2517
|
...(voiceCaption && { voiceCaption }),
|
|
2510
2518
|
...(rawContent !== undefined && { rawContent }),
|
|
2511
|
-
...(options.dispatchError !== undefined && { dispatchError: options.dispatchError })
|
|
2519
|
+
...(options.dispatchError !== undefined && { dispatchError: options.dispatchError }),
|
|
2520
|
+
...(options.author !== undefined && { author: { ...options.author } })
|
|
2512
2521
|
};
|
|
2513
2522
|
|
|
2514
2523
|
}
|
|
@@ -3008,14 +3017,15 @@ export class AgentWidgetSession {
|
|
|
3008
3017
|
* the new attempt gets fresh message ids.
|
|
3009
3018
|
*
|
|
3010
3019
|
* Returns false when the id is unknown, is not a user message, or the rebuilt
|
|
3011
|
-
* turn would carry no content
|
|
3020
|
+
* turn would carry no content, or truncation would remove an operator reply;
|
|
3021
|
+
* nothing is truncated in those cases.
|
|
3012
3022
|
*/
|
|
3013
3023
|
public resubmitFrom(
|
|
3014
3024
|
messageId: string,
|
|
3015
3025
|
options: ComposerResubmitOptions
|
|
3016
3026
|
): boolean {
|
|
3017
3027
|
const boundary = findUserTurnBoundary(this.messages, messageId);
|
|
3018
|
-
if (!boundary) return false;
|
|
3028
|
+
if (!boundary || !canResubmitUserMessage(this.messages, messageId)) return false;
|
|
3019
3029
|
|
|
3020
3030
|
// Operate on the STORED message, never a caller-held object: appendMessage
|
|
3021
3031
|
// keeps a copy (see `ensureSequence`), so the array entry is the only ref
|
package/src/styles/widget.css
CHANGED
|
@@ -2171,6 +2171,13 @@
|
|
|
2171
2171
|
);
|
|
2172
2172
|
}
|
|
2173
2173
|
|
|
2174
|
+
.persona-message-row[data-message-role="operator"] > * {
|
|
2175
|
+
max-width: var(
|
|
2176
|
+
--persona-message-row-max-width,
|
|
2177
|
+
var(--persona-message-operator-max-width, var(--persona-message-assistant-max-width, 85%))
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2174
2181
|
.persona-message-row[data-message-width="content"] > * {
|
|
2175
2182
|
flex: 0 1 auto;
|
|
2176
2183
|
}
|
|
@@ -3086,6 +3093,18 @@
|
|
|
3086
3093
|
box-shadow: var(--persona-message-assistant-shadow, 0 1px 2px 0 rgb(0 0 0 / 0.05));
|
|
3087
3094
|
}
|
|
3088
3095
|
|
|
3096
|
+
/* Operator bubble: a human on the agent's side (role "operator"). Renders on
|
|
3097
|
+
the assistant's side; V4 tints the container with the primary, V5 outlines it. */
|
|
3098
|
+
.persona-message-operator-bubble {
|
|
3099
|
+
border-radius: var(--persona-components-message-operator-borderRadius, var(--persona-radius-lg, 0.5rem));
|
|
3100
|
+
border-color: var(--persona-components-message-operator-border, var(--persona-border, #e5e7eb));
|
|
3101
|
+
border-width: var(--persona-components-message-operator-borderWidth, 1px);
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
[data-persona-root] .persona-message-operator-bubble.persona-shadow-sm {
|
|
3105
|
+
box-shadow: var(--persona-components-message-operator-shadow, 0 1px 2px 0 rgb(0 0 0 / 0.05));
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3089
3108
|
[data-persona-root] .persona-message-user-bubble.persona-shadow-sm {
|
|
3090
3109
|
box-shadow: var(--persona-message-user-shadow);
|
|
3091
3110
|
}
|
|
@@ -3244,7 +3263,8 @@
|
|
|
3244
3263
|
text-decoration: underline;
|
|
3245
3264
|
}
|
|
3246
3265
|
|
|
3247
|
-
[data-persona-root] .persona-message-assistant-bubble a
|
|
3266
|
+
[data-persona-root] .persona-message-assistant-bubble a,
|
|
3267
|
+
[data-persona-root] .persona-message-operator-bubble a {
|
|
3248
3268
|
color: var(--persona-md-link-color, var(--persona-accent, #0f0f0f));
|
|
3249
3269
|
}
|
|
3250
3270
|
|
|
@@ -3329,6 +3349,8 @@
|
|
|
3329
3349
|
|
|
3330
3350
|
.persona-message-assistant-bubble p,
|
|
3331
3351
|
.persona-message-assistant-bubble li,
|
|
3352
|
+
.persona-message-operator-bubble p,
|
|
3353
|
+
.persona-message-operator-bubble li,
|
|
3332
3354
|
.persona-error-detail p,
|
|
3333
3355
|
.persona-error-detail li {
|
|
3334
3356
|
color: inherit;
|
package/src/theme-reference.ts
CHANGED
|
@@ -144,6 +144,8 @@ export const THEME_TOKEN_DOCS = {
|
|
|
144
144
|
user: 'background, text, borderRadius, shadow.',
|
|
145
145
|
assistant:
|
|
146
146
|
'background, text, borderRadius, border (optional), shadow (optional).',
|
|
147
|
+
operator:
|
|
148
|
+
'Human operator bubble (role "operator", e.g. a support takeover), on the assistant side. background (default: container tinted 6% with the primary), text (semantic.colors.text), borderRadius, border (container tinted 20% with the primary), borderWidth (1px), shadow, plus padding, maxWidth, fontSize, fontFamily, lineHeight. V5 outlines it instead: transparent background, semantic.colors.border border.',
|
|
147
149
|
},
|
|
148
150
|
markdown: {
|
|
149
151
|
description: 'Markdown rendering in messages and artifact pane.',
|
|
@@ -325,7 +327,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
325
327
|
header:
|
|
326
328
|
'"default" | "minimal". Options: showIcon, showTitle, showSubtitle, showCloseButton, showClearChat.',
|
|
327
329
|
messages:
|
|
328
|
-
'"bubble" | "flat" | "minimal". Options: groupConsecutive, avatar (show, position, userAvatar, assistantAvatar), timestamp (show, position).',
|
|
330
|
+
'"bubble" | "flat" | "minimal". Options: groupConsecutive, avatar (show, position, userAvatar, assistantAvatar, operatorAvatar), timestamp (show, position, format).',
|
|
329
331
|
},
|
|
330
332
|
statusIndicator: {
|
|
331
333
|
description: 'Status text shown below the composer.',
|