@runtypelabs/persona 4.32.2 → 4.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-ui.d.cts +3 -15
- package/dist/activity-ui.d.ts +3 -15
- package/dist/animations/glyph-cycle.d.cts +1 -3
- package/dist/animations/glyph-cycle.d.ts +1 -3
- package/dist/animations/wipe.d.cts +1 -3
- package/dist/animations/wipe.d.ts +1 -3
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +3 -15
- package/dist/approval-ui.d.ts +3 -15
- package/dist/approval-ui.js +1 -1
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +3 -21
- package/dist/artifacts-ui.d.ts +3 -21
- package/dist/background-runtime.d.cts +2 -3
- package/dist/background-runtime.d.ts +2 -3
- package/dist/{chunk-BbXc58OA.js → chunk-6CUrH_jm.js} +1 -1
- package/dist/{chunk-B_H0l3u2.js → chunk-BRM3bHcJ.js} +1 -1
- package/dist/{chunk-DNdeIFJ6.js → chunk-Brdx4NCz.js} +1 -1
- package/dist/chunk-C-rux8Q9.js +1 -0
- package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
- package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
- package/dist/chunk-CEPM3FOp.js +1 -0
- package/dist/{chunk-BpjtKnGI.js → chunk-CV8EPIhT.js} +1 -1
- package/dist/{chunk-DVZ8ri1z.js → chunk-CWzHNuVK.js} +1 -1
- package/dist/{chunk-B3l7TOOv.js → chunk-CYpt7PsO.js} +1 -1
- package/dist/{chunk-2FnmZ9AA.js → chunk-Csr59G5V.js} +1 -1
- package/dist/{chunk-B9Zu-wrk.js → chunk-CxNPSY-b.js} +1 -1
- package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
- package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
- package/dist/chunk-Dqa_GOcj.js +1 -0
- package/dist/chunk-DsWv3EGF.js +1 -0
- package/dist/{chunk-C7-m0EmI.js → chunk-NUNdcnAZ.js} +1 -1
- package/dist/{chunk-D0tcIZJM.js → chunk-YMkMvAsa.js} +2 -2
- package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
- package/dist/chunk-i4qGOJAL.js +1 -0
- package/dist/chunk-kizhspe5.js +1 -0
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
- package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +4 -20
- package/dist/composer-actions-runtime.d.ts +4 -20
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-i_FiUp6J.d.cts} +1 -7
- package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-i_FiUp6J.d.ts} +1 -7
- package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-DhGNgG7Z.d.cts} +1 -3
- package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-DhGNgG7Z.d.ts} +1 -3
- package/dist/config-diagnostics.d.cts +0 -2
- package/dist/config-diagnostics.d.ts +0 -2
- package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-DL-xwGfL.d.cts} +2 -6
- package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-DL-xwGfL.d.ts} +2 -6
- package/dist/context-mentions-inline.cjs +1 -1
- package/dist/context-mentions-inline.d.cts +2 -4
- package/dist/context-mentions-inline.d.ts +2 -4
- package/dist/context-mentions-inline.js +1 -1
- package/dist/context-mentions.cjs +1 -1
- package/dist/context-mentions.d.cts +3 -9
- package/dist/context-mentions.d.ts +3 -9
- package/dist/{defaults-CjX0kKzb.d.ts → defaults-Bga2Xrsp.d.cts} +2 -5
- package/dist/{defaults-CjX0kKzb.d.cts → defaults-Bga2Xrsp.d.ts} +2 -5
- package/dist/dictation-runtime.cjs +1 -1
- package/dist/dictation-runtime.d.cts +4 -13
- package/dist/dictation-runtime.d.ts +4 -13
- package/dist/dictation-runtime.js +1 -1
- package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.cts} +0 -2
- package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.ts} +0 -2
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +3 -16
- package/dist/event-stream-view.d.ts +3 -16
- package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-kLdyPcmk.d.cts} +1 -3
- package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-kLdyPcmk.d.ts} +1 -3
- package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-CmwZGKrf.d.cts} +1 -5
- package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-CmwZGKrf.d.ts} +1 -5
- package/dist/forms-ui.d.cts +2 -4
- package/dist/forms-ui.d.ts +2 -4
- package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-i46-lPN_.d.cts} +2 -14
- package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-i46-lPN_.d.ts} +2 -14
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +8 -27
- package/dist/history-shell.d.ts +8 -27
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/icons-extra.cjs +1 -1
- package/dist/icons-extra.d.cts +0 -2
- package/dist/icons-extra.d.ts +0 -2
- package/dist/index.cjs +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-Ccr3ETd3.d.cts} +1 -3
- package/dist/{loading-label-runtime-loader-CPxnoaoi.d.cts → loading-label-runtime-loader-Ccr3ETd3.d.ts} +1 -3
- package/dist/loading-label-runtime.cjs +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/loading-label-runtime.js +1 -1
- package/dist/markdown-parsers.js +1 -1
- package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-C6AVaH7f.d.cts} +21 -4
- package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-C6AVaH7f.d.ts} +21 -4
- package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-BVggqE3C.d.cts} +0 -2
- package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-BVggqE3C.d.ts} +0 -2
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/preferences-runtime.cjs +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/preferences-runtime.js +1 -1
- package/dist/{preview-DdUvoLb4.d.cts → preview-BMdRffo4.d.cts} +2 -6
- package/dist/{preview-DdUvoLb4.d.ts → preview-BMdRffo4.d.ts} +2 -6
- package/dist/{registry-Dz4iHNST.d.ts → registry-B16l8s1b.d.cts} +1 -8
- package/dist/{registry-Dz4iHNST.d.cts → registry-B16l8s1b.d.ts} +1 -8
- package/dist/{session-Baxp9iRK.d.cts → session-CtZHe7fc.d.cts} +10 -93
- package/dist/{session-Baxp9iRK.d.ts → session-CtZHe7fc.d.ts} +10 -93
- package/dist/session-reconnect.d.cts +2 -4
- package/dist/session-reconnect.d.ts +2 -4
- package/dist/smart-dom-reader.d.cts +2 -8
- package/dist/smart-dom-reader.d.ts +2 -8
- package/dist/testing.d.cts +0 -2
- package/dist/testing.d.ts +0 -2
- package/dist/theme-editor-preview.cjs +42 -42
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +25 -25
- package/dist/theme-editor.cjs +25 -5
- package/dist/theme-editor.d.cts +2 -22
- package/dist/theme-editor.d.ts +2 -22
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +4 -4
- package/dist/theme-reference.d.ts +4 -4
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-D2cyFvmM.d.cts} +1 -7
- package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-D2cyFvmM.d.ts} +1 -7
- package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
- package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
- package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-Do1Nd-Ow.d.cts} +2 -12
- package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-Do1Nd-Ow.d.ts} +2 -12
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-BOZHQIk0.d.cts → types-BC7MRxO5.d.cts} +167 -36
- package/dist/{types-BOZHQIk0.d.ts → types-BC7MRxO5.d.ts} +167 -36
- package/dist/ui-extras.js +3 -3
- package/dist/voice-client-tools.d.cts +1 -3
- package/dist/voice-client-tools.d.ts +1 -3
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -12
- package/dist/voice-runtime.d.ts +2 -12
- package/dist/voice-runtime.js +2 -2
- package/dist/voice-worklet-player.d.cts +2 -6
- package/dist/voice-worklet-player.d.ts +2 -6
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-B8g46vS0.d.cts} +1 -5
- package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-B8g46vS0.d.ts} +1 -5
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +4 -4
- package/src/client-dispatch.ts +69 -16
- package/src/client.operator-role.test.ts +217 -0
- package/src/components/event-stream-view.test.ts +5 -0
- package/src/components/message-bubble.test.ts +169 -0
- package/src/components/message-bubble.ts +123 -34
- package/src/components/scroll-to-bottom.test.ts +135 -0
- package/src/components/scroll-to-bottom.ts +50 -13
- package/src/components/user-message-collapse.ts +117 -0
- package/src/defaults-parity.test.ts +14 -0
- package/src/defaults.ts +6 -0
- package/src/index-core.ts +7 -0
- package/src/session.resubmit-from.test.ts +27 -0
- package/src/session.test.ts +10 -0
- package/src/session.ts +8 -5
- package/src/styles/widget.css +79 -1
- package/src/theme-reference.ts +5 -3
- package/src/types/theme.ts +50 -0
- package/src/types.ts +120 -11
- package/src/ui.ask-user-question-plugin.test.ts +154 -0
- package/src/ui.operator-role.test.ts +336 -0
- package/src/ui.scroll-additive.test.ts +219 -117
- package/src/ui.scroll.test.ts +176 -13
- package/src/ui.ts +267 -119
- package/src/ui.turn-activity.test.ts +7 -7
- package/src/ui.user-message-collapse.test.ts +540 -0
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
- package/src/utils/auto-follow.test.ts +0 -38
- package/src/utils/auto-follow.ts +0 -35
- package/src/utils/brand.test.ts +14 -0
- package/src/utils/brand.ts +8 -4
- package/src/utils/history-messages.test.ts +30 -0
- package/src/utils/history-messages.ts +26 -8
- package/src/utils/message-fingerprint.ts +4 -0
- package/src/utils/resubmit-turn.test.ts +13 -0
- package/src/utils/resubmit-turn.ts +12 -1
- package/src/utils/theme.test.ts +23 -0
- package/src/utils/tokens.ts +47 -0
- package/src/welcome.test.ts +16 -0
- package/src/welcome.ts +10 -2
- package/dist/chunk-BNE40cEG.js +0 -1
- package/dist/chunk-Bz9jAgba.js +0 -1
- package/dist/chunk-CVMIncEY.js +0 -1
- package/dist/chunk-CdpVP6LZ.js +0 -1
- package/dist/chunk-CjkechxR.js +0 -1
- package/dist/chunk-DgZZqy14.js +0 -1
package/src/ui.ts
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
type MessageEditEditor
|
|
113
113
|
} from "./components/message-edit-editor";
|
|
114
114
|
import {
|
|
115
|
+
canResubmitUserMessage,
|
|
115
116
|
findPrecedingUserMessageId,
|
|
116
117
|
findRetryableAssistantMessageId,
|
|
117
118
|
isEditableUserMessage
|
|
@@ -202,7 +203,6 @@ import {
|
|
|
202
203
|
createFollowStateController,
|
|
203
204
|
getScrollBottomOffset,
|
|
204
205
|
hasSelectionWithin,
|
|
205
|
-
isAttentionWorthyMessage,
|
|
206
206
|
isElementNearBottom,
|
|
207
207
|
resolveFollowStateFromScroll,
|
|
208
208
|
resolveFollowStateFromWheel
|
|
@@ -271,6 +271,17 @@ import { buildHeaderWithLayout } from "./components/header-layouts";
|
|
|
271
271
|
import { positionMap } from "./utils/positioning";
|
|
272
272
|
import type { HeaderElements as _HeaderElements, ComposerElements as _ComposerElements } from "./components/panel";
|
|
273
273
|
import { MessageTransform, MessageActionCallbacks, MessageActionEligibility } from "./components/message-bubble";
|
|
274
|
+
import {
|
|
275
|
+
applyUserCollapseState,
|
|
276
|
+
isUserMessageCollapseEnabled,
|
|
277
|
+
resolveUserMessageCollapseLabels,
|
|
278
|
+
resolveUserMessageCollapseMaxHeight,
|
|
279
|
+
userMessageOverflows,
|
|
280
|
+
USER_COLLAPSE_REGION_ATTR,
|
|
281
|
+
USER_COLLAPSE_TOGGLE_ATTR,
|
|
282
|
+
type UserMessageCollapseRender,
|
|
283
|
+
type UserMessageCollapseState,
|
|
284
|
+
} from "./components/user-message-collapse";
|
|
274
285
|
import {
|
|
275
286
|
createStandardBubble,
|
|
276
287
|
createTypingIndicator,
|
|
@@ -971,6 +982,31 @@ export const createAgentExperience = (
|
|
|
971
982
|
const toolExpansionState = new Set<string>();
|
|
972
983
|
const reasoningExpansionState = new Set<string>();
|
|
973
984
|
const approvalDetailsExpansionState = new Map<string, boolean>();
|
|
985
|
+
// Long user message collapse, per message id: the measured overflow and the
|
|
986
|
+
// reader's own toggle (true = expanded). Render state derives from both, so
|
|
987
|
+
// re-renders and morphs never re-collapse or re-expand a toggled bubble.
|
|
988
|
+
const userCollapseOverflow = new Map<string, boolean>();
|
|
989
|
+
const userCollapseToggled = new Map<string, boolean>();
|
|
990
|
+
// Voice transcripts rendered while interim. Their final render stays
|
|
991
|
+
// expanded ("settling") so the post-morph sync applies the collapse with
|
|
992
|
+
// the viewport held, rather than the morph collapsing it underneath.
|
|
993
|
+
const userCollapseLive = new Set<string>();
|
|
994
|
+
const resolveUserCollapse = (message: AgentWidgetMessage): UserMessageCollapseRender | undefined => {
|
|
995
|
+
if (message.role !== "user" || !isUserMessageCollapseEnabled(config)) return undefined;
|
|
996
|
+
if (message.voiceProcessing) userCollapseLive.add(message.id);
|
|
997
|
+
const state: UserMessageCollapseState = message.voiceProcessing
|
|
998
|
+
? "live"
|
|
999
|
+
: userCollapseLive.has(message.id)
|
|
1000
|
+
? "settling"
|
|
1001
|
+
: !userCollapseOverflow.get(message.id)
|
|
1002
|
+
? "fit"
|
|
1003
|
+
: userCollapseToggled.get(message.id) ? "expanded" : "collapsed";
|
|
1004
|
+
return {
|
|
1005
|
+
state,
|
|
1006
|
+
maxHeight: resolveUserMessageCollapseMaxHeight(config),
|
|
1007
|
+
...resolveUserMessageCollapseLabels(config),
|
|
1008
|
+
};
|
|
1009
|
+
};
|
|
974
1010
|
const applyTooltipTiming = (): void => {
|
|
975
1011
|
configureTooltipTiming({
|
|
976
1012
|
delayMs: config.tooltip?.delayMs ?? DEFAULT_TOOLTIP_DELAY_MS,
|
|
@@ -1408,8 +1444,9 @@ export const createAgentExperience = (
|
|
|
1408
1444
|
const messageActionCallbacks: MessageActionCallbacks = {
|
|
1409
1445
|
onCopy: (message: AgentWidgetMessage) => {
|
|
1410
1446
|
eventBus.emit("message:copy", message);
|
|
1411
|
-
// Send copy feedback to API if in client token mode
|
|
1412
|
-
|
|
1447
|
+
// Send copy feedback to API if in client token mode. Operator messages
|
|
1448
|
+
// are host-injected and unknown to the API, so they never report.
|
|
1449
|
+
if (session?.isClientTokenMode() && message.role !== "operator") {
|
|
1413
1450
|
session.submitMessageFeedback(message.id, 'copy').catch((error) => {
|
|
1414
1451
|
if (config.debug) {
|
|
1415
1452
|
// eslint-disable-next-line no-console
|
|
@@ -2862,6 +2899,40 @@ export const createAgentExperience = (
|
|
|
2862
2899
|
}
|
|
2863
2900
|
}, { capture: true });
|
|
2864
2901
|
|
|
2902
|
+
// "Show more" / "Show less" on a long user message. Toggling is reading
|
|
2903
|
+
// intent like a tool disclosure: it pauses follow and never re-anchors.
|
|
2904
|
+
const setUserCollapseExpanded = (bubble: HTMLElement, expanded: boolean) => {
|
|
2905
|
+
const region = bubble.querySelector<HTMLElement>(`[${USER_COLLAPSE_REGION_ATTR}]`);
|
|
2906
|
+
const messageId = bubble.getAttribute("data-message-id");
|
|
2907
|
+
if (!region || !messageId) return;
|
|
2908
|
+
markReaderEngaged();
|
|
2909
|
+
pauseAutoScroll();
|
|
2910
|
+
userCollapseToggled.set(messageId, expanded);
|
|
2911
|
+
applyUserCollapseChange(bubble, region, messageId, {
|
|
2912
|
+
state: expanded ? "expanded" : "collapsed",
|
|
2913
|
+
maxHeight: resolveUserMessageCollapseMaxHeight(config),
|
|
2914
|
+
...resolveUserMessageCollapseLabels(config),
|
|
2915
|
+
}, "toggle");
|
|
2916
|
+
};
|
|
2917
|
+
messagesWrapper.addEventListener("click", (event) => {
|
|
2918
|
+
const target = event.target;
|
|
2919
|
+
if (!(target instanceof Element)) return;
|
|
2920
|
+
const toggle = target.closest<HTMLButtonElement>(`button[${USER_COLLAPSE_TOGGLE_ATTR}]`);
|
|
2921
|
+
const bubble = toggle?.closest<HTMLElement>("[data-message-id]");
|
|
2922
|
+
if (!toggle || !bubble) return;
|
|
2923
|
+
event.preventDefault();
|
|
2924
|
+
setUserCollapseExpanded(bubble, toggle.getAttribute("aria-expanded") !== "true");
|
|
2925
|
+
});
|
|
2926
|
+
// Keyboard focus landing on a link or control inside the clipped part
|
|
2927
|
+
// expands the message, so a focused element is never hidden behind the clip.
|
|
2928
|
+
messagesWrapper.addEventListener("focusin", (event) => {
|
|
2929
|
+
const target = event.target;
|
|
2930
|
+
if (!(target instanceof Element)) return;
|
|
2931
|
+
const region = target.closest(`[${USER_COLLAPSE_REGION_ATTR}="collapsed"]`);
|
|
2932
|
+
const bubble = region?.closest<HTMLElement>("[data-message-id]");
|
|
2933
|
+
if (bubble) setUserCollapseExpanded(bubble, true);
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2865
2936
|
// Add event delegation for reasoning and tool bubble expansion
|
|
2866
2937
|
// This handles clicks even after idiomorph morphs the DOM
|
|
2867
2938
|
const handleBubbleExpansion = (event: Event) => {
|
|
@@ -5118,14 +5189,13 @@ export const createAgentExperience = (
|
|
|
5118
5189
|
// pinned and never re-arms the fallback, so a late-loading embed can't yank
|
|
5119
5190
|
// the viewport down to the bottom.
|
|
5120
5191
|
let currentTurnAnchored = false;
|
|
5121
|
-
//
|
|
5122
|
-
//
|
|
5123
|
-
//
|
|
5192
|
+
// The sent user message the anchor is parked on. It holds for the whole
|
|
5193
|
+
// reply, however tall the message or its tool prelude: the jump-to-latest
|
|
5194
|
+
// control is how the reader reaches output below the fold.
|
|
5124
5195
|
let anchoredMessageId: string | null = null;
|
|
5125
|
-
// Has the reader taken over positioning this turn?
|
|
5126
|
-
//
|
|
5127
|
-
//
|
|
5128
|
-
// viewport under them. Reset on each user send (an explicit "take me along").
|
|
5196
|
+
// Has the reader taken over positioning this turn? Layout re-pins (a host
|
|
5197
|
+
// move) stand down once they demonstrably are, and never move the viewport
|
|
5198
|
+
// under them. Reset on each user send (an explicit "take me along").
|
|
5129
5199
|
//
|
|
5130
5200
|
// Driven by real INPUT (wheel, touch drag, pointer press, transcript
|
|
5131
5201
|
// keyboard nav, focus, selection) rather than by scroll deltas: streaming
|
|
@@ -5486,9 +5556,9 @@ export const createAgentExperience = (
|
|
|
5486
5556
|
(getScrollMode() !== "anchor-top" || isActivityWhilePinnedEnabled());
|
|
5487
5557
|
|
|
5488
5558
|
const updateScrollToBottomCountBadge = () => {
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5559
|
+
const visible = scrollToBottomButton.getAttribute("data-persona-scroll-to-bottom-visible") === "true";
|
|
5560
|
+
scrollToBottomControl.update(config, visible, isStreamingOutOfView(), newMessagesSincePause,
|
|
5561
|
+
visible && isApprovalBelow());
|
|
5492
5562
|
};
|
|
5493
5563
|
|
|
5494
5564
|
const resetNewMessagesCount = () => {
|
|
@@ -5500,14 +5570,42 @@ export const createAgentExperience = (
|
|
|
5500
5570
|
// Spacer air and composer reserve are scrollable, but aren't unread content.
|
|
5501
5571
|
// Compare the real content bottom with the readable viewport instead. An
|
|
5502
5572
|
// overlay footer clips that viewport; a block footer sits below it.
|
|
5503
|
-
const
|
|
5504
|
-
const contentHeight = measureContentHeight(anchorState?.spacerHeight ?? 0, false);
|
|
5573
|
+
const readableViewportHeight = () => {
|
|
5505
5574
|
const bodyRect = body.getBoundingClientRect();
|
|
5506
5575
|
const footerRect = footer.getBoundingClientRect();
|
|
5507
|
-
|
|
5576
|
+
return footerRect.height > 0 && footerRect.bottom > bodyRect.top
|
|
5508
5577
|
? Math.min(body.clientHeight, Math.max(0, footerRect.top - bodyRect.top - body.clientTop))
|
|
5509
5578
|
: body.clientHeight;
|
|
5510
|
-
|
|
5579
|
+
};
|
|
5580
|
+
const isNearLatestContent = () => {
|
|
5581
|
+
const contentHeight = measureContentHeight(anchorState?.spacerHeight ?? 0, false);
|
|
5582
|
+
return contentHeight - body.scrollTop - readableViewportHeight() <= BOTTOM_THRESHOLD;
|
|
5583
|
+
};
|
|
5584
|
+
|
|
5585
|
+
// A pending approval whose card starts below the readable viewport. The
|
|
5586
|
+
// anchor never scrolls the reader to it, so the jump control names it.
|
|
5587
|
+
const isBelowFold = (element: HTMLElement) =>
|
|
5588
|
+
offsetTopWithinBody(element) > body.scrollTop + readableViewportHeight() - BOTTOM_THRESHOLD;
|
|
5589
|
+
const pendingApprovalBubbles = () => (session?.getMessages() ?? [])
|
|
5590
|
+
.filter((message) => message.variant === "approval" && message.approval?.status === "pending")
|
|
5591
|
+
.map((message) => queryMessageBubble(message.id))
|
|
5592
|
+
.filter((bubble): bubble is HTMLElement => Boolean(bubble?.isConnected));
|
|
5593
|
+
const isApprovalBelow = () => pendingApprovalBubbles().some(isBelowFold);
|
|
5594
|
+
const isFullyInView = (element: HTMLElement) => {
|
|
5595
|
+
const top = offsetTopWithinBody(element);
|
|
5596
|
+
return top >= body.scrollTop && top + element.offsetHeight <= body.scrollTop + readableViewportHeight();
|
|
5597
|
+
};
|
|
5598
|
+
// The approvals a jump from the "Approval needed" control was taken for.
|
|
5599
|
+
// Once it lands, focus moves to the first of them whose primary action is
|
|
5600
|
+
// fully in view, and never to one the reader can't see.
|
|
5601
|
+
let approvalJumpTargets: string[] = [];
|
|
5602
|
+
const focusApprovalJumpTarget = () => {
|
|
5603
|
+
const targets = approvalJumpTargets;
|
|
5604
|
+
approvalJumpTargets = [];
|
|
5605
|
+
for (const id of targets) {
|
|
5606
|
+
const allow = queryMessageBubble(id)?.querySelector<HTMLButtonElement>('[data-action="allow"]');
|
|
5607
|
+
if (allow && isFullyInView(allow)) { allow.focus({ preventScroll: true }); return; }
|
|
5608
|
+
}
|
|
5511
5609
|
};
|
|
5512
5610
|
|
|
5513
5611
|
// Whether the user is currently away from the latest content: drives both
|
|
@@ -5539,7 +5637,7 @@ export const createAgentExperience = (
|
|
|
5539
5637
|
const hasOverflow = getScrollBottomOffset(body) > 0;
|
|
5540
5638
|
const show = !latestScroll.active && hasOverflow && isAwayFromLatest() && transcriptHasMessages();
|
|
5541
5639
|
if (!show) newMessagesSincePause = 0;
|
|
5542
|
-
scrollToBottomControl.update(config, show, isStreamingOutOfView(), newMessagesSincePause);
|
|
5640
|
+
scrollToBottomControl.update(config, show, isStreamingOutOfView(), newMessagesSincePause, show && isApprovalBelow());
|
|
5543
5641
|
};
|
|
5544
5642
|
|
|
5545
5643
|
const pauseAutoScroll = () => {
|
|
@@ -5609,8 +5707,8 @@ export const createAgentExperience = (
|
|
|
5609
5707
|
|
|
5610
5708
|
const latestScroll = createScrollToLatest(body, container,
|
|
5611
5709
|
() => { lastScrollTop = body.scrollTop; },
|
|
5612
|
-
() => { resumeAutoScroll(); scheduleAutoScroll(true); syncScrollToBottomButton(); },
|
|
5613
|
-
() => { autoFollow.pause(); syncScrollToBottomButton(); });
|
|
5710
|
+
() => { resumeAutoScroll(); scheduleAutoScroll(true); syncScrollToBottomButton(); focusApprovalJumpTarget(); },
|
|
5711
|
+
() => { approvalJumpTargets = []; autoFollow.pause(); syncScrollToBottomButton(); });
|
|
5614
5712
|
|
|
5615
5713
|
// Custom smooth scroll animation with easing
|
|
5616
5714
|
const smoothScrollToBottom = (element: HTMLElement, duration = 500) => {
|
|
@@ -5657,9 +5755,6 @@ export const createAgentExperience = (
|
|
|
5657
5755
|
syncScrollToBottomButton();
|
|
5658
5756
|
};
|
|
5659
5757
|
|
|
5660
|
-
// Defined here (rather than beside the scroll listeners) because
|
|
5661
|
-
// `maybeAnchorToFirstUnread` runs on every render and would otherwise hit
|
|
5662
|
-
// these consts in their temporal dead zone on a synchronous first paint.
|
|
5663
5758
|
const getTranscriptSelection = (): Selection | null => {
|
|
5664
5759
|
// Selections inside a shadow root are not always reflected by
|
|
5665
5760
|
// document.getSelection(); prefer the shadow root's view when available
|
|
@@ -5793,14 +5888,6 @@ export const createAgentExperience = (
|
|
|
5793
5888
|
`[data-message-id="${escapeMessageId(messageId)}"]`
|
|
5794
5889
|
);
|
|
5795
5890
|
|
|
5796
|
-
// Progress inside a turn disclosure belongs to its visible header. Pinning
|
|
5797
|
-
// the nested prose instead clips that header, and leaves a hidden anchor
|
|
5798
|
-
// when the completed turn folds away. Keep ordinary message lookups intact.
|
|
5799
|
-
const queryScrollAnchor = (messageId: string): HTMLElement | null => {
|
|
5800
|
-
const bubble = queryMessageBubble(messageId);
|
|
5801
|
-
return bubble?.closest<HTMLElement>("[data-persona-turn-activity]") ?? bubble;
|
|
5802
|
-
};
|
|
5803
|
-
|
|
5804
5891
|
// Anchor-top mode: scroll `messageId` to rest `anchorTopOffset` px below the
|
|
5805
5892
|
// viewport top and hold it there while the response streams in beneath it.
|
|
5806
5893
|
// Deferred one frame so the message bubble has been rendered and laid out.
|
|
@@ -5810,7 +5897,7 @@ export const createAgentExperience = (
|
|
|
5810
5897
|
}
|
|
5811
5898
|
anchorRAF = requestAnimationFrame(() => {
|
|
5812
5899
|
anchorRAF = null;
|
|
5813
|
-
const bubble =
|
|
5900
|
+
const bubble = queryMessageBubble(messageId);
|
|
5814
5901
|
if (!bubble) return;
|
|
5815
5902
|
|
|
5816
5903
|
// Bubble top relative to the scroll content. offsetTop is used instead
|
|
@@ -5864,7 +5951,7 @@ export const createAgentExperience = (
|
|
|
5864
5951
|
const repinAnchoredMessage = () => {
|
|
5865
5952
|
if (getScrollMode() !== "anchor-top") return;
|
|
5866
5953
|
if (!anchorState || !anchoredMessageId) return;
|
|
5867
|
-
const bubble =
|
|
5954
|
+
const bubble = queryMessageBubble(anchoredMessageId);
|
|
5868
5955
|
if (!bubble) return;
|
|
5869
5956
|
cancelSmoothScroll();
|
|
5870
5957
|
const previousSpacerHeight = anchorState.spacerHeight;
|
|
@@ -5918,6 +6005,94 @@ export const createAgentExperience = (
|
|
|
5918
6005
|
syncScrollToBottomButton();
|
|
5919
6006
|
};
|
|
5920
6007
|
|
|
6008
|
+
// Re-applies a user bubble's collapse state in place, holding the viewport.
|
|
6009
|
+
// A toggle holds the bubble's top (a collapse whose top scrolled out of view
|
|
6010
|
+
// brings it back to the anchor offset, so the toggle stays in view). A measurement
|
|
6011
|
+
// change holds what the reader sees: the top when it is in view, otherwise
|
|
6012
|
+
// the bubble's bottom edge, so the content below never moves. While
|
|
6013
|
+
// anchored, the spacer is re-reserved first so a shrink can't clamp
|
|
6014
|
+
// scrollTop, then trimmed to the held position with a fresh drain baseline.
|
|
6015
|
+
const applyUserCollapseChange = (
|
|
6016
|
+
bubble: HTMLElement,
|
|
6017
|
+
region: HTMLElement,
|
|
6018
|
+
messageId: string,
|
|
6019
|
+
render: UserMessageCollapseRender,
|
|
6020
|
+
intent: "toggle" | "measure"
|
|
6021
|
+
) => {
|
|
6022
|
+
messageCache.delete(messageId);
|
|
6023
|
+
const holdViewport = intent === "toggle" || !(isFollowEffective() && autoFollow.isFollowing());
|
|
6024
|
+
if (!holdViewport) {
|
|
6025
|
+
applyUserCollapseState(bubble, region, messageId, render);
|
|
6026
|
+
return;
|
|
6027
|
+
}
|
|
6028
|
+
const bodyTop = body.getBoundingClientRect().top;
|
|
6029
|
+
const before = bubble.getBoundingClientRect();
|
|
6030
|
+
const anchor = anchorState;
|
|
6031
|
+
if (anchor) {
|
|
6032
|
+
cancelSmoothScroll();
|
|
6033
|
+
setAnchorSpacerHeight(anchor.spacerHeight + bubble.offsetHeight);
|
|
6034
|
+
}
|
|
6035
|
+
applyUserCollapseState(bubble, region, messageId, render);
|
|
6036
|
+
const after = bubble.getBoundingClientRect();
|
|
6037
|
+
const prevTop = before.top - bodyTop;
|
|
6038
|
+
const shift = intent === "toggle"
|
|
6039
|
+
? after.top - bodyTop - (render.state === "collapsed" && prevTop < 0 ? getAnchorTopOffset() : prevTop)
|
|
6040
|
+
: prevTop >= 0 ? after.top - before.top : after.bottom - before.bottom;
|
|
6041
|
+
const target = Math.max(0, body.scrollTop + shift);
|
|
6042
|
+
if (anchor && anchorState === anchor) {
|
|
6043
|
+
setAnchorSpacerHeight(target + body.clientHeight + BOTTOM_THRESHOLD - measureContentHeight(anchor.spacerHeight));
|
|
6044
|
+
anchor.initialSpacerHeight = anchor.spacerHeight;
|
|
6045
|
+
anchor.contentHeightAtAnchor = body.scrollHeight - anchor.spacerHeight;
|
|
6046
|
+
}
|
|
6047
|
+
if (Math.abs(target - body.scrollTop) >= 1) {
|
|
6048
|
+
isAutoScrolling = true;
|
|
6049
|
+
body.scrollTop = target;
|
|
6050
|
+
lastScrollTop = body.scrollTop;
|
|
6051
|
+
isAutoScrolling = false;
|
|
6052
|
+
}
|
|
6053
|
+
syncScrollToBottomButton();
|
|
6054
|
+
};
|
|
6055
|
+
|
|
6056
|
+
// Measures every built-in user bubble's real rendered height (markdown,
|
|
6057
|
+
// code, wrapping, attachments) after a morph or resize and collapses the
|
|
6058
|
+
// ones that overflow. Unlaid regions (a closed panel, jsdom) measure 0 and
|
|
6059
|
+
// keep their state until a later pass can measure them.
|
|
6060
|
+
const syncUserMessageCollapse = () => {
|
|
6061
|
+
if (!isUserMessageCollapseEnabled(config)) return;
|
|
6062
|
+
const maxHeight = resolveUserMessageCollapseMaxHeight(config);
|
|
6063
|
+
for (const region of Array.from(
|
|
6064
|
+
messagesWrapper.querySelectorAll<HTMLElement>(`[${USER_COLLAPSE_REGION_ATTR}]`)
|
|
6065
|
+
)) {
|
|
6066
|
+
const bubble = region.closest<HTMLElement>("[data-message-id]");
|
|
6067
|
+
const messageId = bubble?.getAttribute("data-message-id");
|
|
6068
|
+
if (!bubble || !messageId) continue;
|
|
6069
|
+
const naturalHeight = region.scrollHeight;
|
|
6070
|
+
if (naturalHeight > 0) {
|
|
6071
|
+
userCollapseOverflow.set(messageId, userMessageOverflows(naturalHeight, maxHeight));
|
|
6072
|
+
}
|
|
6073
|
+
const overflows = userCollapseOverflow.get(messageId) ?? false;
|
|
6074
|
+
const current = region.getAttribute(USER_COLLAPSE_REGION_ATTR);
|
|
6075
|
+
// An interim voice transcript stays expanded so the speaker can watch it.
|
|
6076
|
+
if (current === "live") continue;
|
|
6077
|
+
if (current === "settling") userCollapseLive.delete(messageId);
|
|
6078
|
+
const state: UserMessageCollapseState = !overflows
|
|
6079
|
+
? "fit"
|
|
6080
|
+
: userCollapseToggled.get(messageId) ? "expanded" : "collapsed";
|
|
6081
|
+
if (state === current) continue;
|
|
6082
|
+
applyUserCollapseChange(bubble, region, messageId, {
|
|
6083
|
+
state, maxHeight, ...resolveUserMessageCollapseLabels(config),
|
|
6084
|
+
}, "measure");
|
|
6085
|
+
}
|
|
6086
|
+
};
|
|
6087
|
+
|
|
6088
|
+
const pruneUserMessageCollapse = (messages: AgentWidgetMessage[]) => {
|
|
6089
|
+
if (userCollapseOverflow.size === 0 && userCollapseToggled.size === 0 && userCollapseLive.size === 0) return;
|
|
6090
|
+
const ids = new Set(messages.map((message) => message.id));
|
|
6091
|
+
for (const map of [userCollapseOverflow, userCollapseToggled, userCollapseLive]) {
|
|
6092
|
+
for (const id of map.keys()) if (!ids.has(id)) map.delete(id);
|
|
6093
|
+
}
|
|
6094
|
+
};
|
|
6095
|
+
|
|
5921
6096
|
// Reacts to a user message the user just sent (seeded so restored history
|
|
5922
6097
|
// never triggers it). Follow mode re-sticks to the bottom even if the user
|
|
5923
6098
|
// had scrolled up: sending is an unambiguous "take me to the latest"
|
|
@@ -5933,8 +6108,8 @@ export const createAgentExperience = (
|
|
|
5933
6108
|
// next user send.
|
|
5934
6109
|
followFallbackActive = false;
|
|
5935
6110
|
currentTurnAnchored = true;
|
|
5936
|
-
// A send is an explicit "take me along": it re-arms
|
|
5937
|
-
//
|
|
6111
|
+
// A send is an explicit "take me along": it re-arms anchoring even if
|
|
6112
|
+
// the reader had scrolled away during the last turn.
|
|
5938
6113
|
readerEngagedThisTurn = false;
|
|
5939
6114
|
anchoredMessageId = messageId;
|
|
5940
6115
|
scheduleAnchorToMessage(messageId);
|
|
@@ -5960,75 +6135,6 @@ export const createAgentExperience = (
|
|
|
5960
6135
|
scheduleAutoScroll(true);
|
|
5961
6136
|
};
|
|
5962
6137
|
|
|
5963
|
-
// Anchor-top positioning: park on the turn's FIRST UNREAD attention-worthy
|
|
5964
|
-
// block.
|
|
5965
|
-
//
|
|
5966
|
-
// The read boundary is the last user send — the reader wrote it, so it (and
|
|
5967
|
-
// everything above) is read; every attention-worthy block after it is not.
|
|
5968
|
-
// Tool calls and reasoning are chrome and never targeted, so they scroll
|
|
5969
|
-
// past underneath rather than parking the reader on progress output.
|
|
5970
|
-
//
|
|
5971
|
-
// The policy assumes the reader is NOT watching the stream, and picks the
|
|
5972
|
-
// best place for them to land when they look back: the start of what they
|
|
5973
|
-
// haven't seen. `readerEngagedThisTurn` is the escape hatch — once someone
|
|
5974
|
-
// is demonstrably driving, their position wins and this stands down until
|
|
5975
|
-
// the next send.
|
|
5976
|
-
//
|
|
5977
|
-
// Anchoring to the first unread (not the newest) block means this fires at
|
|
5978
|
-
// most once per turn in practice, and later blocks stream in below the
|
|
5979
|
-
// anchor exactly as they do today.
|
|
5980
|
-
|
|
5981
|
-
// Minimum readable slice of the first unread block that must fit under the
|
|
5982
|
-
// pinned user message for the anchor to stay put. Below this, the block is
|
|
5983
|
-
// "stranded" and the anchor hands off to it.
|
|
5984
|
-
const ANCHOR_HANDOFF_MIN_VISIBLE = 96;
|
|
5985
|
-
|
|
5986
|
-
// Market pattern (ChatGPT/Claude/Gemini): the sent user message stays
|
|
5987
|
-
// pinned at the top as the visible header of the streaming reply. The
|
|
5988
|
-
// handoff to the first unread block exists for the stranded case — a long
|
|
5989
|
-
// user message or a tall tool-call prelude pushing the reply below the
|
|
5990
|
-
// fold — so it only fires when the block's start is not readably within
|
|
5991
|
-
// the viewport while the user message holds the anchor.
|
|
5992
|
-
const isFirstUnreadStranded = (messageId: string): boolean => {
|
|
5993
|
-
if (lastSentUserMessageId === null) return true;
|
|
5994
|
-
const userBubble = queryMessageBubble(lastSentUserMessageId);
|
|
5995
|
-
const blockBubble = queryScrollAnchor(messageId);
|
|
5996
|
-
// Unmeasurable geometry: fall back to handing off (pre-conditional
|
|
5997
|
-
// behavior) rather than risking an answer parked below the fold.
|
|
5998
|
-
if (!userBubble || !blockBubble) return true;
|
|
5999
|
-
const delta =
|
|
6000
|
-
offsetTopWithinBody(blockBubble) - offsetTopWithinBody(userBubble);
|
|
6001
|
-
return (
|
|
6002
|
-
getAnchorTopOffset() + delta >
|
|
6003
|
-
body.clientHeight - ANCHOR_HANDOFF_MIN_VISIBLE
|
|
6004
|
-
);
|
|
6005
|
-
};
|
|
6006
|
-
|
|
6007
|
-
const maybeAnchorToFirstUnread = (messages: AgentWidgetMessage[]) => {
|
|
6008
|
-
if (getScrollMode() !== "anchor-top") return;
|
|
6009
|
-
if (!currentTurnAnchored) return;
|
|
6010
|
-
if (readerEngagedThisTurn) return;
|
|
6011
|
-
if (hasActiveTranscriptSelection()) return;
|
|
6012
|
-
|
|
6013
|
-
let pastReadBoundary = lastSentUserMessageId === null;
|
|
6014
|
-
for (const message of messages) {
|
|
6015
|
-
if (!pastReadBoundary) {
|
|
6016
|
-
if (message.id === lastSentUserMessageId) pastReadBoundary = true;
|
|
6017
|
-
continue;
|
|
6018
|
-
}
|
|
6019
|
-
if (!isAttentionWorthyMessage(message)) continue;
|
|
6020
|
-
// Already parked on it: hold still while it streams.
|
|
6021
|
-
if (message.id === anchoredMessageId) return;
|
|
6022
|
-
// Block starts within view under the pinned user message: keep the
|
|
6023
|
-
// user message anchored. Re-evaluated every render, so a tool prelude
|
|
6024
|
-
// that later pushes the block below the fold still hands off.
|
|
6025
|
-
if (!isFirstUnreadStranded(message.id)) return;
|
|
6026
|
-
anchoredMessageId = message.id;
|
|
6027
|
-
scheduleAnchorToMessage(message.id);
|
|
6028
|
-
return;
|
|
6029
|
-
}
|
|
6030
|
-
};
|
|
6031
|
-
|
|
6032
6138
|
const trackMessages = (messages: AgentWidgetMessage[]) => {
|
|
6033
6139
|
const nextState = new Map<
|
|
6034
6140
|
string,
|
|
@@ -6097,21 +6203,26 @@ export const createAgentExperience = (
|
|
|
6097
6203
|
wrapper: HTMLElement,
|
|
6098
6204
|
role: AgentWidgetMessage["role"]
|
|
6099
6205
|
): void => {
|
|
6206
|
+
// Operator rows sit on the assistant's side and share its max width, but
|
|
6207
|
+
// always shrink-wrap: a full-column operator bubble reads as an input.
|
|
6100
6208
|
const sizingRole = role === "user" ? "user" : "assistant";
|
|
6101
6209
|
const roleLayout = config.layout?.messages?.[sizingRole];
|
|
6102
|
-
const
|
|
6210
|
+
const configuredWidth = roleLayout?.width ?? "content";
|
|
6211
|
+
const width = role === "operator" ? "content" : configuredWidth;
|
|
6103
6212
|
// Only an explicit config value is stamped inline; the 85%/100% defaults
|
|
6104
6213
|
// live in widget.css so `components.message.<role>.maxWidth` can win.
|
|
6214
|
+
const fullMaxWidth = role === "operator"
|
|
6215
|
+
? "var(--persona-message-operator-max-width, var(--persona-message-assistant-max-width, 100%))"
|
|
6216
|
+
: `var(--persona-message-${sizingRole}-max-width, 100%)`;
|
|
6105
6217
|
const maxWidth =
|
|
6106
|
-
roleLayout?.maxWidth ?? (
|
|
6107
|
-
? `var(--persona-message-${sizingRole}-max-width, 100%)`
|
|
6108
|
-
: undefined);
|
|
6218
|
+
roleLayout?.maxWidth ?? (configuredWidth === "full" ? fullMaxWidth : undefined);
|
|
6109
6219
|
|
|
6110
6220
|
wrapper.classList.add("persona-message-row");
|
|
6111
6221
|
wrapper.classList.remove(
|
|
6112
6222
|
"persona-message-row-user",
|
|
6113
6223
|
"persona-message-row-assistant",
|
|
6114
6224
|
"persona-message-row-system",
|
|
6225
|
+
"persona-message-row-operator",
|
|
6115
6226
|
"persona-message-width-content",
|
|
6116
6227
|
"persona-message-width-full"
|
|
6117
6228
|
);
|
|
@@ -6248,6 +6359,10 @@ export const createAgentExperience = (
|
|
|
6248
6359
|
? findRetryableAssistantMessageId(messages)
|
|
6249
6360
|
: null;
|
|
6250
6361
|
const editEnabled = actionsCfg?.showEdit === true && actionsCfg.enabled !== false;
|
|
6362
|
+
let lastOperatorIndex = -1;
|
|
6363
|
+
messages.forEach((message, index) => {
|
|
6364
|
+
if (message.role === "operator") lastOperatorIndex = index;
|
|
6365
|
+
});
|
|
6251
6366
|
const quoteEnabled = actionsCfg?.showQuote === true && actionsCfg.enabled !== false;
|
|
6252
6367
|
/** The live editor, re-grafted into its morphed wrapper after the pass. */
|
|
6253
6368
|
let editHydrate: { messageId: string; element: HTMLElement } | null = null;
|
|
@@ -6289,7 +6404,7 @@ export const createAgentExperience = (
|
|
|
6289
6404
|
);
|
|
6290
6405
|
};
|
|
6291
6406
|
|
|
6292
|
-
messages.forEach((message) => {
|
|
6407
|
+
messages.forEach((message, messageIndex) => {
|
|
6293
6408
|
if (isDroppedCompletedRow(message)) {
|
|
6294
6409
|
return;
|
|
6295
6410
|
}
|
|
@@ -6356,7 +6471,7 @@ export const createAgentExperience = (
|
|
|
6356
6471
|
isLastAssistant: message.id === lastAssistantId,
|
|
6357
6472
|
canRegenerate: retryableAssistantId === message.id &&
|
|
6358
6473
|
(actionsCfg?.showRegenerate === true || message.dispatchError === true),
|
|
6359
|
-
canEdit: editEnabled && isEditableUserMessage(message),
|
|
6474
|
+
canEdit: editEnabled && messageIndex > lastOperatorIndex && isEditableUserMessage(message),
|
|
6360
6475
|
canQuote:
|
|
6361
6476
|
quoteEnabled && !message.variant && (message.content?.trim().length ?? 0) > 0,
|
|
6362
6477
|
};
|
|
@@ -6381,8 +6496,10 @@ export const createAgentExperience = (
|
|
|
6381
6496
|
? { toolCallId: linkedTool.id, status: linkedTool.status === "running"
|
|
6382
6497
|
? "running" : linkedTool.success === false || linkedTool.error ? "failed" : "completed" }
|
|
6383
6498
|
: undefined;
|
|
6499
|
+
const userCollapse = resolveUserCollapse(message);
|
|
6384
6500
|
const fingerprint =
|
|
6385
6501
|
computeMessageFingerprint(message, configVersion) + askMeta + `:${eligibilityKey}` +
|
|
6502
|
+
(userCollapse ? `:uc-${userCollapse.state}` : "") +
|
|
6386
6503
|
(approvalWithPlugin ? `:${message.approval?.toolCallId ?? ""}:${JSON.stringify(execution)}` : "");
|
|
6387
6504
|
const cachedWrapper = (askWithPlugin || approvalWithPlugin || hasDirectiveBubble ||
|
|
6388
6505
|
(message.dispatchError && config.errorDisplay === "block" && !approvalUi))
|
|
@@ -6431,7 +6548,8 @@ export const createAgentExperience = (
|
|
|
6431
6548
|
// Keep the detail readable even while the secondary UI chunk loads.
|
|
6432
6549
|
const bubble = createStandardBubble(message, transform, config.layout?.messages,
|
|
6433
6550
|
config.messageActions, messageActionCallbacks, {
|
|
6434
|
-
loadingIndicatorRenderer: inlineLoadingRenderer, widgetConfig: config, actionEligibility
|
|
6551
|
+
loadingIndicatorRenderer: inlineLoadingRenderer, widgetConfig: config, actionEligibility,
|
|
6552
|
+
userCollapse
|
|
6435
6553
|
});
|
|
6436
6554
|
bubble.dataset.personaDefaultMessage = "true";
|
|
6437
6555
|
return bubble;
|
|
@@ -6957,10 +7075,23 @@ export const createAgentExperience = (
|
|
|
6957
7075
|
// "the assistant is already visibly responding": otherwise the transcript
|
|
6958
7076
|
// goes blank for the whole tool phase. When those rows are shown, their own
|
|
6959
7077
|
// bubble carries the loading animation and the standalone dots stay hidden.
|
|
7078
|
+
//
|
|
7079
|
+
// A pending `ask_user_question` call is visible only once its question has
|
|
7080
|
+
// rendered: a plugin bubble mounted for it this pass, or the built-in sheet
|
|
7081
|
+
// mounted for its tool call. Until then (arguments still streaming, a plugin
|
|
7082
|
+
// returning null for a partial payload, the sheet waiting on the pause) it
|
|
7083
|
+
// renders nothing, so it must not hide the dots either.
|
|
7084
|
+
const isUnrenderedAskRow = (msg: AgentWidgetMessage): boolean =>
|
|
7085
|
+
!(msg.toolCall?.id && liveAskToolIds.has(msg.toolCall.id)) &&
|
|
7086
|
+
!askPluginHydrate.some((entry) => entry.messageId === msg.id);
|
|
6960
7087
|
const isHiddenActivityRow = (msg: AgentWidgetMessage): boolean =>
|
|
6961
|
-
(msg
|
|
6962
|
-
|
|
6963
|
-
|
|
7088
|
+
isAskUserQuestionMessage(msg) &&
|
|
7089
|
+
msg.agentMetadata?.askUserQuestionAnswered !== true &&
|
|
7090
|
+
config.features?.askUserQuestion?.enabled !== false
|
|
7091
|
+
? isUnrenderedAskRow(msg)
|
|
7092
|
+
: (msg.variant === "tool" && !showToolCalls) ||
|
|
7093
|
+
(msg.variant === "reasoning" && !showReasoning) ||
|
|
7094
|
+
isDroppedCompletedRow(msg);
|
|
6964
7095
|
const hasStreamingAssistantMessage = messages.some(
|
|
6965
7096
|
(msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
|
|
6966
7097
|
);
|
|
@@ -7203,7 +7334,12 @@ export const createAgentExperience = (
|
|
|
7203
7334
|
wrapper.replaceChildren(bubble);
|
|
7204
7335
|
wrapper.setAttribute("data-bubble-fp", fingerprint);
|
|
7205
7336
|
lastApprovalBubbleFingerprint.set(messageId, fingerprint);
|
|
7206
|
-
|
|
7337
|
+
// Never park focus (and Enter-to-allow) on a card the reader can't
|
|
7338
|
+
// see: a paused reader below the fold gets the "Approval needed" jump.
|
|
7339
|
+
const following = isFollowEffective() && autoFollow.isFollowing();
|
|
7340
|
+
if (focusDefault && (following || !isBelowFold(wrapper))) {
|
|
7341
|
+
bubble.querySelector<HTMLButtonElement>('[data-action="allow"]')?.focus();
|
|
7342
|
+
}
|
|
7207
7343
|
}
|
|
7208
7344
|
}
|
|
7209
7345
|
|
|
@@ -7231,6 +7367,8 @@ export const createAgentExperience = (
|
|
|
7231
7367
|
) => {
|
|
7232
7368
|
if (messages.length > 0) warmClientStream();
|
|
7233
7369
|
renderMessagesWithPluginsImpl(container, messages, transform);
|
|
7370
|
+
pruneUserMessageCollapse(messages);
|
|
7371
|
+
syncUserMessageCollapse();
|
|
7234
7372
|
refreshReadAloudButtons();
|
|
7235
7373
|
if (revealedToolCalls.size > 0) {
|
|
7236
7374
|
for (const [id, timer] of revealedToolCalls) {
|
|
@@ -8681,7 +8819,6 @@ export const createAgentExperience = (
|
|
|
8681
8819
|
handleAssistantTurnStarted();
|
|
8682
8820
|
}
|
|
8683
8821
|
if (lastAssistantMessage) lastHandledAssistantId = lastAssistantMessage.id;
|
|
8684
|
-
maybeAnchorToFirstUnread(messages);
|
|
8685
8822
|
|
|
8686
8823
|
const prevLastUserMessageId = voiceState.lastUserMessageId;
|
|
8687
8824
|
if (lastUserMessage && lastUserMessage.id !== prevLastUserMessageId) {
|
|
@@ -10053,7 +10190,8 @@ export const createAgentExperience = (
|
|
|
10053
10190
|
function beginMessageEdit(messageId: string): void {
|
|
10054
10191
|
if (config.messageActions?.showEdit !== true) return;
|
|
10055
10192
|
const message = session.getMessages().find((m) => m.id === messageId);
|
|
10056
|
-
if (!message || !isEditableUserMessage(message)
|
|
10193
|
+
if (!message || !isEditableUserMessage(message) ||
|
|
10194
|
+
!canResubmitUserMessage(session.getMessages(), messageId)) return;
|
|
10057
10195
|
if (editingMessageId === messageId) return;
|
|
10058
10196
|
cancelMessageEdit();
|
|
10059
10197
|
editingMessageId = messageId;
|
|
@@ -11145,6 +11283,8 @@ export const createAgentExperience = (
|
|
|
11145
11283
|
// (viewport resize) so the pin survives all of them.
|
|
11146
11284
|
if (typeof ResizeObserver !== "undefined") {
|
|
11147
11285
|
const contentResizeObserver = new ResizeObserver(() => {
|
|
11286
|
+
// Width changes rewrap text and late images grow attachments.
|
|
11287
|
+
syncUserMessageCollapse();
|
|
11148
11288
|
handleContentResize();
|
|
11149
11289
|
syncTranscriptEdgeFades();
|
|
11150
11290
|
// A resize can start or stop a table overflowing without a render or a
|
|
@@ -11190,7 +11330,7 @@ export const createAgentExperience = (
|
|
|
11190
11330
|
]);
|
|
11191
11331
|
const handleTranscriptKeydown = (event: KeyboardEvent) => {
|
|
11192
11332
|
if (!NAV_KEYS.has(event.key)) return;
|
|
11193
|
-
// Reader intent for
|
|
11333
|
+
// Reader intent for anchor re-pins is tracked unconditionally;
|
|
11194
11334
|
// only the follow-mode *pause* below stays behind the opt-in flag.
|
|
11195
11335
|
markReaderEngaged();
|
|
11196
11336
|
if (!isPauseOnInteractionEnabled()) return;
|
|
@@ -11244,10 +11384,13 @@ export const createAgentExperience = (
|
|
|
11244
11384
|
});
|
|
11245
11385
|
destroyCallbacks.push(() => body.removeEventListener("wheel", handleWheel));
|
|
11246
11386
|
scrollToBottomButton.addEventListener("click", () => {
|
|
11387
|
+
const targets = pendingApprovalBubbles().filter(isBelowFold)
|
|
11388
|
+
.map((bubble) => bubble.getAttribute("data-message-id")!);
|
|
11247
11389
|
// Jumping to the latest abandons the current anchor: drop the spacer
|
|
11248
11390
|
// first so "bottom" means the real end of content, not spacer padding
|
|
11249
11391
|
// that would keep shrinking underneath the reader.
|
|
11250
11392
|
resetAnchorState();
|
|
11393
|
+
approvalJumpTargets = targets;
|
|
11251
11394
|
cancelAutoScroll();
|
|
11252
11395
|
autoFollow.pause();
|
|
11253
11396
|
latestScroll.start(config);
|
|
@@ -11785,6 +11928,10 @@ export const createAgentExperience = (
|
|
|
11785
11928
|
const previousShowToolCalls = config.features?.showToolCalls;
|
|
11786
11929
|
const previousToolCallDisplay = config.features?.toolCallDisplay;
|
|
11787
11930
|
const previousReasoningDisplay = config.features?.reasoningDisplay;
|
|
11931
|
+
const userCollapseConfigKey = () => JSON.stringify([
|
|
11932
|
+
config.features?.userMessageCollapse, config.copy?.showMoreLabel, config.copy?.showLessLabel,
|
|
11933
|
+
]);
|
|
11934
|
+
const previousUserCollapseConfig = userCollapseConfigKey();
|
|
11788
11935
|
const previousStreamAnimationType = config.features?.streamAnimation?.type;
|
|
11789
11936
|
// History provider identity: enablement, eligibility, and advertised
|
|
11790
11937
|
// scopes are all resolved at construction, so a change rebuilds it.
|
|
@@ -12042,7 +12189,8 @@ export const createAgentExperience = (
|
|
|
12042
12189
|
const featuresChanged = (config.features?.showReasoning ?? true) !== (previousShowReasoning ?? true)
|
|
12043
12190
|
|| (config.features?.showToolCalls ?? true) !== (previousShowToolCalls ?? true)
|
|
12044
12191
|
|| JSON.stringify(config.features?.toolCallDisplay) !== JSON.stringify(previousToolCallDisplay)
|
|
12045
|
-
|| JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay)
|
|
12192
|
+
|| JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay)
|
|
12193
|
+
|| userCollapseConfigKey() !== previousUserCollapseConfig;
|
|
12046
12194
|
const messagesConfigChanged = (config.future?.v5Defaults === true) !== previousDefaultsVersion
|
|
12047
12195
|
|| "toolCall" in nextConfig || messageActionsChanged || layoutMessagesChanged
|
|
12048
12196
|
|| "loadingIndicator" in nextConfig || iterationDisplayChanged || featuresChanged
|