@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/src/ui.ts
CHANGED
|
@@ -203,7 +203,6 @@ import {
|
|
|
203
203
|
createFollowStateController,
|
|
204
204
|
getScrollBottomOffset,
|
|
205
205
|
hasSelectionWithin,
|
|
206
|
-
isAttentionWorthyMessage,
|
|
207
206
|
isElementNearBottom,
|
|
208
207
|
resolveFollowStateFromScroll,
|
|
209
208
|
resolveFollowStateFromWheel
|
|
@@ -272,6 +271,17 @@ import { buildHeaderWithLayout } from "./components/header-layouts";
|
|
|
272
271
|
import { positionMap } from "./utils/positioning";
|
|
273
272
|
import type { HeaderElements as _HeaderElements, ComposerElements as _ComposerElements } from "./components/panel";
|
|
274
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";
|
|
275
285
|
import {
|
|
276
286
|
createStandardBubble,
|
|
277
287
|
createTypingIndicator,
|
|
@@ -972,6 +982,31 @@ export const createAgentExperience = (
|
|
|
972
982
|
const toolExpansionState = new Set<string>();
|
|
973
983
|
const reasoningExpansionState = new Set<string>();
|
|
974
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
|
+
};
|
|
975
1010
|
const applyTooltipTiming = (): void => {
|
|
976
1011
|
configureTooltipTiming({
|
|
977
1012
|
delayMs: config.tooltip?.delayMs ?? DEFAULT_TOOLTIP_DELAY_MS,
|
|
@@ -2864,6 +2899,40 @@ export const createAgentExperience = (
|
|
|
2864
2899
|
}
|
|
2865
2900
|
}, { capture: true });
|
|
2866
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
|
+
|
|
2867
2936
|
// Add event delegation for reasoning and tool bubble expansion
|
|
2868
2937
|
// This handles clicks even after idiomorph morphs the DOM
|
|
2869
2938
|
const handleBubbleExpansion = (event: Event) => {
|
|
@@ -5120,14 +5189,13 @@ export const createAgentExperience = (
|
|
|
5120
5189
|
// pinned and never re-arms the fallback, so a late-loading embed can't yank
|
|
5121
5190
|
// the viewport down to the bottom.
|
|
5122
5191
|
let currentTurnAnchored = false;
|
|
5123
|
-
//
|
|
5124
|
-
//
|
|
5125
|
-
//
|
|
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.
|
|
5126
5195
|
let anchoredMessageId: string | null = null;
|
|
5127
|
-
// Has the reader taken over positioning this turn?
|
|
5128
|
-
//
|
|
5129
|
-
//
|
|
5130
|
-
// 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").
|
|
5131
5199
|
//
|
|
5132
5200
|
// Driven by real INPUT (wheel, touch drag, pointer press, transcript
|
|
5133
5201
|
// keyboard nav, focus, selection) rather than by scroll deltas: streaming
|
|
@@ -5488,9 +5556,9 @@ export const createAgentExperience = (
|
|
|
5488
5556
|
(getScrollMode() !== "anchor-top" || isActivityWhilePinnedEnabled());
|
|
5489
5557
|
|
|
5490
5558
|
const updateScrollToBottomCountBadge = () => {
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5559
|
+
const visible = scrollToBottomButton.getAttribute("data-persona-scroll-to-bottom-visible") === "true";
|
|
5560
|
+
scrollToBottomControl.update(config, visible, isStreamingOutOfView(), newMessagesSincePause,
|
|
5561
|
+
visible && isApprovalBelow());
|
|
5494
5562
|
};
|
|
5495
5563
|
|
|
5496
5564
|
const resetNewMessagesCount = () => {
|
|
@@ -5502,14 +5570,42 @@ export const createAgentExperience = (
|
|
|
5502
5570
|
// Spacer air and composer reserve are scrollable, but aren't unread content.
|
|
5503
5571
|
// Compare the real content bottom with the readable viewport instead. An
|
|
5504
5572
|
// overlay footer clips that viewport; a block footer sits below it.
|
|
5505
|
-
const
|
|
5506
|
-
const contentHeight = measureContentHeight(anchorState?.spacerHeight ?? 0, false);
|
|
5573
|
+
const readableViewportHeight = () => {
|
|
5507
5574
|
const bodyRect = body.getBoundingClientRect();
|
|
5508
5575
|
const footerRect = footer.getBoundingClientRect();
|
|
5509
|
-
|
|
5576
|
+
return footerRect.height > 0 && footerRect.bottom > bodyRect.top
|
|
5510
5577
|
? Math.min(body.clientHeight, Math.max(0, footerRect.top - bodyRect.top - body.clientTop))
|
|
5511
5578
|
: body.clientHeight;
|
|
5512
|
-
|
|
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
|
+
}
|
|
5513
5609
|
};
|
|
5514
5610
|
|
|
5515
5611
|
// Whether the user is currently away from the latest content: drives both
|
|
@@ -5541,7 +5637,7 @@ export const createAgentExperience = (
|
|
|
5541
5637
|
const hasOverflow = getScrollBottomOffset(body) > 0;
|
|
5542
5638
|
const show = !latestScroll.active && hasOverflow && isAwayFromLatest() && transcriptHasMessages();
|
|
5543
5639
|
if (!show) newMessagesSincePause = 0;
|
|
5544
|
-
scrollToBottomControl.update(config, show, isStreamingOutOfView(), newMessagesSincePause);
|
|
5640
|
+
scrollToBottomControl.update(config, show, isStreamingOutOfView(), newMessagesSincePause, show && isApprovalBelow());
|
|
5545
5641
|
};
|
|
5546
5642
|
|
|
5547
5643
|
const pauseAutoScroll = () => {
|
|
@@ -5611,8 +5707,8 @@ export const createAgentExperience = (
|
|
|
5611
5707
|
|
|
5612
5708
|
const latestScroll = createScrollToLatest(body, container,
|
|
5613
5709
|
() => { lastScrollTop = body.scrollTop; },
|
|
5614
|
-
() => { resumeAutoScroll(); scheduleAutoScroll(true); syncScrollToBottomButton(); },
|
|
5615
|
-
() => { autoFollow.pause(); syncScrollToBottomButton(); });
|
|
5710
|
+
() => { resumeAutoScroll(); scheduleAutoScroll(true); syncScrollToBottomButton(); focusApprovalJumpTarget(); },
|
|
5711
|
+
() => { approvalJumpTargets = []; autoFollow.pause(); syncScrollToBottomButton(); });
|
|
5616
5712
|
|
|
5617
5713
|
// Custom smooth scroll animation with easing
|
|
5618
5714
|
const smoothScrollToBottom = (element: HTMLElement, duration = 500) => {
|
|
@@ -5659,9 +5755,6 @@ export const createAgentExperience = (
|
|
|
5659
5755
|
syncScrollToBottomButton();
|
|
5660
5756
|
};
|
|
5661
5757
|
|
|
5662
|
-
// Defined here (rather than beside the scroll listeners) because
|
|
5663
|
-
// `maybeAnchorToFirstUnread` runs on every render and would otherwise hit
|
|
5664
|
-
// these consts in their temporal dead zone on a synchronous first paint.
|
|
5665
5758
|
const getTranscriptSelection = (): Selection | null => {
|
|
5666
5759
|
// Selections inside a shadow root are not always reflected by
|
|
5667
5760
|
// document.getSelection(); prefer the shadow root's view when available
|
|
@@ -5795,14 +5888,6 @@ export const createAgentExperience = (
|
|
|
5795
5888
|
`[data-message-id="${escapeMessageId(messageId)}"]`
|
|
5796
5889
|
);
|
|
5797
5890
|
|
|
5798
|
-
// Progress inside a turn disclosure belongs to its visible header. Pinning
|
|
5799
|
-
// the nested prose instead clips that header, and leaves a hidden anchor
|
|
5800
|
-
// when the completed turn folds away. Keep ordinary message lookups intact.
|
|
5801
|
-
const queryScrollAnchor = (messageId: string): HTMLElement | null => {
|
|
5802
|
-
const bubble = queryMessageBubble(messageId);
|
|
5803
|
-
return bubble?.closest<HTMLElement>("[data-persona-turn-activity]") ?? bubble;
|
|
5804
|
-
};
|
|
5805
|
-
|
|
5806
5891
|
// Anchor-top mode: scroll `messageId` to rest `anchorTopOffset` px below the
|
|
5807
5892
|
// viewport top and hold it there while the response streams in beneath it.
|
|
5808
5893
|
// Deferred one frame so the message bubble has been rendered and laid out.
|
|
@@ -5812,7 +5897,7 @@ export const createAgentExperience = (
|
|
|
5812
5897
|
}
|
|
5813
5898
|
anchorRAF = requestAnimationFrame(() => {
|
|
5814
5899
|
anchorRAF = null;
|
|
5815
|
-
const bubble =
|
|
5900
|
+
const bubble = queryMessageBubble(messageId);
|
|
5816
5901
|
if (!bubble) return;
|
|
5817
5902
|
|
|
5818
5903
|
// Bubble top relative to the scroll content. offsetTop is used instead
|
|
@@ -5866,7 +5951,7 @@ export const createAgentExperience = (
|
|
|
5866
5951
|
const repinAnchoredMessage = () => {
|
|
5867
5952
|
if (getScrollMode() !== "anchor-top") return;
|
|
5868
5953
|
if (!anchorState || !anchoredMessageId) return;
|
|
5869
|
-
const bubble =
|
|
5954
|
+
const bubble = queryMessageBubble(anchoredMessageId);
|
|
5870
5955
|
if (!bubble) return;
|
|
5871
5956
|
cancelSmoothScroll();
|
|
5872
5957
|
const previousSpacerHeight = anchorState.spacerHeight;
|
|
@@ -5920,6 +6005,94 @@ export const createAgentExperience = (
|
|
|
5920
6005
|
syncScrollToBottomButton();
|
|
5921
6006
|
};
|
|
5922
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
|
+
|
|
5923
6096
|
// Reacts to a user message the user just sent (seeded so restored history
|
|
5924
6097
|
// never triggers it). Follow mode re-sticks to the bottom even if the user
|
|
5925
6098
|
// had scrolled up: sending is an unambiguous "take me to the latest"
|
|
@@ -5935,8 +6108,8 @@ export const createAgentExperience = (
|
|
|
5935
6108
|
// next user send.
|
|
5936
6109
|
followFallbackActive = false;
|
|
5937
6110
|
currentTurnAnchored = true;
|
|
5938
|
-
// A send is an explicit "take me along": it re-arms
|
|
5939
|
-
//
|
|
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.
|
|
5940
6113
|
readerEngagedThisTurn = false;
|
|
5941
6114
|
anchoredMessageId = messageId;
|
|
5942
6115
|
scheduleAnchorToMessage(messageId);
|
|
@@ -5962,75 +6135,6 @@ export const createAgentExperience = (
|
|
|
5962
6135
|
scheduleAutoScroll(true);
|
|
5963
6136
|
};
|
|
5964
6137
|
|
|
5965
|
-
// Anchor-top positioning: park on the turn's FIRST UNREAD attention-worthy
|
|
5966
|
-
// block.
|
|
5967
|
-
//
|
|
5968
|
-
// The read boundary is the last user send — the reader wrote it, so it (and
|
|
5969
|
-
// everything above) is read; every attention-worthy block after it is not.
|
|
5970
|
-
// Tool calls and reasoning are chrome and never targeted, so they scroll
|
|
5971
|
-
// past underneath rather than parking the reader on progress output.
|
|
5972
|
-
//
|
|
5973
|
-
// The policy assumes the reader is NOT watching the stream, and picks the
|
|
5974
|
-
// best place for them to land when they look back: the start of what they
|
|
5975
|
-
// haven't seen. `readerEngagedThisTurn` is the escape hatch — once someone
|
|
5976
|
-
// is demonstrably driving, their position wins and this stands down until
|
|
5977
|
-
// the next send.
|
|
5978
|
-
//
|
|
5979
|
-
// Anchoring to the first unread (not the newest) block means this fires at
|
|
5980
|
-
// most once per turn in practice, and later blocks stream in below the
|
|
5981
|
-
// anchor exactly as they do today.
|
|
5982
|
-
|
|
5983
|
-
// Minimum readable slice of the first unread block that must fit under the
|
|
5984
|
-
// pinned user message for the anchor to stay put. Below this, the block is
|
|
5985
|
-
// "stranded" and the anchor hands off to it.
|
|
5986
|
-
const ANCHOR_HANDOFF_MIN_VISIBLE = 96;
|
|
5987
|
-
|
|
5988
|
-
// Market pattern (ChatGPT/Claude/Gemini): the sent user message stays
|
|
5989
|
-
// pinned at the top as the visible header of the streaming reply. The
|
|
5990
|
-
// handoff to the first unread block exists for the stranded case — a long
|
|
5991
|
-
// user message or a tall tool-call prelude pushing the reply below the
|
|
5992
|
-
// fold — so it only fires when the block's start is not readably within
|
|
5993
|
-
// the viewport while the user message holds the anchor.
|
|
5994
|
-
const isFirstUnreadStranded = (messageId: string): boolean => {
|
|
5995
|
-
if (lastSentUserMessageId === null) return true;
|
|
5996
|
-
const userBubble = queryMessageBubble(lastSentUserMessageId);
|
|
5997
|
-
const blockBubble = queryScrollAnchor(messageId);
|
|
5998
|
-
// Unmeasurable geometry: fall back to handing off (pre-conditional
|
|
5999
|
-
// behavior) rather than risking an answer parked below the fold.
|
|
6000
|
-
if (!userBubble || !blockBubble) return true;
|
|
6001
|
-
const delta =
|
|
6002
|
-
offsetTopWithinBody(blockBubble) - offsetTopWithinBody(userBubble);
|
|
6003
|
-
return (
|
|
6004
|
-
getAnchorTopOffset() + delta >
|
|
6005
|
-
body.clientHeight - ANCHOR_HANDOFF_MIN_VISIBLE
|
|
6006
|
-
);
|
|
6007
|
-
};
|
|
6008
|
-
|
|
6009
|
-
const maybeAnchorToFirstUnread = (messages: AgentWidgetMessage[]) => {
|
|
6010
|
-
if (getScrollMode() !== "anchor-top") return;
|
|
6011
|
-
if (!currentTurnAnchored) return;
|
|
6012
|
-
if (readerEngagedThisTurn) return;
|
|
6013
|
-
if (hasActiveTranscriptSelection()) return;
|
|
6014
|
-
|
|
6015
|
-
let pastReadBoundary = lastSentUserMessageId === null;
|
|
6016
|
-
for (const message of messages) {
|
|
6017
|
-
if (!pastReadBoundary) {
|
|
6018
|
-
if (message.id === lastSentUserMessageId) pastReadBoundary = true;
|
|
6019
|
-
continue;
|
|
6020
|
-
}
|
|
6021
|
-
if (!isAttentionWorthyMessage(message)) continue;
|
|
6022
|
-
// Already parked on it: hold still while it streams.
|
|
6023
|
-
if (message.id === anchoredMessageId) return;
|
|
6024
|
-
// Block starts within view under the pinned user message: keep the
|
|
6025
|
-
// user message anchored. Re-evaluated every render, so a tool prelude
|
|
6026
|
-
// that later pushes the block below the fold still hands off.
|
|
6027
|
-
if (!isFirstUnreadStranded(message.id)) return;
|
|
6028
|
-
anchoredMessageId = message.id;
|
|
6029
|
-
scheduleAnchorToMessage(message.id);
|
|
6030
|
-
return;
|
|
6031
|
-
}
|
|
6032
|
-
};
|
|
6033
|
-
|
|
6034
6138
|
const trackMessages = (messages: AgentWidgetMessage[]) => {
|
|
6035
6139
|
const nextState = new Map<
|
|
6036
6140
|
string,
|
|
@@ -6392,8 +6496,10 @@ export const createAgentExperience = (
|
|
|
6392
6496
|
? { toolCallId: linkedTool.id, status: linkedTool.status === "running"
|
|
6393
6497
|
? "running" : linkedTool.success === false || linkedTool.error ? "failed" : "completed" }
|
|
6394
6498
|
: undefined;
|
|
6499
|
+
const userCollapse = resolveUserCollapse(message);
|
|
6395
6500
|
const fingerprint =
|
|
6396
6501
|
computeMessageFingerprint(message, configVersion) + askMeta + `:${eligibilityKey}` +
|
|
6502
|
+
(userCollapse ? `:uc-${userCollapse.state}` : "") +
|
|
6397
6503
|
(approvalWithPlugin ? `:${message.approval?.toolCallId ?? ""}:${JSON.stringify(execution)}` : "");
|
|
6398
6504
|
const cachedWrapper = (askWithPlugin || approvalWithPlugin || hasDirectiveBubble ||
|
|
6399
6505
|
(message.dispatchError && config.errorDisplay === "block" && !approvalUi))
|
|
@@ -6442,7 +6548,8 @@ export const createAgentExperience = (
|
|
|
6442
6548
|
// Keep the detail readable even while the secondary UI chunk loads.
|
|
6443
6549
|
const bubble = createStandardBubble(message, transform, config.layout?.messages,
|
|
6444
6550
|
config.messageActions, messageActionCallbacks, {
|
|
6445
|
-
loadingIndicatorRenderer: inlineLoadingRenderer, widgetConfig: config, actionEligibility
|
|
6551
|
+
loadingIndicatorRenderer: inlineLoadingRenderer, widgetConfig: config, actionEligibility,
|
|
6552
|
+
userCollapse
|
|
6446
6553
|
});
|
|
6447
6554
|
bubble.dataset.personaDefaultMessage = "true";
|
|
6448
6555
|
return bubble;
|
|
@@ -7227,7 +7334,12 @@ export const createAgentExperience = (
|
|
|
7227
7334
|
wrapper.replaceChildren(bubble);
|
|
7228
7335
|
wrapper.setAttribute("data-bubble-fp", fingerprint);
|
|
7229
7336
|
lastApprovalBubbleFingerprint.set(messageId, fingerprint);
|
|
7230
|
-
|
|
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
|
+
}
|
|
7231
7343
|
}
|
|
7232
7344
|
}
|
|
7233
7345
|
|
|
@@ -7255,6 +7367,8 @@ export const createAgentExperience = (
|
|
|
7255
7367
|
) => {
|
|
7256
7368
|
if (messages.length > 0) warmClientStream();
|
|
7257
7369
|
renderMessagesWithPluginsImpl(container, messages, transform);
|
|
7370
|
+
pruneUserMessageCollapse(messages);
|
|
7371
|
+
syncUserMessageCollapse();
|
|
7258
7372
|
refreshReadAloudButtons();
|
|
7259
7373
|
if (revealedToolCalls.size > 0) {
|
|
7260
7374
|
for (const [id, timer] of revealedToolCalls) {
|
|
@@ -8705,7 +8819,6 @@ export const createAgentExperience = (
|
|
|
8705
8819
|
handleAssistantTurnStarted();
|
|
8706
8820
|
}
|
|
8707
8821
|
if (lastAssistantMessage) lastHandledAssistantId = lastAssistantMessage.id;
|
|
8708
|
-
maybeAnchorToFirstUnread(messages);
|
|
8709
8822
|
|
|
8710
8823
|
const prevLastUserMessageId = voiceState.lastUserMessageId;
|
|
8711
8824
|
if (lastUserMessage && lastUserMessage.id !== prevLastUserMessageId) {
|
|
@@ -11170,6 +11283,8 @@ export const createAgentExperience = (
|
|
|
11170
11283
|
// (viewport resize) so the pin survives all of them.
|
|
11171
11284
|
if (typeof ResizeObserver !== "undefined") {
|
|
11172
11285
|
const contentResizeObserver = new ResizeObserver(() => {
|
|
11286
|
+
// Width changes rewrap text and late images grow attachments.
|
|
11287
|
+
syncUserMessageCollapse();
|
|
11173
11288
|
handleContentResize();
|
|
11174
11289
|
syncTranscriptEdgeFades();
|
|
11175
11290
|
// A resize can start or stop a table overflowing without a render or a
|
|
@@ -11215,7 +11330,7 @@ export const createAgentExperience = (
|
|
|
11215
11330
|
]);
|
|
11216
11331
|
const handleTranscriptKeydown = (event: KeyboardEvent) => {
|
|
11217
11332
|
if (!NAV_KEYS.has(event.key)) return;
|
|
11218
|
-
// Reader intent for
|
|
11333
|
+
// Reader intent for anchor re-pins is tracked unconditionally;
|
|
11219
11334
|
// only the follow-mode *pause* below stays behind the opt-in flag.
|
|
11220
11335
|
markReaderEngaged();
|
|
11221
11336
|
if (!isPauseOnInteractionEnabled()) return;
|
|
@@ -11269,10 +11384,13 @@ export const createAgentExperience = (
|
|
|
11269
11384
|
});
|
|
11270
11385
|
destroyCallbacks.push(() => body.removeEventListener("wheel", handleWheel));
|
|
11271
11386
|
scrollToBottomButton.addEventListener("click", () => {
|
|
11387
|
+
const targets = pendingApprovalBubbles().filter(isBelowFold)
|
|
11388
|
+
.map((bubble) => bubble.getAttribute("data-message-id")!);
|
|
11272
11389
|
// Jumping to the latest abandons the current anchor: drop the spacer
|
|
11273
11390
|
// first so "bottom" means the real end of content, not spacer padding
|
|
11274
11391
|
// that would keep shrinking underneath the reader.
|
|
11275
11392
|
resetAnchorState();
|
|
11393
|
+
approvalJumpTargets = targets;
|
|
11276
11394
|
cancelAutoScroll();
|
|
11277
11395
|
autoFollow.pause();
|
|
11278
11396
|
latestScroll.start(config);
|
|
@@ -11810,6 +11928,10 @@ export const createAgentExperience = (
|
|
|
11810
11928
|
const previousShowToolCalls = config.features?.showToolCalls;
|
|
11811
11929
|
const previousToolCallDisplay = config.features?.toolCallDisplay;
|
|
11812
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();
|
|
11813
11935
|
const previousStreamAnimationType = config.features?.streamAnimation?.type;
|
|
11814
11936
|
// History provider identity: enablement, eligibility, and advertised
|
|
11815
11937
|
// scopes are all resolved at construction, so a change rebuilds it.
|
|
@@ -12067,7 +12189,8 @@ export const createAgentExperience = (
|
|
|
12067
12189
|
const featuresChanged = (config.features?.showReasoning ?? true) !== (previousShowReasoning ?? true)
|
|
12068
12190
|
|| (config.features?.showToolCalls ?? true) !== (previousShowToolCalls ?? true)
|
|
12069
12191
|
|| JSON.stringify(config.features?.toolCallDisplay) !== JSON.stringify(previousToolCallDisplay)
|
|
12070
|
-
|| JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay)
|
|
12192
|
+
|| JSON.stringify(config.features?.reasoningDisplay) !== JSON.stringify(previousReasoningDisplay)
|
|
12193
|
+
|| userCollapseConfigKey() !== previousUserCollapseConfig;
|
|
12071
12194
|
const messagesConfigChanged = (config.future?.v5Defaults === true) !== previousDefaultsVersion
|
|
12072
12195
|
|| "toolCall" in nextConfig || messageActionsChanged || layoutMessagesChanged
|
|
12073
12196
|
|| "loadingIndicator" in nextConfig || iterationDisplayChanged || featuresChanged
|
|
@@ -72,7 +72,7 @@ it("keeps a manual close/open choice through streaming and completion", async ()
|
|
|
72
72
|
|
|
73
73
|
it.each([
|
|
74
74
|
["instant", false], ["height", false], ["instant", true], ["height", true],
|
|
75
|
-
] as const)("
|
|
75
|
+
] as const)("keeps a long user prompt anchored through %s collapse (reader scrolled: %s)", async (animation, readerScrolled) => {
|
|
76
76
|
// Vitest's global performance and JSDOM frame timestamps can have different
|
|
77
77
|
// origins. Match the browser's shared clock so the 220ms ease stays bounded.
|
|
78
78
|
const requestFrame = globalThis.requestAnimationFrame;
|
|
@@ -83,9 +83,9 @@ it.each([
|
|
|
83
83
|
scrollHeight: { configurable: true, get: () => 3000 },
|
|
84
84
|
clientHeight: { configurable: true, get: () => 400 },
|
|
85
85
|
});
|
|
86
|
-
// A long user prompt
|
|
87
|
-
//
|
|
88
|
-
//
|
|
86
|
+
// A long user prompt puts the first progress block below the viewport. The
|
|
87
|
+
// prompt keeps the anchor (100 - 16): the disclosure streaming in and
|
|
88
|
+
// folding away below the fold never moves the reader to its header.
|
|
89
89
|
vi.spyOn(HTMLElement.prototype, "offsetTop", "get").mockImplementation(function (this: HTMLElement) {
|
|
90
90
|
if (this.hasAttribute("data-persona-turn-activity")) return 600;
|
|
91
91
|
if (this.closest("[data-persona-turn-activity]")) return 644;
|
|
@@ -93,7 +93,7 @@ it.each([
|
|
|
93
93
|
return 0;
|
|
94
94
|
});
|
|
95
95
|
await start(); await text("p", "Investigating the issue.");
|
|
96
|
-
await vi.waitFor(() => expect(body.scrollTop).toBe(
|
|
96
|
+
await vi.waitFor(() => expect(body.scrollTop).toBe(84));
|
|
97
97
|
if (readerScrolled) {
|
|
98
98
|
body.dispatchEvent(new WheelEvent("wheel", { deltaY: 120, bubbles: true }));
|
|
99
99
|
body.scrollTop = 800;
|
|
@@ -101,9 +101,9 @@ it.each([
|
|
|
101
101
|
await text("answer", "The answer stays below the header.", "final");
|
|
102
102
|
push("execution_complete", { kind: "agent", success: true }); close();
|
|
103
103
|
await vi.waitFor(() => expect(header().getAttribute("aria-expanded")).toBe("false"));
|
|
104
|
-
expect(body.scrollTop).toBe(readerScrolled ? 800 :
|
|
104
|
+
expect(body.scrollTop).toBe(readerScrolled ? 800 : 84);
|
|
105
105
|
|
|
106
|
-
// Later render/resize passes must not pull a reader back to the
|
|
106
|
+
// Later render/resize passes must not pull a reader back to the prompt.
|
|
107
107
|
body.dispatchEvent(new WheelEvent("wheel", { deltaY: 120, bubbles: true }));
|
|
108
108
|
body.scrollTop = 800;
|
|
109
109
|
controller.update({ expansion: { animation, duration: 120 } });
|