@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.
Files changed (215) hide show
  1. package/dist/activity-ui.d.cts +3 -15
  2. package/dist/activity-ui.d.ts +3 -15
  3. package/dist/animations/glyph-cycle.d.cts +1 -3
  4. package/dist/animations/glyph-cycle.d.ts +1 -3
  5. package/dist/animations/wipe.d.cts +1 -3
  6. package/dist/animations/wipe.d.ts +1 -3
  7. package/dist/approval-ui.cjs +2 -2
  8. package/dist/approval-ui.d.cts +3 -15
  9. package/dist/approval-ui.d.ts +3 -15
  10. package/dist/approval-ui.js +1 -1
  11. package/dist/artifacts-ui.cjs +1 -1
  12. package/dist/artifacts-ui.d.cts +3 -21
  13. package/dist/artifacts-ui.d.ts +3 -21
  14. package/dist/background-runtime.d.cts +2 -3
  15. package/dist/background-runtime.d.ts +2 -3
  16. package/dist/{chunk-BbXc58OA.js → chunk-6CUrH_jm.js} +1 -1
  17. package/dist/{chunk-B_H0l3u2.js → chunk-BRM3bHcJ.js} +1 -1
  18. package/dist/{chunk-DNdeIFJ6.js → chunk-Brdx4NCz.js} +1 -1
  19. package/dist/chunk-C-rux8Q9.js +1 -0
  20. package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
  21. package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
  22. package/dist/chunk-CEPM3FOp.js +1 -0
  23. package/dist/{chunk-BpjtKnGI.js → chunk-CV8EPIhT.js} +1 -1
  24. package/dist/{chunk-DVZ8ri1z.js → chunk-CWzHNuVK.js} +1 -1
  25. package/dist/{chunk-B3l7TOOv.js → chunk-CYpt7PsO.js} +1 -1
  26. package/dist/{chunk-2FnmZ9AA.js → chunk-Csr59G5V.js} +1 -1
  27. package/dist/{chunk-B9Zu-wrk.js → chunk-CxNPSY-b.js} +1 -1
  28. package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
  29. package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
  30. package/dist/chunk-Dqa_GOcj.js +1 -0
  31. package/dist/chunk-DsWv3EGF.js +1 -0
  32. package/dist/{chunk-C7-m0EmI.js → chunk-NUNdcnAZ.js} +1 -1
  33. package/dist/{chunk-D0tcIZJM.js → chunk-YMkMvAsa.js} +2 -2
  34. package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
  35. package/dist/chunk-i4qGOJAL.js +1 -0
  36. package/dist/chunk-kizhspe5.js +1 -0
  37. package/dist/client-dispatch.js +1 -1
  38. package/dist/client-history.js +1 -1
  39. package/dist/client-stream.js +1 -1
  40. package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
  41. package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
  42. package/dist/codegen.cjs +1 -1
  43. package/dist/codegen.d.cts +1 -1
  44. package/dist/codegen.d.ts +1 -1
  45. package/dist/codegen.js +1 -1
  46. package/dist/composer-actions-runtime.cjs +1 -1
  47. package/dist/composer-actions-runtime.d.cts +4 -20
  48. package/dist/composer-actions-runtime.d.ts +4 -20
  49. package/dist/composer-actions-runtime.js +1 -1
  50. package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-i_FiUp6J.d.cts} +1 -7
  51. package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-i_FiUp6J.d.ts} +1 -7
  52. package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-DhGNgG7Z.d.cts} +1 -3
  53. package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-DhGNgG7Z.d.ts} +1 -3
  54. package/dist/config-diagnostics.d.cts +0 -2
  55. package/dist/config-diagnostics.d.ts +0 -2
  56. package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-DL-xwGfL.d.cts} +2 -6
  57. package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-DL-xwGfL.d.ts} +2 -6
  58. package/dist/context-mentions-inline.cjs +1 -1
  59. package/dist/context-mentions-inline.d.cts +2 -4
  60. package/dist/context-mentions-inline.d.ts +2 -4
  61. package/dist/context-mentions-inline.js +1 -1
  62. package/dist/context-mentions.cjs +1 -1
  63. package/dist/context-mentions.d.cts +3 -9
  64. package/dist/context-mentions.d.ts +3 -9
  65. package/dist/{defaults-CjX0kKzb.d.ts → defaults-Bga2Xrsp.d.cts} +2 -5
  66. package/dist/{defaults-CjX0kKzb.d.cts → defaults-Bga2Xrsp.d.ts} +2 -5
  67. package/dist/dictation-runtime.cjs +1 -1
  68. package/dist/dictation-runtime.d.cts +4 -13
  69. package/dist/dictation-runtime.d.ts +4 -13
  70. package/dist/dictation-runtime.js +1 -1
  71. package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.cts} +0 -2
  72. package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.ts} +0 -2
  73. package/dist/event-stream-view.cjs +1 -1
  74. package/dist/event-stream-view.d.cts +3 -16
  75. package/dist/event-stream-view.d.ts +3 -16
  76. package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-kLdyPcmk.d.cts} +1 -3
  77. package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-kLdyPcmk.d.ts} +1 -3
  78. package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-CmwZGKrf.d.cts} +1 -5
  79. package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-CmwZGKrf.d.ts} +1 -5
  80. package/dist/forms-ui.d.cts +2 -4
  81. package/dist/forms-ui.d.ts +2 -4
  82. package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-i46-lPN_.d.cts} +2 -14
  83. package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-i46-lPN_.d.ts} +2 -14
  84. package/dist/history-shell.cjs +1 -1
  85. package/dist/history-shell.d.cts +8 -27
  86. package/dist/history-shell.d.ts +8 -27
  87. package/dist/history-shell.js +1 -1
  88. package/dist/history-view.d.cts +1 -1
  89. package/dist/history-view.d.ts +1 -1
  90. package/dist/icons-extra.cjs +1 -1
  91. package/dist/icons-extra.d.cts +0 -2
  92. package/dist/icons-extra.d.ts +0 -2
  93. package/dist/index.cjs +44 -44
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.d.cts +11 -11
  96. package/dist/index.d.ts +11 -11
  97. package/dist/index.global.js +1 -1
  98. package/dist/index.global.js.map +1 -1
  99. package/dist/index.js +46 -46
  100. package/dist/index.js.map +1 -1
  101. package/dist/install.global.js +1 -1
  102. package/dist/install.global.js.map +1 -1
  103. package/dist/launcher.global.js +2 -2
  104. package/dist/launcher.global.js.map +1 -1
  105. package/dist/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-Ccr3ETd3.d.cts} +1 -3
  106. package/dist/{loading-label-runtime-loader-CPxnoaoi.d.cts → loading-label-runtime-loader-Ccr3ETd3.d.ts} +1 -3
  107. package/dist/loading-label-runtime.cjs +1 -1
  108. package/dist/loading-label-runtime.d.cts +1 -1
  109. package/dist/loading-label-runtime.d.ts +1 -1
  110. package/dist/loading-label-runtime.js +1 -1
  111. package/dist/markdown-parsers.js +1 -1
  112. package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-C6AVaH7f.d.cts} +21 -4
  113. package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-C6AVaH7f.d.ts} +21 -4
  114. package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-BVggqE3C.d.cts} +0 -2
  115. package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-BVggqE3C.d.ts} +0 -2
  116. package/dist/plugin-kit.d.cts +1 -1
  117. package/dist/plugin-kit.d.ts +1 -1
  118. package/dist/preferences-runtime.cjs +1 -1
  119. package/dist/preferences-runtime.d.cts +1 -1
  120. package/dist/preferences-runtime.d.ts +1 -1
  121. package/dist/preferences-runtime.js +1 -1
  122. package/dist/{preview-DdUvoLb4.d.cts → preview-BMdRffo4.d.cts} +2 -6
  123. package/dist/{preview-DdUvoLb4.d.ts → preview-BMdRffo4.d.ts} +2 -6
  124. package/dist/{registry-Dz4iHNST.d.ts → registry-B16l8s1b.d.cts} +1 -8
  125. package/dist/{registry-Dz4iHNST.d.cts → registry-B16l8s1b.d.ts} +1 -8
  126. package/dist/{session-Baxp9iRK.d.cts → session-CtZHe7fc.d.cts} +10 -93
  127. package/dist/{session-Baxp9iRK.d.ts → session-CtZHe7fc.d.ts} +10 -93
  128. package/dist/session-reconnect.d.cts +2 -4
  129. package/dist/session-reconnect.d.ts +2 -4
  130. package/dist/smart-dom-reader.d.cts +2 -8
  131. package/dist/smart-dom-reader.d.ts +2 -8
  132. package/dist/testing.d.cts +0 -2
  133. package/dist/testing.d.ts +0 -2
  134. package/dist/theme-editor-preview.cjs +42 -42
  135. package/dist/theme-editor-preview.d.cts +1 -1
  136. package/dist/theme-editor-preview.d.ts +1 -1
  137. package/dist/theme-editor-preview.js +25 -25
  138. package/dist/theme-editor.cjs +25 -5
  139. package/dist/theme-editor.d.cts +2 -22
  140. package/dist/theme-editor.d.ts +2 -22
  141. package/dist/theme-editor.js +1 -1
  142. package/dist/theme-reference.cjs +1 -1
  143. package/dist/theme-reference.d.cts +4 -4
  144. package/dist/theme-reference.d.ts +4 -4
  145. package/dist/theme-reference.js +1 -1
  146. package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-D2cyFvmM.d.cts} +1 -7
  147. package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-D2cyFvmM.d.ts} +1 -7
  148. package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
  149. package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
  150. package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-Do1Nd-Ow.d.cts} +2 -12
  151. package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-Do1Nd-Ow.d.ts} +2 -12
  152. package/dist/turn-activity-ui.d.cts +1 -1
  153. package/dist/turn-activity-ui.d.ts +1 -1
  154. package/dist/{types-BOZHQIk0.d.cts → types-BC7MRxO5.d.cts} +167 -36
  155. package/dist/{types-BOZHQIk0.d.ts → types-BC7MRxO5.d.ts} +167 -36
  156. package/dist/ui-extras.js +3 -3
  157. package/dist/voice-client-tools.d.cts +1 -3
  158. package/dist/voice-client-tools.d.ts +1 -3
  159. package/dist/voice-runtime.cjs +1 -1
  160. package/dist/voice-runtime.d.cts +2 -12
  161. package/dist/voice-runtime.d.ts +2 -12
  162. package/dist/voice-runtime.js +2 -2
  163. package/dist/voice-worklet-player.d.cts +2 -6
  164. package/dist/voice-worklet-player.d.ts +2 -6
  165. package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-B8g46vS0.d.cts} +1 -5
  166. package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-B8g46vS0.d.ts} +1 -5
  167. package/dist/webmcp-runtime.d.cts +1 -1
  168. package/dist/webmcp-runtime.d.ts +1 -1
  169. package/dist/widget.css +1 -1
  170. package/package.json +4 -4
  171. package/src/client-dispatch.ts +69 -16
  172. package/src/client.operator-role.test.ts +217 -0
  173. package/src/components/event-stream-view.test.ts +5 -0
  174. package/src/components/message-bubble.test.ts +169 -0
  175. package/src/components/message-bubble.ts +123 -34
  176. package/src/components/scroll-to-bottom.test.ts +135 -0
  177. package/src/components/scroll-to-bottom.ts +50 -13
  178. package/src/components/user-message-collapse.ts +117 -0
  179. package/src/defaults-parity.test.ts +14 -0
  180. package/src/defaults.ts +6 -0
  181. package/src/index-core.ts +7 -0
  182. package/src/session.resubmit-from.test.ts +27 -0
  183. package/src/session.test.ts +10 -0
  184. package/src/session.ts +8 -5
  185. package/src/styles/widget.css +79 -1
  186. package/src/theme-reference.ts +5 -3
  187. package/src/types/theme.ts +50 -0
  188. package/src/types.ts +120 -11
  189. package/src/ui.ask-user-question-plugin.test.ts +154 -0
  190. package/src/ui.operator-role.test.ts +336 -0
  191. package/src/ui.scroll-additive.test.ts +219 -117
  192. package/src/ui.scroll.test.ts +176 -13
  193. package/src/ui.ts +267 -119
  194. package/src/ui.turn-activity.test.ts +7 -7
  195. package/src/ui.user-message-collapse.test.ts +540 -0
  196. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
  197. package/src/utils/auto-follow.test.ts +0 -38
  198. package/src/utils/auto-follow.ts +0 -35
  199. package/src/utils/brand.test.ts +14 -0
  200. package/src/utils/brand.ts +8 -4
  201. package/src/utils/history-messages.test.ts +30 -0
  202. package/src/utils/history-messages.ts +26 -8
  203. package/src/utils/message-fingerprint.ts +4 -0
  204. package/src/utils/resubmit-turn.test.ts +13 -0
  205. package/src/utils/resubmit-turn.ts +12 -1
  206. package/src/utils/theme.test.ts +23 -0
  207. package/src/utils/tokens.ts +47 -0
  208. package/src/welcome.test.ts +16 -0
  209. package/src/welcome.ts +10 -2
  210. package/dist/chunk-BNE40cEG.js +0 -1
  211. package/dist/chunk-Bz9jAgba.js +0 -1
  212. package/dist/chunk-CVMIncEY.js +0 -1
  213. package/dist/chunk-CdpVP6LZ.js +0 -1
  214. package/dist/chunk-CjkechxR.js +0 -1
  215. package/dist/chunk-DgZZqy14.js +0 -1
