@runtypelabs/persona 4.32.2 → 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.
Files changed (158) hide show
  1. package/dist/activity-ui.d.cts +3 -3
  2. package/dist/activity-ui.d.ts +3 -3
  3. package/dist/animations/glyph-cycle.d.cts +1 -1
  4. package/dist/animations/glyph-cycle.d.ts +1 -1
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/approval-ui.d.cts +2 -2
  8. package/dist/approval-ui.d.ts +2 -2
  9. package/dist/artifacts-ui.d.cts +3 -3
  10. package/dist/artifacts-ui.d.ts +3 -3
  11. package/dist/background-runtime.d.cts +1 -1
  12. package/dist/background-runtime.d.ts +1 -1
  13. package/dist/{chunk-BbXc58OA.js → chunk-6CUrH_jm.js} +1 -1
  14. package/dist/{chunk-B_H0l3u2.js → chunk-BRM3bHcJ.js} +1 -1
  15. package/dist/chunk-CEPM3FOp.js +1 -0
  16. package/dist/{chunk-D0tcIZJM.js → chunk-CXQJG7pb.js} +1 -1
  17. package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
  18. package/dist/{chunk-BpjtKnGI.js → chunk-D6U0E82p.js} +1 -1
  19. package/dist/{chunk-2FnmZ9AA.js → chunk-DOZmcve8.js} +1 -1
  20. package/dist/chunk-DVgzoVCB.js +1 -0
  21. package/dist/{chunk-B9Zu-wrk.js → chunk-DW7Vg_l2.js} +1 -1
  22. package/dist/{chunk-B3l7TOOv.js → chunk-DbGLjU2P.js} +1 -1
  23. package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
  24. package/dist/{chunk-DgZZqy14.js → chunk-d54BJdKd.js} +1 -1
  25. package/dist/chunk-dLJWKfOb.js +1 -0
  26. package/dist/{chunk-DNdeIFJ6.js → chunk-gu34lr-P.js} +1 -1
  27. package/dist/chunk-i4qGOJAL.js +1 -0
  28. package/dist/{chunk-DVZ8ri1z.js → chunk-ql5N2I_t.js} +1 -1
  29. package/dist/client-dispatch.js +1 -1
  30. package/dist/client-history.js +1 -1
  31. package/dist/codegen.cjs +1 -1
  32. package/dist/codegen.js +1 -1
  33. package/dist/composer-actions-runtime.d.cts +2 -2
  34. package/dist/composer-actions-runtime.d.ts +2 -2
  35. package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-DnqGCXZz.d.cts} +1 -1
  36. package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-DnqGCXZz.d.ts} +1 -1
  37. package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-6Iwy6WLI.d.cts} +1 -1
  38. package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-6Iwy6WLI.d.ts} +1 -1
  39. package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-B-rHF98n.d.cts} +2 -2
  40. package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-B-rHF98n.d.ts} +2 -2
  41. package/dist/context-mentions-inline.d.cts +2 -2
  42. package/dist/context-mentions-inline.d.ts +2 -2
  43. package/dist/context-mentions.d.cts +3 -3
  44. package/dist/context-mentions.d.ts +3 -3
  45. package/dist/{defaults-CjX0kKzb.d.ts → defaults-BuOjqUAG.d.cts} +2 -1
  46. package/dist/{defaults-CjX0kKzb.d.cts → defaults-BuOjqUAG.d.ts} +2 -1
  47. package/dist/dictation-runtime.d.cts +2 -2
  48. package/dist/dictation-runtime.d.ts +2 -2
  49. package/dist/event-stream-view.d.cts +1 -1
  50. package/dist/event-stream-view.d.ts +1 -1
  51. package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-SqEfkGda.d.cts} +1 -1
  52. package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-SqEfkGda.d.ts} +1 -1
  53. package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-DqAmje-2.d.cts} +1 -1
  54. package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-DqAmje-2.d.ts} +1 -1
  55. package/dist/forms-ui.d.cts +2 -2
  56. package/dist/forms-ui.d.ts +2 -2
  57. package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-B3L7QNcO.d.cts} +1 -1
  58. package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-B3L7QNcO.d.ts} +1 -1
  59. package/dist/history-shell.cjs +1 -1
  60. package/dist/history-shell.d.cts +6 -6
  61. package/dist/history-shell.d.ts +6 -6
  62. package/dist/history-shell.js +1 -1
  63. package/dist/history-view.d.cts +1 -1
  64. package/dist/history-view.d.ts +1 -1
  65. package/dist/index.cjs +38 -38
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +9 -9
  68. package/dist/index.d.ts +9 -9
  69. package/dist/index.global.js +1 -1
  70. package/dist/index.global.js.map +1 -1
  71. package/dist/index.js +52 -52
  72. package/dist/index.js.map +1 -1
  73. package/dist/install.global.js +1 -1
  74. package/dist/launcher.global.js +2 -2
  75. package/dist/launcher.global.js.map +1 -1
  76. package/dist/{loading-label-runtime-loader-CPxnoaoi.d.cts → loading-label-runtime-loader-JecjOfJS.d.cts} +1 -1
  77. package/dist/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-JecjOfJS.d.ts} +1 -1
  78. package/dist/loading-label-runtime.d.cts +1 -1
  79. package/dist/loading-label-runtime.d.ts +1 -1
  80. package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-CBYtpDsT.d.cts} +2 -2
  81. package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-CBYtpDsT.d.ts} +2 -2
  82. package/dist/preferences-runtime.d.cts +1 -1
  83. package/dist/preferences-runtime.d.ts +1 -1
  84. package/dist/{preview-DdUvoLb4.d.cts → preview-_UJwwt2L.d.cts} +2 -2
  85. package/dist/{preview-DdUvoLb4.d.ts → preview-_UJwwt2L.d.ts} +2 -2
  86. package/dist/{registry-Dz4iHNST.d.cts → registry-B5ZV3K-h.d.cts} +1 -1
  87. package/dist/{registry-Dz4iHNST.d.ts → registry-B5ZV3K-h.d.ts} +1 -1
  88. package/dist/{session-Baxp9iRK.d.cts → session-Bys8_XuB.d.cts} +10 -9
  89. package/dist/{session-Baxp9iRK.d.ts → session-Bys8_XuB.d.ts} +10 -9
  90. package/dist/session-reconnect.d.cts +2 -2
  91. package/dist/session-reconnect.d.ts +2 -2
  92. package/dist/smart-dom-reader.d.cts +1 -1
  93. package/dist/smart-dom-reader.d.ts +1 -1
  94. package/dist/theme-editor-preview.cjs +31 -31
  95. package/dist/theme-editor-preview.d.cts +1 -1
  96. package/dist/theme-editor-preview.d.ts +1 -1
  97. package/dist/theme-editor-preview.js +19 -19
  98. package/dist/theme-editor.cjs +1 -1
  99. package/dist/theme-editor.d.cts +2 -2
  100. package/dist/theme-editor.d.ts +2 -2
  101. package/dist/theme-editor.js +1 -1
  102. package/dist/theme-reference.cjs +1 -1
  103. package/dist/theme-reference.d.cts +4 -2
  104. package/dist/theme-reference.d.ts +4 -2
  105. package/dist/theme-reference.js +1 -1
  106. package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-d5qAQcwv.d.cts} +1 -1
  107. package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-d5qAQcwv.d.ts} +1 -1
  108. package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-q6fHVnx-.d.cts} +2 -2
  109. package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-q6fHVnx-.d.ts} +2 -2
  110. package/dist/turn-activity-ui.d.cts +1 -1
  111. package/dist/turn-activity-ui.d.ts +1 -1
  112. package/dist/{types-BOZHQIk0.d.cts → types-B_mj21jg.d.cts} +91 -7
  113. package/dist/{types-BOZHQIk0.d.ts → types-B_mj21jg.d.ts} +91 -7
  114. package/dist/voice-client-tools.d.cts +1 -1
  115. package/dist/voice-client-tools.d.ts +1 -1
  116. package/dist/voice-runtime.cjs +1 -1
  117. package/dist/voice-runtime.d.cts +2 -2
  118. package/dist/voice-runtime.d.ts +2 -2
  119. package/dist/voice-runtime.js +1 -1
  120. package/dist/voice-worklet-player.d.cts +2 -2
  121. package/dist/voice-worklet-player.d.ts +2 -2
  122. package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-ClEO3qdI.d.cts} +1 -1
  123. package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-ClEO3qdI.d.ts} +1 -1
  124. package/dist/webmcp-runtime.d.cts +1 -1
  125. package/dist/webmcp-runtime.d.ts +1 -1
  126. package/dist/widget.css +1 -1
  127. package/package.json +1 -1
  128. package/src/client-dispatch.ts +69 -16
  129. package/src/client.operator-role.test.ts +217 -0
  130. package/src/components/message-bubble.test.ts +169 -0
  131. package/src/components/message-bubble.ts +97 -28
  132. package/src/defaults-parity.test.ts +14 -0
  133. package/src/index-core.ts +7 -0
  134. package/src/session.resubmit-from.test.ts +27 -0
  135. package/src/session.test.ts +10 -0
  136. package/src/session.ts +8 -5
  137. package/src/styles/widget.css +23 -1
  138. package/src/theme-reference.ts +3 -1
  139. package/src/types/theme.ts +21 -0
  140. package/src/types.ts +71 -6
  141. package/src/ui.ask-user-question-plugin.test.ts +154 -0
  142. package/src/ui.operator-role.test.ts +336 -0
  143. package/src/ui.ts +37 -12
  144. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
  145. package/src/utils/brand.test.ts +14 -0
  146. package/src/utils/brand.ts +8 -4
  147. package/src/utils/history-messages.test.ts +30 -0
  148. package/src/utils/history-messages.ts +26 -8
  149. package/src/utils/message-fingerprint.ts +4 -0
  150. package/src/utils/resubmit-turn.test.ts +13 -0
  151. package/src/utils/resubmit-turn.ts +12 -1
  152. package/src/utils/tokens.ts +31 -0
  153. package/src/welcome.test.ts +16 -0
  154. package/src/welcome.ts +10 -2
  155. package/dist/chunk-BNE40cEG.js +0 -1
  156. package/dist/chunk-Bz9jAgba.js +0 -1
  157. package/dist/chunk-CdpVP6LZ.js +0 -1
  158. package/dist/chunk-CjkechxR.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: "user" | "assistant"
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 avatarContent = role === "user"
588
- ? avatarConfig.userAvatar
589
- : avatarConfig.assistantAvatar;
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
- avatar.classList.add(
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
- avatar.classList.add(
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: "user" | "assistant" | "system",
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" ? "user" : "assistant";
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
- bubble.setAttribute("data-persona-theme-zone", message.role === "user" ? "user-message" : "assistant-message");
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, ...options?.widgetConfig?.darkTheme?.components?.message?.user }
1013
- : { ...defaultMessageTokens.assistant, ...messageTokens?.assistant, ...options?.widgetConfig?.darkTheme?.components?.message?.assistant }
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
- if (layout === "flat") {
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 streaming and has content)
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
- message.content &&
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
- : actionsConfig,
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,
@@ -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
+ });
@@ -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({
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",
@@ -2500,7 +2501,7 @@ export class AgentWidgetSession {
2500
2501
  ? generateUserMessageId()
2501
2502
  : role === "assistant"
2502
2503
  ? generateAssistantMessageId()
2503
- : `system-${Date.now()}-${Math.random().toString(16).slice(2)}`);
2504
+ : `${role === "operator" ? "operator" : "system"}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
2504
2505
 
2505
2506
  return {
2506
2507
  id: messageId,
@@ -2515,7 +2516,8 @@ export class AgentWidgetSession {
2515
2516
  ...(voiceProcessing !== undefined && { voiceProcessing }),
2516
2517
  ...(voiceCaption && { voiceCaption }),
2517
2518
  ...(rawContent !== undefined && { rawContent }),
2518
- ...(options.dispatchError !== undefined && { dispatchError: options.dispatchError })
2519
+ ...(options.dispatchError !== undefined && { dispatchError: options.dispatchError }),
2520
+ ...(options.author !== undefined && { author: { ...options.author } })
2519
2521
  };
2520
2522
 
2521
2523
  }
@@ -3015,14 +3017,15 @@ export class AgentWidgetSession {
3015
3017
  * the new attempt gets fresh message ids.
3016
3018
  *
3017
3019
  * Returns false when the id is unknown, is not a user message, or the rebuilt
3018
- * turn would carry no content; nothing is truncated in that case.
3020
+ * turn would carry no content, or truncation would remove an operator reply;
3021
+ * nothing is truncated in those cases.
3019
3022
  */
3020
3023
  public resubmitFrom(
3021
3024
  messageId: string,
3022
3025
  options: ComposerResubmitOptions
3023
3026
  ): boolean {
3024
3027
  const boundary = findUserTurnBoundary(this.messages, messageId);
3025
- if (!boundary) return false;
3028
+ if (!boundary || !canResubmitUserMessage(this.messages, messageId)) return false;
3026
3029
 
3027
3030
  // Operate on the STORED message, never a caller-held object: appendMessage
3028
3031
  // keeps a copy (see `ensureSequence`), so the array entry is the only ref
@@ -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;
@@ -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.',
@@ -580,6 +580,27 @@ export interface MessageTokens {
580
580
  /** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
581
581
  shadow?: string;
582
582
  };
583
+ /**
584
+ * Operator bubble: a human speaking on the agent's side (`role: "operator"`).
585
+ * Renders on the assistant's side. Defaults tint the container with the
586
+ * theme primary (`--persona-primary`), so `brand` themes follow without
587
+ * extra config. Geometry keys fall back to the layout preset, not to the
588
+ * assistant's geometry.
589
+ */
590
+ operator?: MessageGeometryTokens & {
591
+ /** @default "color-mix(in srgb, var(--persona-primary) 6%, var(--persona-container))" */
592
+ background?: TokenReference<'color'>;
593
+ /** @default semantic.colors.text */
594
+ text?: TokenReference<'color'>;
595
+ /** @default palette.radius.lg */
596
+ borderRadius?: TokenReference<'radius'>;
597
+ /** @default "color-mix(in srgb, var(--persona-primary) 20%, var(--persona-container))" */
598
+ border?: TokenReference<'color'>;
599
+ /** Border width; set to 0px to drop it. @default "1px" */
600
+ borderWidth?: string;
601
+ /** Operator bubble box-shadow (token ref or raw CSS, e.g. `none`). */
602
+ shadow?: string;
603
+ };
583
604
  /** Border color between messages in the thread. */
584
605
  border?: TokenReference<'color'>;
585
606
  }
package/src/types.ts CHANGED
@@ -616,7 +616,7 @@ export type AgentWidgetReplayedToolResult = {
616
616
  * from its own stored transcript there.
617
617
  */
618
618
  export type AgentWidgetRequestPayloadMessage = {
619
- role: AgentWidgetMessageRole | "tool";
619
+ role: AgentWidgetModelMessageRole | "tool";
620
620
  content: MessageContent;
621
621
  createdAt: string;
622
622
  /** Optional assistant phase for backend adapters that replay provider history. */
@@ -1123,6 +1123,21 @@ export type AgentMessageMetadata = {
1123
1123
  };
1124
1124
  };
1125
1125
 
1126
+ /** Ordinary model context only; the serializer owns tool-call/result pairing. */
1127
+ export type AgentWidgetModelMessageProjection = Pick<AgentWidgetRequestPayloadMessage, "content" | "createdAt" | "phase"> & {
1128
+ role: AgentWidgetModelMessageRole;
1129
+ };
1130
+
1131
+ /** Synchronous, per-message conversion before any dispatch transport serializes history. */
1132
+ export type AgentWidgetModelMessageTransform = (context: {
1133
+ /** Full UI transcript entry, including identity and human provenance. */
1134
+ message: Readonly<AgentWidgetMessage>;
1135
+ /** Resolved model content: contentParts > llmContent > rawContent > content. */
1136
+ content: MessageContent;
1137
+ /** Normal model projection; null for operator messages. Return null to omit. */
1138
+ modelMessage: AgentWidgetModelMessageProjection | null;
1139
+ }) => AgentWidgetModelMessageProjection | null;
1140
+
1126
1141
  export type AgentWidgetRequestMiddlewareContext = {
1127
1142
  payload: AgentWidgetRequestPayload;
1128
1143
  config: AgentWidgetConfig;
@@ -1391,7 +1406,7 @@ export type AgentWidgetMessageActionsConfig = {
1391
1406
  };
1392
1407
 
1393
1408
  /** Which roles a custom message action appears on. */
1394
- export type AgentWidgetCustomMessageActionRole = "user" | "assistant";
1409
+ export type AgentWidgetCustomMessageActionRole = "user" | "assistant" | "operator";
1395
1410
 
1396
1411
  /** One host-contributed message action. */
1397
1412
  export type AgentWidgetCustomMessageAction = {
@@ -7191,6 +7206,11 @@ export type AgentWidgetAvatarConfig = {
7191
7206
  userAvatar?: string;
7192
7207
  /** URL or emoji for assistant avatar */
7193
7208
  assistantAvatar?: string;
7209
+ /**
7210
+ * URL or emoji for operator (human takeover) avatar. Defaults to the letter
7211
+ * "O" on a neutral fill.
7212
+ */
7213
+ operatorAvatar?: string;
7194
7214
  };
7195
7215
 
7196
7216
  /**
@@ -7246,7 +7266,10 @@ export type AgentWidgetMessageLayoutConfig = {
7246
7266
  groupConsecutive?: boolean;
7247
7267
  /** Width behavior for user-authored message rows */
7248
7268
  user?: AgentWidgetMessageRoleLayout;
7249
- /** Width behavior for assistant-authored message rows and assistant UI variants */
7269
+ /**
7270
+ * Width behavior for assistant-authored message rows and assistant UI
7271
+ * variants. Operator rows sit on the same side and use it too.
7272
+ */
7250
7273
  assistant?: AgentWidgetMessageRoleLayout;
7251
7274
  /**
7252
7275
  * Custom renderer for user messages
@@ -8398,6 +8421,8 @@ export type AgentWidgetConfig = {
8398
8421
  */
8399
8422
  getHeaders?: AgentWidgetHeadersFunction;
8400
8423
  copy?: {
8424
+ /** Visible and accessible label for human operator replies. @default "Human operator" */
8425
+ operatorLabel?: string;
8401
8426
  /** @deprecated Use `welcome.title`. */
8402
8427
  welcomeTitle?: string;
8403
8428
  /** @deprecated Use `welcome.subtitle`. */
@@ -8590,6 +8615,16 @@ export type AgentWidgetConfig = {
8590
8615
  * See `docs/context-mentions-plan.md`.
8591
8616
  */
8592
8617
  contextMentions?: AgentWidgetContextMentionConfig;
8618
+ /**
8619
+ * Synchronous model-context conversion for each nonempty conversation entry.
8620
+ * Runs on proxy, agent and client-token dispatch, after local-only notices and
8621
+ * voice captions are excluded. By default operator messages are omitted;
8622
+ * stateless adapters can explicitly map their historical human contributions
8623
+ * into supported model roles. Do not replay contributions already owned by
8624
+ * server history. This does not authorize steering, takeover or tool approval.
8625
+ * Invalid return values fail dispatch. See docs/MESSAGE-INJECTION.md.
8626
+ */
8627
+ modelMessageTransform?: AgentWidgetModelMessageTransform;
8593
8628
  requestMiddleware?: AgentWidgetRequestMiddleware;
8594
8629
  actionParsers?: AgentWidgetActionParser[];
8595
8630
  actionHandlers?: AgentWidgetActionHandler[];
@@ -9093,7 +9128,25 @@ type ConfigPatch<T> = T extends (...args: never[]) => unknown
9093
9128
  ? { [K in keyof T]?: ConfigPatch<T[K]> }
9094
9129
  : T;
9095
9130
 
9096
- export type AgentWidgetMessageRole = "user" | "assistant" | "system";
9131
+ /**
9132
+ * Who authored a message.
9133
+ *
9134
+ * - `user`: the end user.
9135
+ * - `assistant`: the agent (model output).
9136
+ * - `system`: context for the model, rendered like an assistant row.
9137
+ * - `operator`: a human speaking on the agent's side of the conversation, such
9138
+ * as a support agent who has taken over the thread. It renders on the
9139
+ * assistant's side with its own bubble style (`theme.components.message.operator`).
9140
+ * Operator messages are omitted from model requests by default. A stateless
9141
+ * adapter can explicitly convert them with `modelMessageTransform`.
9142
+ */
9143
+ export type AgentWidgetMessageRole = "user" | "assistant" | "system" | "operator";
9144
+
9145
+ /**
9146
+ * Roles that reach a model request. Human operator UI entries must be explicitly
9147
+ * converted to a supported model role by an adapter.
9148
+ */
9149
+ export type AgentWidgetModelMessageRole = Exclude<AgentWidgetMessageRole, "operator">;
9097
9150
 
9098
9151
  export type AgentWidgetReasoning = {
9099
9152
  id: string;
@@ -9216,11 +9269,18 @@ export type AgentWidgetTurnActivityConfig = {
9216
9269
  }) => HTMLElement | string | null | undefined;
9217
9270
  };
9218
9271
 
9272
+ /** Display provenance, supplied by the host; never an authorization credential. */
9273
+ export type AgentWidgetMessageAuthor = {
9274
+ id?: string;
9275
+ displayName?: string;
9276
+ kind?: "human" | "ai" | "system";
9277
+ };
9278
+
9219
9279
  /**
9220
9280
  * Represents a message in the chat conversation.
9221
9281
  *
9222
9282
  * @property id - Unique message identifier
9223
- * @property role - Message role: "user", "assistant", or "system"
9283
+ * @property role - Message role: "user", "assistant", "system", or "operator"
9224
9284
  * @property content - Message text content (for display)
9225
9285
  * @property contentParts - Original multi-modal content parts (for API requests)
9226
9286
  * @property createdAt - ISO timestamp when message was created
@@ -9234,6 +9294,8 @@ export type AgentWidgetTurnActivityConfig = {
9234
9294
  * Useful for implementing voice-specific behaviors like auto-reactivation.
9235
9295
  */
9236
9296
  export type AgentWidgetMessage = {
9297
+ /** Preserved by injection, history and persistence; not sent to models automatically. */
9298
+ author?: AgentWidgetMessageAuthor;
9237
9299
  /** Opt-in whole-turn presentation metadata. Does not change model-visible content. */
9238
9300
  turnActivity?: AgentWidgetTurnActivity;
9239
9301
  id: string;
@@ -9388,10 +9450,13 @@ export type AgentWidgetMessage = {
9388
9450
  * }
9389
9451
  */
9390
9452
  export type InjectMessageOptions = {
9453
+ /** Host-supplied display provenance. Does not grant model or backend authority. */
9454
+ author?: AgentWidgetMessageAuthor;
9391
9455
  /** Mark assistant content as dispatch failure detail for `errorDisplay`. */
9392
9456
  dispatchError?: boolean;
9393
9457
  /**
9394
- * Message role: "assistant", "user", or "system"
9458
+ * Message role: "assistant", "user", "system", or "operator". Operator
9459
+ * messages are omitted from requests unless modelMessageTransform converts them.
9395
9460
  */
9396
9461
  role: AgentWidgetMessageRole;
9397
9462