@runtypelabs/persona 4.33.0 → 4.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-ui.d.cts +3 -15
- package/dist/activity-ui.d.ts +3 -15
- package/dist/animations/glyph-cycle.d.cts +1 -3
- package/dist/animations/glyph-cycle.d.ts +1 -3
- package/dist/animations/wipe.d.cts +1 -3
- package/dist/animations/wipe.d.ts +1 -3
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +3 -15
- package/dist/approval-ui.d.ts +3 -15
- package/dist/approval-ui.js +1 -1
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +3 -21
- package/dist/artifacts-ui.d.ts +3 -21
- package/dist/background-runtime.d.cts +2 -3
- package/dist/background-runtime.d.ts +2 -3
- package/dist/{chunk-gu34lr-P.js → chunk-Brdx4NCz.js} +1 -1
- package/dist/{chunk-dLJWKfOb.js → chunk-C-rux8Q9.js} +1 -1
- package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
- package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
- package/dist/{chunk-D6U0E82p.js → chunk-CV8EPIhT.js} +1 -1
- package/dist/{chunk-ql5N2I_t.js → chunk-CWzHNuVK.js} +1 -1
- package/dist/{chunk-DbGLjU2P.js → chunk-CYpt7PsO.js} +1 -1
- package/dist/{chunk-DOZmcve8.js → chunk-Csr59G5V.js} +1 -1
- package/dist/{chunk-DW7Vg_l2.js → chunk-CxNPSY-b.js} +1 -1
- package/dist/chunk-Dqa_GOcj.js +1 -0
- package/dist/chunk-DsWv3EGF.js +1 -0
- package/dist/{chunk-C7-m0EmI.js → chunk-NUNdcnAZ.js} +1 -1
- package/dist/{chunk-CXQJG7pb.js → chunk-YMkMvAsa.js} +2 -2
- package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
- package/dist/chunk-kizhspe5.js +1 -0
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
- package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +4 -20
- package/dist/composer-actions-runtime.d.ts +4 -20
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-DnqGCXZz.d.ts → composer-input-i_FiUp6J.d.cts} +1 -7
- package/dist/{composer-input-DnqGCXZz.d.cts → composer-input-i_FiUp6J.d.ts} +1 -7
- package/dist/{composer-store-6Iwy6WLI.d.ts → composer-store-DhGNgG7Z.d.cts} +1 -3
- package/dist/{composer-store-6Iwy6WLI.d.cts → composer-store-DhGNgG7Z.d.ts} +1 -3
- package/dist/config-diagnostics.d.cts +0 -2
- package/dist/config-diagnostics.d.ts +0 -2
- package/dist/{context-mention-manager-B-rHF98n.d.ts → context-mention-manager-DL-xwGfL.d.cts} +2 -6
- package/dist/{context-mention-manager-B-rHF98n.d.cts → context-mention-manager-DL-xwGfL.d.ts} +2 -6
- package/dist/context-mentions-inline.cjs +1 -1
- package/dist/context-mentions-inline.d.cts +2 -4
- package/dist/context-mentions-inline.d.ts +2 -4
- package/dist/context-mentions-inline.js +1 -1
- package/dist/context-mentions.cjs +1 -1
- package/dist/context-mentions.d.cts +3 -9
- package/dist/context-mentions.d.ts +3 -9
- package/dist/{defaults-BuOjqUAG.d.ts → defaults-Bga2Xrsp.d.cts} +1 -5
- package/dist/{defaults-BuOjqUAG.d.cts → defaults-Bga2Xrsp.d.ts} +1 -5
- package/dist/dictation-runtime.cjs +1 -1
- package/dist/dictation-runtime.d.cts +4 -13
- package/dist/dictation-runtime.d.ts +4 -13
- package/dist/dictation-runtime.js +1 -1
- package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.cts} +0 -2
- package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.ts} +0 -2
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +3 -16
- package/dist/event-stream-view.d.ts +3 -16
- package/dist/{fallback-speech-engine-SqEfkGda.d.ts → fallback-speech-engine-kLdyPcmk.d.cts} +1 -3
- package/dist/{fallback-speech-engine-SqEfkGda.d.cts → fallback-speech-engine-kLdyPcmk.d.ts} +1 -3
- package/dist/{feature-preferences-DqAmje-2.d.ts → feature-preferences-CmwZGKrf.d.cts} +1 -5
- package/dist/{feature-preferences-DqAmje-2.d.cts → feature-preferences-CmwZGKrf.d.ts} +1 -5
- package/dist/forms-ui.d.cts +2 -4
- package/dist/forms-ui.d.ts +2 -4
- package/dist/{history-confirm-dialog-B3L7QNcO.d.cts → history-confirm-dialog-i46-lPN_.d.cts} +2 -14
- package/dist/{history-confirm-dialog-B3L7QNcO.d.ts → history-confirm-dialog-i46-lPN_.d.ts} +2 -14
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +8 -27
- package/dist/history-shell.d.ts +8 -27
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/icons-extra.cjs +1 -1
- package/dist/icons-extra.d.cts +0 -2
- package/dist/icons-extra.d.ts +0 -2
- package/dist/index.cjs +45 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +49 -49
- 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-JecjOfJS.d.ts → loading-label-runtime-loader-Ccr3ETd3.d.cts} +1 -3
- package/dist/{loading-label-runtime-loader-JecjOfJS.d.cts → loading-label-runtime-loader-Ccr3ETd3.d.ts} +1 -3
- package/dist/loading-label-runtime.cjs +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/loading-label-runtime.js +1 -1
- package/dist/markdown-parsers.js +1 -1
- package/dist/{message-bubble-CBYtpDsT.d.ts → message-bubble-C6AVaH7f.d.cts} +20 -3
- package/dist/{message-bubble-CBYtpDsT.d.cts → message-bubble-C6AVaH7f.d.ts} +20 -3
- package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-BVggqE3C.d.cts} +0 -2
- package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-BVggqE3C.d.ts} +0 -2
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/preferences-runtime.cjs +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/preferences-runtime.js +1 -1
- package/dist/{preview-_UJwwt2L.d.cts → preview-BMdRffo4.d.cts} +2 -6
- package/dist/{preview-_UJwwt2L.d.ts → preview-BMdRffo4.d.ts} +2 -6
- package/dist/{registry-B5ZV3K-h.d.ts → registry-B16l8s1b.d.cts} +1 -8
- package/dist/{registry-B5ZV3K-h.d.cts → registry-B16l8s1b.d.ts} +1 -8
- package/dist/{session-Bys8_XuB.d.cts → session-CtZHe7fc.d.cts} +8 -92
- package/dist/{session-Bys8_XuB.d.ts → session-CtZHe7fc.d.ts} +8 -92
- package/dist/session-reconnect.d.cts +2 -4
- package/dist/session-reconnect.d.ts +2 -4
- package/dist/smart-dom-reader.d.cts +2 -8
- package/dist/smart-dom-reader.d.ts +2 -8
- package/dist/testing.d.cts +0 -2
- package/dist/testing.d.ts +0 -2
- package/dist/theme-editor-preview.cjs +42 -42
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +25 -25
- package/dist/theme-editor.cjs +25 -5
- package/dist/theme-editor.d.cts +2 -22
- package/dist/theme-editor.d.ts +2 -22
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -4
- package/dist/theme-reference.d.ts +2 -4
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-d5qAQcwv.d.ts → tool-loading-animation-D2cyFvmM.d.cts} +1 -7
- package/dist/{tool-loading-animation-d5qAQcwv.d.cts → tool-loading-animation-D2cyFvmM.d.ts} +1 -7
- package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
- package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
- package/dist/{turn-activity-ui-q6fHVnx-.d.cts → turn-activity-ui-Do1Nd-Ow.d.cts} +2 -12
- package/dist/{turn-activity-ui-q6fHVnx-.d.ts → turn-activity-ui-Do1Nd-Ow.d.ts} +2 -12
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-B_mj21jg.d.cts → types-BC7MRxO5.d.cts} +76 -29
- package/dist/{types-B_mj21jg.d.ts → types-BC7MRxO5.d.ts} +76 -29
- package/dist/ui-extras.js +3 -3
- package/dist/voice-client-tools.d.cts +1 -3
- package/dist/voice-client-tools.d.ts +1 -3
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -12
- package/dist/voice-runtime.d.ts +2 -12
- package/dist/voice-runtime.js +2 -2
- package/dist/voice-worklet-player.d.cts +2 -6
- package/dist/voice-worklet-player.d.ts +2 -6
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.ts → webmcp-runtime-entry-B8g46vS0.d.cts} +1 -5
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.cts → webmcp-runtime-entry-B8g46vS0.d.ts} +1 -5
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +4 -4
- package/src/components/event-stream-view.test.ts +5 -0
- package/src/components/message-bubble.ts +26 -6
- package/src/components/scroll-to-bottom.test.ts +135 -0
- package/src/components/scroll-to-bottom.ts +50 -13
- package/src/components/user-message-collapse.ts +117 -0
- package/src/defaults.ts +6 -0
- package/src/styles/widget.css +56 -0
- package/src/theme-reference.ts +2 -2
- package/src/types/theme.ts +29 -0
- package/src/types.ts +49 -5
- package/src/ui.scroll-additive.test.ts +219 -117
- package/src/ui.scroll.test.ts +176 -13
- package/src/ui.ts +230 -107
- package/src/ui.turn-activity.test.ts +7 -7
- package/src/ui.user-message-collapse.test.ts +540 -0
- package/src/utils/auto-follow.test.ts +0 -38
- package/src/utils/auto-follow.ts +0 -35
- package/src/utils/theme.test.ts +23 -0
- package/src/utils/tokens.ts +16 -0
- package/dist/chunk-CVMIncEY.js +0 -1
- package/dist/chunk-DVgzoVCB.js +0 -1
- package/dist/chunk-d54BJdKd.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.34.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"@types/jsdom": "^30.0.0",
|
|
192
192
|
"@types/node": "^20.12.7",
|
|
193
193
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
194
|
-
"@vitest/ui": "^5.0.
|
|
194
|
+
"@vitest/ui": "^5.0.5",
|
|
195
195
|
"acorn": "^8.15.0",
|
|
196
196
|
"fake-indexeddb": "^6.2.5",
|
|
197
197
|
"jsdom": "^28.1.0",
|
|
@@ -199,9 +199,9 @@
|
|
|
199
199
|
"rimraf": "^5.0.5",
|
|
200
200
|
"size-limit": "^12.1.0",
|
|
201
201
|
"terser": "^5.49.0",
|
|
202
|
-
"tsdown": "^0.23.
|
|
202
|
+
"tsdown": "^0.23.2",
|
|
203
203
|
"typescript": "^6.0.3",
|
|
204
|
-
"vitest": "^5.0.
|
|
204
|
+
"vitest": "^5.0.5",
|
|
205
205
|
"webmcp-polyfill-v6": "npm:@mcp-b/webmcp-polyfill@6.0.0"
|
|
206
206
|
},
|
|
207
207
|
"engines": {
|
|
@@ -36,6 +36,11 @@ function createMockElement(tag = "div"): any {
|
|
|
36
36
|
clientHeight: 300,
|
|
37
37
|
// For <select>
|
|
38
38
|
options: [] as any[],
|
|
39
|
+
append(...nodes: (Node | string)[]) {
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
el.appendChild(typeof node === "string" ? { nodeType: 3, textContent: node } : node);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
39
44
|
appendChild(child: any) {
|
|
40
45
|
// Handle DocumentFragment: move its children into this element
|
|
41
46
|
if (child.tagName === "FRAGMENT") {
|
|
@@ -25,6 +25,11 @@ import { createMentionTokenElement } from "../utils/mention-token";
|
|
|
25
25
|
import { createIconButton } from "../utils/buttons";
|
|
26
26
|
import { renderLucideIcon } from "../utils/icons";
|
|
27
27
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
|
|
28
|
+
import {
|
|
29
|
+
applyUserCollapseState,
|
|
30
|
+
createUserCollapseRegion,
|
|
31
|
+
type UserMessageCollapseRender,
|
|
32
|
+
} from "./user-message-collapse";
|
|
28
33
|
import {
|
|
29
34
|
applyStreamBuffer,
|
|
30
35
|
createSkeletonPlaceholder,
|
|
@@ -998,6 +1003,11 @@ export type CreateStandardBubbleOptions = {
|
|
|
998
1003
|
* Full widget config (needed for loading indicator context)
|
|
999
1004
|
*/
|
|
1000
1005
|
widgetConfig?: import("../types").AgentWidgetConfig;
|
|
1006
|
+
/**
|
|
1007
|
+
* Long user message collapse (`features.userMessageCollapse`). When set on
|
|
1008
|
+
* a user bubble, its content is wrapped in a clip region with the toggle.
|
|
1009
|
+
*/
|
|
1010
|
+
userCollapse?: UserMessageCollapseRender;
|
|
1001
1011
|
};
|
|
1002
1012
|
|
|
1003
1013
|
/**
|
|
@@ -1238,6 +1248,12 @@ export const createStandardBubble = (
|
|
|
1238
1248
|
}
|
|
1239
1249
|
}
|
|
1240
1250
|
|
|
1251
|
+
// Everything a long user message can spill (attachments, chips, text) goes
|
|
1252
|
+
// in the collapse region; the timestamp and actions stay outside the clip.
|
|
1253
|
+
const userCollapse = message.role === "user" ? options?.userCollapse : undefined;
|
|
1254
|
+
const collapseRegion = userCollapse ? createUserCollapseRegion(message.id, userCollapse.maxHeight) : null;
|
|
1255
|
+
const contentParent = collapseRegion ?? bubble;
|
|
1256
|
+
|
|
1241
1257
|
if (imageParts.length > 0) {
|
|
1242
1258
|
const imagePreviews = createMessageImagePreviews(
|
|
1243
1259
|
imageParts,
|
|
@@ -1250,7 +1266,7 @@ export const createStandardBubble = (
|
|
|
1250
1266
|
);
|
|
1251
1267
|
|
|
1252
1268
|
if (imagePreviews) {
|
|
1253
|
-
|
|
1269
|
+
contentParent.appendChild(imagePreviews);
|
|
1254
1270
|
} else if (shouldHideTextUntilPreviewFails && textContentDiv) {
|
|
1255
1271
|
textContentDiv.style.display = "";
|
|
1256
1272
|
}
|
|
@@ -1260,7 +1276,7 @@ export const createStandardBubble = (
|
|
|
1260
1276
|
if (audioParts.length > 0) {
|
|
1261
1277
|
const audioPreviews = createMessageAudioPreviews(audioParts);
|
|
1262
1278
|
if (audioPreviews) {
|
|
1263
|
-
|
|
1279
|
+
contentParent.appendChild(audioPreviews);
|
|
1264
1280
|
}
|
|
1265
1281
|
}
|
|
1266
1282
|
|
|
@@ -1268,7 +1284,7 @@ export const createStandardBubble = (
|
|
|
1268
1284
|
if (videoParts.length > 0) {
|
|
1269
1285
|
const videoPreviews = createMessageVideoPreviews(videoParts);
|
|
1270
1286
|
if (videoPreviews) {
|
|
1271
|
-
|
|
1287
|
+
contentParent.appendChild(videoPreviews);
|
|
1272
1288
|
}
|
|
1273
1289
|
}
|
|
1274
1290
|
|
|
@@ -1276,7 +1292,7 @@ export const createStandardBubble = (
|
|
|
1276
1292
|
if (fileParts.length > 0) {
|
|
1277
1293
|
const filePreviews = createMessageFilePreviews(fileParts);
|
|
1278
1294
|
if (filePreviews) {
|
|
1279
|
-
|
|
1295
|
+
contentParent.appendChild(filePreviews);
|
|
1280
1296
|
}
|
|
1281
1297
|
}
|
|
1282
1298
|
|
|
@@ -1286,10 +1302,14 @@ export const createStandardBubble = (
|
|
|
1286
1302
|
? null
|
|
1287
1303
|
: createMessageMentionChips(message.contextMentions);
|
|
1288
1304
|
if (mentionChips) {
|
|
1289
|
-
|
|
1305
|
+
contentParent.appendChild(mentionChips);
|
|
1290
1306
|
}
|
|
1291
1307
|
|
|
1292
|
-
|
|
1308
|
+
contentParent.appendChild(contentDiv);
|
|
1309
|
+
if (collapseRegion && userCollapse) {
|
|
1310
|
+
bubble.appendChild(collapseRegion);
|
|
1311
|
+
applyUserCollapseState(bubble, collapseRegion, message.id, userCollapse);
|
|
1312
|
+
}
|
|
1293
1313
|
|
|
1294
1314
|
// Add timestamp below if configured
|
|
1295
1315
|
if (showTimestamp && timestampPosition === "below" && message.createdAt) {
|
|
@@ -40,6 +40,49 @@ describe("scroll-to-bottom chrome", () => {
|
|
|
40
40
|
control.destroy();
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it("names a pending approval below with a labelled shield pill, then reverts", () => {
|
|
44
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
45
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
46
|
+
control.update(config, true, false, 2, true);
|
|
47
|
+
const el = control.element;
|
|
48
|
+
expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
|
|
49
|
+
expect(el.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("true");
|
|
50
|
+
expect(el.textContent).toBe("Approval needed");
|
|
51
|
+
expect(el.querySelector("svg")?.getAttribute("data-icon")).toBe("shield-check");
|
|
52
|
+
// The label replaces the unread count; assistive tech hears where it leads.
|
|
53
|
+
expect(el.querySelector<HTMLElement>("[data-persona-scroll-to-bottom-count]")?.style.display).toBe("none");
|
|
54
|
+
expect(el.getAttribute("aria-label")).toBe("Approval needed below. Jump to latest");
|
|
55
|
+
|
|
56
|
+
// Hidden, the approval state never shows.
|
|
57
|
+
control.update(config, false, false, 0, true);
|
|
58
|
+
expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(false);
|
|
59
|
+
|
|
60
|
+
control.update(config, true, false, 2, false);
|
|
61
|
+
expect(el.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(false);
|
|
62
|
+
expect(el.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("false");
|
|
63
|
+
expect(el.querySelector("svg")?.getAttribute("data-icon")).toBe("arrow-down");
|
|
64
|
+
expect(el.getAttribute("aria-label")).toBe("Jump to latest (2 new)");
|
|
65
|
+
control.destroy();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses a custom approval label, opts out with an empty one, and tells custom glyphs", () => {
|
|
69
|
+
const states: string[] = [];
|
|
70
|
+
const renderIcon = vi.fn((_context: AgentWidgetScrollToBottomIconContext) => ({
|
|
71
|
+
element: document.createElement("span"),
|
|
72
|
+
update: (next: AgentWidgetScrollToBottomIconContext) => { states.push(next.state.activity); },
|
|
73
|
+
}));
|
|
74
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
75
|
+
control.update({ features: { scrollToBottom: { approvalLabel: "Review request", renderIcon } } }, true, false, 0, true);
|
|
76
|
+
expect(control.element.textContent).toBe("Review request");
|
|
77
|
+
expect(renderIcon.mock.calls[0][0].state.activity).toBe("approval-below");
|
|
78
|
+
control.update({ features: { scrollToBottom: { approvalLabel: "", renderIcon } } }, true, false, 1, true);
|
|
79
|
+
expect(control.element.getAttribute("data-persona-scroll-to-bottom-has-label")).toBe("false");
|
|
80
|
+
expect(control.element.getAttribute("aria-label")).toBe("Approval needed below. Jump to latest");
|
|
81
|
+
control.update({ features: { scrollToBottom: { renderIcon } } }, true, false, 0, false);
|
|
82
|
+
expect(states.at(-1)).toBe("idle");
|
|
83
|
+
control.destroy();
|
|
84
|
+
});
|
|
85
|
+
|
|
43
86
|
it("makes fading controls inert immediately and returns focus only when owned, including Shadow DOM", () => {
|
|
44
87
|
const host = document.createElement("div"); document.body.appendChild(host);
|
|
45
88
|
const root = host.attachShadow({ mode: "open" });
|
|
@@ -122,6 +165,98 @@ describe("scroll-to-bottom chrome", () => {
|
|
|
122
165
|
control.destroy(); warning.mockRestore();
|
|
123
166
|
});
|
|
124
167
|
|
|
168
|
+
describe("approval morph", () => {
|
|
169
|
+
const stubAnimate = () => {
|
|
170
|
+
const calls: { target: Element; keyframes: Keyframe[]; options: KeyframeAnimationOptions; animation: { cancel: ReturnType<typeof vi.fn> } }[] = [];
|
|
171
|
+
const original = (HTMLElement.prototype as { animate?: unknown }).animate;
|
|
172
|
+
(HTMLElement.prototype as { animate?: unknown }).animate = function (this: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions) {
|
|
173
|
+
const animation = { cancel: vi.fn(), finished: new Promise<void>(() => {}) };
|
|
174
|
+
calls.push({ target: this, keyframes, options, animation });
|
|
175
|
+
return animation;
|
|
176
|
+
};
|
|
177
|
+
return { calls, restore: () => { (HTMLElement.prototype as { animate?: unknown }).animate = original; } };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
it("morphs a visible arrow into the pill on the theme's motion tokens, and back", () => {
|
|
181
|
+
const { calls, restore } = stubAnimate();
|
|
182
|
+
try {
|
|
183
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
184
|
+
document.body.appendChild(control.element);
|
|
185
|
+
control.element.style.setProperty("--persona-motion-duration-base", "0.3s");
|
|
186
|
+
control.element.style.setProperty("--persona-motion-easing", "cubic-bezier(0.2, 0, 0, 1)");
|
|
187
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
188
|
+
control.update(config, true);
|
|
189
|
+
expect(calls).toHaveLength(0);
|
|
190
|
+
|
|
191
|
+
control.update(config, true, false, 0, true);
|
|
192
|
+
const [own, labelFade] = calls;
|
|
193
|
+
expect(own.target).toBe(control.element);
|
|
194
|
+
expect(own.keyframes).toHaveLength(2);
|
|
195
|
+
expect(own.keyframes[0]).toMatchObject({ overflow: "hidden" });
|
|
196
|
+
expect(own.keyframes[0]).toHaveProperty("width");
|
|
197
|
+
expect(own.keyframes[1]).toHaveProperty("backgroundColor");
|
|
198
|
+
expect(own.options).toEqual({ duration: 300, easing: "cubic-bezier(0.2, 0, 0, 1)" });
|
|
199
|
+
// The label fades in as the clipped pill widens.
|
|
200
|
+
expect(labelFade.target.textContent).toBe("Approval needed");
|
|
201
|
+
expect(calls).toHaveLength(2);
|
|
202
|
+
|
|
203
|
+
// Ordinary syncs in the same state leave a running morph alone.
|
|
204
|
+
control.update(config, true, false, 0, true);
|
|
205
|
+
expect(calls).toHaveLength(2);
|
|
206
|
+
expect(calls.every((call) => call.animation.cancel.mock.calls.length === 0)).toBe(true);
|
|
207
|
+
|
|
208
|
+
// Back to the arrow: the running morph is replaced, unclipped (the
|
|
209
|
+
// count badge hangs outside the button).
|
|
210
|
+
control.update(config, true, false, 2, false);
|
|
211
|
+
expect(calls.slice(0, 2).every((call) => call.animation.cancel.mock.calls.length === 1)).toBe(true);
|
|
212
|
+
expect(calls).toHaveLength(3);
|
|
213
|
+
expect(calls[2].keyframes[0]).not.toHaveProperty("overflow");
|
|
214
|
+
control.destroy();
|
|
215
|
+
} finally { restore(); }
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("swaps instantly when the theme turns motion off, and falls back when the duration is missing", () => {
|
|
219
|
+
const { calls, restore } = stubAnimate();
|
|
220
|
+
try {
|
|
221
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
222
|
+
const off = createScrollToBottomControl(icon, "transcript");
|
|
223
|
+
off.element.style.setProperty("--persona-motion-duration-base", "0ms");
|
|
224
|
+
off.update(config, true);
|
|
225
|
+
off.update(config, true, false, 0, true);
|
|
226
|
+
expect(calls).toHaveLength(0);
|
|
227
|
+
expect(off.element.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
|
|
228
|
+
off.destroy();
|
|
229
|
+
|
|
230
|
+
const unset = createScrollToBottomControl(icon, "transcript");
|
|
231
|
+
unset.update(config, true);
|
|
232
|
+
unset.update(config, true, false, 0, true);
|
|
233
|
+
expect(calls[0].options.duration).toBe(200);
|
|
234
|
+
unset.destroy();
|
|
235
|
+
} finally { restore(); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("does not morph a control that appears, hides, or prefers reduced motion", () => {
|
|
239
|
+
const { calls, restore } = stubAnimate();
|
|
240
|
+
try {
|
|
241
|
+
const config: AgentWidgetConfig = { future: { v5Defaults: true } };
|
|
242
|
+
const control = createScrollToBottomControl(icon, "transcript");
|
|
243
|
+
control.update(config, false);
|
|
244
|
+
control.update(config, true, false, 0, true); // appears already as the pill
|
|
245
|
+
control.update(config, false, false, 0, true); // hides
|
|
246
|
+
expect(calls).toHaveLength(0);
|
|
247
|
+
control.destroy();
|
|
248
|
+
|
|
249
|
+
vi.stubGlobal("matchMedia", () => ({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }));
|
|
250
|
+
const reduced = createScrollToBottomControl(icon, "transcript");
|
|
251
|
+
reduced.update(config, true);
|
|
252
|
+
reduced.update(config, true, false, 0, true);
|
|
253
|
+
expect(calls).toHaveLength(0);
|
|
254
|
+
expect(reduced.element.hasAttribute("data-persona-scroll-to-bottom-approval")).toBe(true);
|
|
255
|
+
reduced.destroy();
|
|
256
|
+
} finally { restore(); }
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
125
260
|
it("provides live reduced-motion changes and releases its media listener", () => {
|
|
126
261
|
let change: (() => void) | undefined;
|
|
127
262
|
const media = { matches: true, addEventListener: vi.fn((_event, callback) => { change = callback; }), removeEventListener: vi.fn() };
|
|
@@ -25,9 +25,7 @@ export function createScrollToBottomControl(
|
|
|
25
25
|
const label = createElement("span", "");
|
|
26
26
|
const badge = createElement("span", "");
|
|
27
27
|
badge.setAttribute("data-persona-scroll-to-bottom-count", "");
|
|
28
|
-
element.
|
|
29
|
-
element.appendChild(label);
|
|
30
|
-
element.appendChild(badge);
|
|
28
|
+
element.append(glyph, label, badge);
|
|
31
29
|
let config: AgentWidgetConfig = {};
|
|
32
30
|
let state: AgentWidgetScrollToBottomIconState = { activity: "idle", visible: false, hovered: false, focused: false, reducedMotion: false };
|
|
33
31
|
// Keep renderer identity separate from its handle, including null fallbacks.
|
|
@@ -36,6 +34,26 @@ export function createScrollToBottomControl(
|
|
|
36
34
|
let abort: AbortController | undefined;
|
|
37
35
|
let builtin = "";
|
|
38
36
|
const media = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
|
|
37
|
+
// Arrow <-> "Approval needed" pill: one button changing shape, so morph its
|
|
38
|
+
// width and colors on the theme's motion tokens while the new label fades in.
|
|
39
|
+
let morphs: Animation[] = [];
|
|
40
|
+
const paint = () => {
|
|
41
|
+
const style = getComputedStyle(element);
|
|
42
|
+
return { width: `${element.offsetWidth}px`, backgroundColor: style.backgroundColor, borderColor: style.borderColor, color: style.color };
|
|
43
|
+
};
|
|
44
|
+
const morph = (from: Keyframe) => {
|
|
45
|
+
const style = getComputedStyle(element);
|
|
46
|
+
const value = style.getPropertyValue("--persona-motion-duration-base");
|
|
47
|
+
const ms = parseFloat(value) * (value.includes("ms") ? 1 : 1000);
|
|
48
|
+
// "0ms" turns motion off; only a missing or invalid value falls back.
|
|
49
|
+
if (ms === 0) return;
|
|
50
|
+
const timing = { duration: ms > 0 ? ms : 200, easing: style.getPropertyValue("--persona-motion-easing").trim() || "ease-out" };
|
|
51
|
+
// Clip only while widening into the pill: the arrow's count badge hangs
|
|
52
|
+
// outside the button.
|
|
53
|
+
const clip = label.style.display === "none" ? null : { overflow: "hidden" };
|
|
54
|
+
morphs = [element.animate([{ ...from, ...clip }, { ...paint(), ...clip }], timing)];
|
|
55
|
+
if (clip) morphs.push(label.animate([{ opacity: 0 }, { opacity: 1 }], timing));
|
|
56
|
+
};
|
|
39
57
|
state.reducedMotion = media?.matches ?? false;
|
|
40
58
|
const warn = (error: unknown) => console.warn("Persona scroll icon failed", error);
|
|
41
59
|
const context = (): AgentWidgetScrollToBottomIconContext => ({ config, state: { ...state }, view, signal: abort!.signal });
|
|
@@ -51,7 +69,9 @@ export function createScrollToBottomControl(
|
|
|
51
69
|
if (handle?.element) return;
|
|
52
70
|
const feature = config.features?.scrollToBottom;
|
|
53
71
|
const dots = state.activity === "generating-below" && (feature?.streamingIndicator ?? (config.future?.v5Defaults ? "dots" : "arrow")) === "dots";
|
|
54
|
-
|
|
72
|
+
// Same glyph as the approval card it points at, so the two read as one thing.
|
|
73
|
+
const iconName = state.activity === "approval-below" ? "shield-check" : feature?.iconName ?? "arrow-down";
|
|
74
|
+
const key = dots ? "dots" : `arrow:${iconName}`;
|
|
55
75
|
if (builtin === key) return;
|
|
56
76
|
builtin = key;
|
|
57
77
|
glyph.innerHTML = "";
|
|
@@ -64,7 +84,7 @@ export function createScrollToBottomControl(
|
|
|
64
84
|
}
|
|
65
85
|
glyph.appendChild(dotGroup);
|
|
66
86
|
} else {
|
|
67
|
-
const icon = renderIcon(
|
|
87
|
+
const icon = renderIcon(iconName, "14px", "currentColor", 2);
|
|
68
88
|
if (icon) glyph.appendChild(icon);
|
|
69
89
|
}
|
|
70
90
|
};
|
|
@@ -106,11 +126,12 @@ export function createScrollToBottomControl(
|
|
|
106
126
|
|
|
107
127
|
return {
|
|
108
128
|
element,
|
|
109
|
-
|
|
129
|
+
/** `approval` marks a pending approval below the fold: the control names it. */
|
|
130
|
+
update(nextConfig: AgentWidgetConfig, visible: boolean, active = false, count = 0, approval = false) {
|
|
110
131
|
const previous = config.features?.scrollToBottom;
|
|
111
132
|
const next = nextConfig.features?.scrollToBottom;
|
|
112
133
|
const configChanged = config.future?.v5Defaults !== nextConfig.future?.v5Defaults ||
|
|
113
|
-
(["enabled", "iconName", "label", "streamingIndicator", "transition", "scrollBehavior", "scrollDurationMs"] as (keyof AgentWidgetScrollToBottomFeature)[])
|
|
134
|
+
(["enabled", "iconName", "label", "approvalLabel", "streamingIndicator", "transition", "scrollBehavior", "scrollDurationMs"] as (keyof AgentWidgetScrollToBottomFeature)[])
|
|
114
135
|
.some((key) => previous?.[key] !== next?.[key]);
|
|
115
136
|
config = nextConfig;
|
|
116
137
|
const feature = config.features?.scrollToBottom;
|
|
@@ -132,21 +153,37 @@ export function createScrollToBottomControl(
|
|
|
132
153
|
focusTarget.focus({ preventScroll: true });
|
|
133
154
|
}
|
|
134
155
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
// A waiting approval outranks streaming and the unread count: the turn is
|
|
157
|
+
// blocked on the reader, so the control says so in words.
|
|
158
|
+
const waiting = approval && visible;
|
|
159
|
+
// Morph only a visible control changing shape, never its fade in or out.
|
|
160
|
+
const reshape = state.visible && visible && (state.activity === "approval-below") !== waiting;
|
|
161
|
+
if (reshape || !visible) morphs.forEach((animation) => animation.cancel());
|
|
162
|
+
const from = reshape && !state.reducedMotion && typeof element.animate === "function" ? paint() : null;
|
|
163
|
+
const approvalText = feature?.approvalLabel ?? "Approval needed";
|
|
164
|
+
const text = waiting && approvalText ? approvalText : feature?.label ?? "";
|
|
165
|
+
const base = feature?.label || "Jump to latest";
|
|
166
|
+
element.setAttribute("aria-label", waiting
|
|
167
|
+
? `${approvalText || "Approval needed"} below. ${base}`
|
|
168
|
+
: `${base}${count > 0 ? ` (${count} new)` : ""}${active && visible ? " (response streaming below)" : ""}`);
|
|
138
169
|
element.title = text;
|
|
139
170
|
element.setAttribute("data-persona-scroll-to-bottom-has-label", String(Boolean(text)));
|
|
171
|
+
if (waiting) element.setAttribute("data-persona-scroll-to-bottom-approval", "");
|
|
172
|
+
else element.removeAttribute("data-persona-scroll-to-bottom-approval");
|
|
140
173
|
label.textContent = text;
|
|
141
174
|
label.style.display = text ? "" : "none";
|
|
142
|
-
|
|
143
|
-
badge.
|
|
144
|
-
|
|
175
|
+
const shownCount = waiting ? 0 : count;
|
|
176
|
+
badge.textContent = shownCount > 0 ? String(shownCount) : "";
|
|
177
|
+
badge.style.display = shownCount > 0 ? "" : "none";
|
|
178
|
+
const activity = waiting ? "approval-below" as const : active && visible ? "generating-below" as const : "idle" as const;
|
|
179
|
+
const nextState = { ...state, visible, hovered: visible && state.hovered, focused: visible && state.focused, activity };
|
|
145
180
|
const changed = configChanged || stateChanged(nextState);
|
|
146
181
|
state = nextState;
|
|
147
182
|
updateRenderer(changed);
|
|
183
|
+
if (from) morph(from);
|
|
148
184
|
},
|
|
149
185
|
destroy() {
|
|
186
|
+
morphs.forEach((animation) => animation.cancel());
|
|
150
187
|
dispose();
|
|
151
188
|
lifetime.abort();
|
|
152
189
|
media?.removeEventListener?.("change", motion);
|
|
@@ -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
|
+
};
|
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/styles/widget.css
CHANGED
|
@@ -3109,6 +3109,52 @@
|
|
|
3109
3109
|
box-shadow: var(--persona-message-user-shadow);
|
|
3110
3110
|
}
|
|
3111
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
|
+
|
|
3112
3158
|
[data-persona-root] .persona-tool-bubble.persona-shadow-sm {
|
|
3113
3159
|
box-shadow: var(--persona-tool-bubble-shadow);
|
|
3114
3160
|
}
|
|
@@ -4869,6 +4915,16 @@
|
|
|
4869
4915
|
text-align: center;
|
|
4870
4916
|
}
|
|
4871
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
|
+
|
|
4872
4928
|
[data-persona-root] .persona-scroll-to-bottom-indicator:hover {
|
|
4873
4929
|
opacity: 0.92;
|
|
4874
4930
|
}
|
package/src/theme-reference.ts
CHANGED
|
@@ -171,7 +171,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
171
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.',
|
|
172
172
|
attachment: 'image (background, border).',
|
|
173
173
|
scrollToBottom:
|
|
174
|
-
'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.',
|
|
175
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.',
|
|
176
176
|
scrollbar:
|
|
177
177
|
'Shared scrollbar appearance for every scroller: thumb (default semantic.colors.border), track (default transparent).',
|
|
@@ -250,7 +250,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
250
250
|
description:
|
|
251
251
|
'Shared transcript + event-stream jump-to-latest affordance.',
|
|
252
252
|
properties:
|
|
253
|
-
'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".',
|
|
254
254
|
},
|
|
255
255
|
toolCall: {
|
|
256
256
|
description:
|