@@ -0,0 +1,117 @@
1
+ import type { AgentWidgetConfig } from "../types";
2
+ import { createNode } from "../utils/dom";
3
+
4
+ /**
5
+ * Long user message collapse ("Show more" / "Show less").
6
+ *
7
+ * The default user bubble wraps its content (attachments, mention chips and
8
+ * text) in a clip region. ui.ts measures the region's real rendered height
9
+ * after each morph and decides whether it overflows; the bubble is then
10
+ * re-rendered from that state, so morphs and cached wrappers always agree
11
+ * with the live DOM. The full text stays in the DOM in every state (no nested
12
+ * scroller), so find-in-page, selection, copy and screen readers still reach it.
13
+ */
14
+
15
+ /**
16
+ * Default collapsed height. A bubble line is ~22–24px (0.875rem at
17
+ * leading-relaxed/1.75), so 240px shows about ten lines: enough to recognise
18
+ * the message, while leaving most of a ~640px panel for the reply that
19
+ * anchor-top streams in beneath it.
20
+ */
21
+ export const DEFAULT_USER_MESSAGE_COLLAPSE_MAX_HEIGHT = 240;
22
+
23
+ /**
24
+ * Content within this many px (about two lines) of the limit stays expanded,
25
+ * so the toggle never hides just a line or two behind a button of its own size.
26
+ */
27
+ export const USER_MESSAGE_COLLAPSE_SLACK = 48;
28
+
29
+ /**
30
+ * - `fit`: not measured yet, or short enough; no toggle.
31
+ * - `live`: an interim voice transcript; expanded so the speaker can watch it.
32
+ * - `settling`: the transcript's first final render, still expanded; the
33
+ * post-morph measurement settles it in the same pass, holding the viewport.
34
+ * - `collapsed` / `expanded`: overflowing, with the toggle.
35
+ */
36
+ export type UserMessageCollapseState = "fit" | "live" | "settling" | "collapsed" | "expanded";
37
+
38
+ export type UserMessageCollapseRender = {
39
+ state: UserMessageCollapseState;
40
+ maxHeight: number;
41
+ showMoreLabel: string;
42
+ showLessLabel: string;
43
+ };
44
+
45
+ export const USER_COLLAPSE_REGION_ATTR = "data-persona-user-collapse";
46
+ export const USER_COLLAPSE_TOGGLE_ATTR = "data-persona-user-collapse-toggle";
47
+
48
+ export const isUserMessageCollapseEnabled = (config: AgentWidgetConfig): boolean =>
49
+ config.features?.userMessageCollapse?.enabled === true;
50
+
51
+ export const resolveUserMessageCollapseMaxHeight = (config: AgentWidgetConfig): number => {
52
+ const value = config.features?.userMessageCollapse?.maxHeight;
53
+ return typeof value === "number" && Number.isFinite(value) && value > 0
54
+ ? value
55
+ : DEFAULT_USER_MESSAGE_COLLAPSE_MAX_HEIGHT;
56
+ };
57
+
58
+ export const resolveUserMessageCollapseLabels = (
59
+ config: AgentWidgetConfig
60
+ ): Pick<UserMessageCollapseRender, "showMoreLabel" | "showLessLabel"> => ({
61
+ showMoreLabel: config.copy?.showMoreLabel?.trim() || "Show more",
62
+ showLessLabel: config.copy?.showLessLabel?.trim() || "Show less",
63
+ });
64
+
65
+ /** Whether a measured natural content height warrants collapsing. */
66
+ export const userMessageOverflows = (naturalHeight: number, maxHeight: number): boolean =>
67
+ naturalHeight > maxHeight + USER_MESSAGE_COLLAPSE_SLACK;
68
+
69
+ const regionId = (messageId: string) => `persona-user-collapse-${messageId}`;
70
+
71
+ export const createUserCollapseRegion = (
72
+ messageId: string,
73
+ maxHeight: number
74
+ ): HTMLElement => {
75
+ const region = createNode("div", {
76
+ className: "persona-user-collapse-region",
77
+ attrs: { id: regionId(messageId), [USER_COLLAPSE_REGION_ATTR]: "fit" },
78
+ });
79
+ region.style.setProperty("--persona-user-collapse-max-height", `${maxHeight}px`);
80
+ return region;
81
+ };
82
+
83
+ /**
84
+ * Apply a state to a region and its sibling toggle. Used both while building a
85
+ * fresh bubble and to patch the live bubble in place on a toggle or a new
86
+ * measurement, so the toggle keeps its identity (and focus) across states.
87
+ */
88
+ export const applyUserCollapseState = (
89
+ bubble: HTMLElement,
90
+ region: HTMLElement,
91
+ messageId: string,
92
+ render: UserMessageCollapseRender
93
+ ): void => {
94
+ const { state } = render;
95
+ region.setAttribute(USER_COLLAPSE_REGION_ATTR, state);
96
+ region.style.setProperty("--persona-user-collapse-max-height", `${render.maxHeight}px`);
97
+ let toggle = bubble.querySelector<HTMLButtonElement>(`button[${USER_COLLAPSE_TOGGLE_ATTR}]`);
98
+ if (state !== "collapsed" && state !== "expanded") {
99
+ toggle?.remove();
100
+ return;
101
+ }
102
+ if (!toggle) {
103
+ toggle = createNode("button", {
104
+ className: "persona-user-collapse-toggle",
105
+ attrs: {
106
+ type: "button",
107
+ id: `persona-user-collapse-toggle-${messageId}`,
108
+ [USER_COLLAPSE_TOGGLE_ATTR]: "",
109
+ "aria-controls": regionId(messageId),
110
+ },
111
+ }) as HTMLButtonElement;
112
+ region.after(toggle);
113
+ }
114
+ const expanded = state === "expanded";
115
+ toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
116
+ toggle.textContent = expanded ? render.showLessLabel : render.showMoreLabel;
117
+ };
@@ -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/defaults.ts CHANGED
@@ -266,6 +266,9 @@ export const DEFAULTS_V5: Partial<AgentWidgetConfig> = {
266
266
  features: {
267
267
  artifacts: { enabled: true },
268
268
  scrollToBottom: { streamingIndicator: "dots", transition: "fade", scrollBehavior: "smooth" },
269
+ // Long sent messages (pasted text, voice transcripts) would otherwise fill
270
+ // the viewport under the anchor-top pin and push the reply out of view.
271
+ userMessageCollapse: { enabled: true },
269
272
  toolCallDisplay: DEFAULT_TOOL_CALL_DISPLAY_V5,
270
273
  reasoningDisplay: DEFAULT_REASONING_DISPLAY_V5,
271
274
  },
@@ -382,6 +385,8 @@ export function mergeWithDefaults(
382
385
  const csa = config.features?.streamAnimation;
383
386
  const dau = defaults.features?.askUserQuestion;
384
387
  const cau = config.features?.askUserQuestion;
388
+ const duc = defaults.features?.userMessageCollapse;
389
+ const cuc = config.features?.userMessageCollapse;
385
390
  const dh = defaults.features?.history;
386
391
  const ch = config.features?.history;
387
392
  const mergedArtifacts =
@@ -459,6 +464,7 @@ export function mergeWithDefaults(
459
464
  ...(mergedStreamAnimation !== undefined ? { streamAnimation: mergedStreamAnimation } : {}),
460
465
  ...(mergedAskUserQuestion !== undefined ? { askUserQuestion: mergedAskUserQuestion } : {}),
461
466
  ...(mergedHistory !== undefined ? { history: mergedHistory } : {}),
467
+ ...(duc !== undefined || cuc !== undefined ? { userMessageCollapse: { ...duc, ...cuc } } : {}),
462
468
  };
463
469
  })(),
464
470
  suggestionChips: config.suggestionChips ?? defaults.suggestionChips,
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,10 +3093,68 @@
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
  }
3092
3111
 
3112
+ /* Long user message collapse (features.userMessageCollapse). The region clips
3113
+ without becoming a scroller (overflow: clip), so the full text stays in the
3114
+ DOM for find, copy and screen readers with no nested scroll area. The fade
3115
+ is a mask rather than a gradient overlay, so it works on any bubble fill in
3116
+ light and dark schemes; the toggle inherits the bubble's text color. */
3117
+ .persona-user-collapse-region[data-persona-user-collapse="collapsed"] {
3118
+ max-height: var(--persona-user-collapse-max-height, 240px);
3119
+ overflow: clip;
3120
+ -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--persona-user-collapse-fade-height, 48px)), transparent);
3121
+ mask-image: linear-gradient(to bottom, #000 calc(100% - var(--persona-user-collapse-fade-height, 48px)), transparent);
3122
+ }
3123
+
3124
+ .persona-user-collapse-toggle {
3125
+ display: inline-flex;
3126
+ align-items: center;
3127
+ margin-top: 0.25rem;
3128
+ padding: 0.125rem 0;
3129
+ border: 0;
3130
+ background: none;
3131
+ color: var(--persona-user-collapse-toggle-fg, inherit);
3132
+ font-family: inherit;
3133
+ font-size: var(--persona-user-collapse-toggle-font-size, 0.8125rem);
3134
+ font-weight: var(--persona-user-collapse-toggle-font-weight, 600);
3135
+ line-height: 1.25rem;
3136
+ cursor: pointer;
3137
+ text-decoration: underline;
3138
+ text-decoration-color: transparent;
3139
+ text-underline-offset: 2px;
3140
+ transition: text-decoration-color 150ms ease;
3141
+ }
3142
+
3143
+ .persona-user-collapse-toggle:hover {
3144
+ text-decoration-color: currentColor;
3145
+ }
3146
+
3147
+ .persona-user-collapse-toggle:focus-visible {
3148
+ outline: 2px solid currentColor;
3149
+ outline-offset: 2px;
3150
+ }
3151
+
3152
+ @media (prefers-reduced-motion: reduce) {
3153
+ .persona-user-collapse-toggle {
3154
+ transition: none;
3155
+ }
3156
+ }
3157
+
3093
3158
  [data-persona-root] .persona-tool-bubble.persona-shadow-sm {
3094
3159
  box-shadow: var(--persona-tool-bubble-shadow);
3095
3160
  }
@@ -3244,7 +3309,8 @@
3244
3309
  text-decoration: underline;
3245
3310
  }
3246
3311
 
3247
- [data-persona-root] .persona-message-assistant-bubble a {
3312
+ [data-persona-root] .persona-message-assistant-bubble a,
3313
+ [data-persona-root] .persona-message-operator-bubble a {
3248
3314
  color: var(--persona-md-link-color, var(--persona-accent, #0f0f0f));
3249
3315
  }
3250
3316
 
@@ -3329,6 +3395,8 @@
3329
3395
 
3330
3396
  .persona-message-assistant-bubble p,
3331
3397
  .persona-message-assistant-bubble li,
3398
+ .persona-message-operator-bubble p,
3399
+ .persona-message-operator-bubble li,
3332
3400
  .persona-error-detail p,
3333
3401
  .persona-error-detail li {
3334
3402
  color: inherit;
@@ -4847,6 +4915,16 @@
4847
4915
  text-align: center;
4848
4916
  }
4849
4917
 
4918
+ /* A pending approval below the fold: the control widens to a labelled pill.
4919
+ Unset approval tokens keep the pill's own colors. */
4920
+ [data-persona-root] .persona-scroll-to-bottom-indicator[data-persona-scroll-to-bottom-approval] {
4921
+ border-color: var(--persona-scroll-to-bottom-approval-border, var(--persona-scroll-to-bottom-border, var(--persona-primary, #111827)));
4922
+ background: var(--persona-scroll-to-bottom-approval-bg, var(--persona-scroll-to-bottom-bg, var(--persona-button-primary-bg, var(--persona-accent, #0f0f0f))));
4923
+ color: var(--persona-scroll-to-bottom-approval-fg, var(--persona-scroll-to-bottom-fg, var(--persona-button-primary-fg, var(--persona-text-inverse, #ffffff))));
4924
+ font-weight: var(--persona-scroll-to-bottom-approval-font-weight, 500);
4925
+ white-space: nowrap;
4926
+ }
4927
+
4850
4928
  [data-persona-root] .persona-scroll-to-bottom-indicator:hover {
4851
4929
  opacity: 0.92;
4852
4930
  }
@@ -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.',
@@ -169,7 +171,7 @@ export const THEME_TOKEN_DOCS = {
169
171
  askUserQuestion: 'sheet (background, border, shadow), pill (background, foreground, borderRadius, selectedBackground, selectedForeground), customInput.background. Local features.askUserQuestion.styles still wins; navigation, labels and slide timing remain feature config.',
170
172
  attachment: 'image (background, border).',
171
173
  scrollToBottom:
172
- 'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
174
+ 'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize; approval (background, foreground, border, fontWeight) for the labelled "Approval needed" state, falling back to the control colors.',
173
175
  toolBubble: 'shadow, background, borderColor, borderWidth, borderRadius; header (background, foreground, padding), content (background, foreground, padding), codeBlock (background, foreground, borderColor), toggleForeground, labelForeground, loading (primary, secondary, duration). Applies to card and activity-row variants without changing their distinct defaults. CSS duration uses ms/s; legacy numeric milliseconds must convert explicitly. Theme content.padding precedes activity.bodyPadding/toolBubble.bodyPadding fallbacks; explicit legacy axes override only their axis.',
174
176
  scrollbar:
175
177
  'Shared scrollbar appearance for every scroller: thumb (default semantic.colors.border), track (default transparent).',
@@ -248,7 +250,7 @@ export const THEME_TOKEN_DOCS = {
248
250
  description:
249
251
  'Shared transcript + event-stream jump-to-latest affordance.',
250
252
  properties:
251
- 'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only). Defaults: enabled=true, iconName="arrow-down", label="".',
253
+ 'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only), features.scrollToBottom.approvalLabel (shown while a pending approval is below the fold; empty string keeps the regular control). Defaults: enabled=true, iconName="arrow-down", label="", approvalLabel="Approval needed".',
252
254
  },
253
255
  toolCall: {
254
256
  description:
@@ -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.',
@@ -530,6 +530,22 @@ export interface MessageActionsTokens {
530
530
  borderRadius?: TokenReference<'radius'>;
531
531
  }
532
532
 
533
+ /**
534
+ * Long user message collapse (`features.userMessageCollapse`). Every key is
535
+ * optional; unset, the fade and the toggle follow the user bubble's own text
536
+ * color and type, so they already work on any fill in light and dark schemes.
537
+ */
538
+ export interface UserMessageCollapseTokens {
539
+ /** Height of the bottom fade on a collapsed bubble. @default "48px" */
540
+ fadeHeight?: string;
541
+ /** "Show more" / "Show less" text color. Defaults to the user bubble text color. */
542
+ toggleForeground?: TokenReference<'color'>;
543
+ /** Toggle label type size. @default "0.8125rem" */
544
+ toggleFontSize?: string;
545
+ /** Toggle label weight. @default "600" */
546
+ toggleFontWeight?: string;
547
+ }
548
+
533
549
  /**
534
550
  * Bubble geometry and type, per role. Every key is unset by default and each
535
551
  * one falls back to the value the message layout preset already produced, so
@@ -580,6 +596,27 @@ export interface MessageTokens {
580
596
  /** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
581
597
  shadow?: string;
582
598
  };
599
+ /**
600
+ * Operator bubble: a human speaking on the agent's side (`role: "operator"`).
601
+ * Renders on the assistant's side. Defaults tint the container with the
602
+ * theme primary (`--persona-primary`), so `brand` themes follow without
603
+ * extra config. Geometry keys fall back to the layout preset, not to the
604
+ * assistant's geometry.
605
+ */
606
+ operator?: MessageGeometryTokens & {
607
+ /** @default "color-mix(in srgb, var(--persona-primary) 6%, var(--persona-container))" */
608
+ background?: TokenReference<'color'>;
609
+ /** @default semantic.colors.text */
610
+ text?: TokenReference<'color'>;
611
+ /** @default palette.radius.lg */
612
+ borderRadius?: TokenReference<'radius'>;
613
+ /** @default "color-mix(in srgb, var(--persona-primary) 20%, var(--persona-container))" */
614
+ border?: TokenReference<'color'>;
615
+ /** Border width; set to 0px to drop it. @default "1px" */
616
+ borderWidth?: string;
617
+ /** Operator bubble box-shadow (token ref or raw CSS, e.g. `none`). */
618
+ shadow?: string;
619
+ };
583
620
  /** Border color between messages in the thread. */
584
621
  border?: TokenReference<'color'>;
585
622
  }
@@ -1286,6 +1323,17 @@ export interface ScrollToBottomTokens extends ComponentTokenSet {
1286
1323
  fontSize?: string;
1287
1324
  fontWeight?: string;
1288
1325
  };
1326
+ /**
1327
+ * The labelled "Approval needed" state shown while a pending approval is
1328
+ * below the fold. Omitted values follow the control's own colors, so it
1329
+ * matches the theme until you give it an accent.
1330
+ */
1331
+ approval?: {
1332
+ background?: TokenReference<'color'>;
1333
+ foreground?: TokenReference<'color'>;
1334
+ border?: TokenReference<'color'>;
1335
+ fontWeight?: string;
1336
+ };
1289
1337
  }
1290
1338
 
1291
1339
  /** Visual tokens shared by one suggestion presentation variant. */
@@ -1336,6 +1384,8 @@ export interface ComponentTokens {
1336
1384
  message: MessageTokens;
1337
1385
  /** Hover-revealed per-message action row (copy, vote, read aloud). */
1338
1386
  messageActions?: MessageActionsTokens;
1387
+ /** Fade and "Show more" toggle on collapsed long user messages. */
1388
+ userMessageCollapse?: UserMessageCollapseTokens;
1339
1389
  /** Welcome / intro card shown above the message list. */
1340
1390
  introCard?: IntroCardTokens;
1341
1391
  /** Markdown surfaces (chat + artifact pane). */