@runtypelabs/persona 3.6.0 → 3.8.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/index.cjs +40 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -4
- package/dist/index.d.ts +73 -4
- package/dist/index.global.js +69 -69
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +704 -243
- package/dist/theme-editor.d.cts +75 -5
- package/dist/theme-editor.d.ts +75 -5
- package/dist/theme-editor.js +703 -243
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +53 -0
- package/dist/theme-reference.d.ts +53 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +44 -0
- package/package.json +1 -1
- package/src/components/artifact-card.ts +1 -1
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +142 -0
- package/src/components/event-stream-view.ts +67 -28
- package/src/components/header-builder.ts +3 -0
- package/src/components/launcher.ts +7 -2
- package/src/components/panel.ts +3 -1
- package/src/defaults.ts +15 -0
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +2 -1
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +44 -0
- package/src/theme-editor/index.ts +1 -0
- package/src/theme-editor/role-mappings.ts +12 -0
- package/src/theme-editor/sections.test.ts +43 -0
- package/src/theme-editor/sections.ts +42 -0
- package/src/theme-reference.ts +8 -0
- package/src/types/theme.ts +45 -0
- package/src/types.ts +31 -4
- package/src/ui.overlay-z-index.test.ts +34 -2
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +264 -90
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/constants.ts +13 -0
- package/src/utils/dropdown.ts +2 -1
- package/src/utils/overlay-host-stacking.test.ts +61 -0
- package/src/utils/overlay-host-stacking.ts +38 -0
- package/src/utils/scroll-lock.test.ts +64 -0
- package/src/utils/scroll-lock.ts +62 -0
- package/src/utils/theme.test.ts +34 -0
- package/src/utils/tokens.ts +112 -0
package/src/ui.ts
CHANGED
|
@@ -33,7 +33,16 @@ import { renderLucideIcon } from "./utils/icons";
|
|
|
33
33
|
import { createElement, createElementInDocument } from "./utils/dom";
|
|
34
34
|
import { morphMessages } from "./utils/morph";
|
|
35
35
|
import { computeMessageFingerprint, createMessageCache, getCachedWrapper, setCachedWrapper, pruneCache } from "./utils/message-fingerprint";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
createFollowStateController,
|
|
38
|
+
getScrollBottomOffset,
|
|
39
|
+
isElementNearBottom,
|
|
40
|
+
resolveFollowStateFromScroll,
|
|
41
|
+
resolveFollowStateFromWheel
|
|
42
|
+
} from "./utils/auto-follow";
|
|
43
|
+
import { statusCopy, DEFAULT_OVERLAY_Z_INDEX, PORTALED_OVERLAY_Z_INDEX } from "./utils/constants";
|
|
44
|
+
import { syncOverlayHostStacking } from "./utils/overlay-host-stacking";
|
|
45
|
+
import { acquireScrollLock } from "./utils/scroll-lock";
|
|
37
46
|
import { isDockedMountMode, resolveDockConfig } from "./utils/dock";
|
|
38
47
|
import { createLauncherButton } from "./components/launcher";
|
|
39
48
|
import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
|
|
@@ -545,6 +554,7 @@ export const createAgentExperience = (
|
|
|
545
554
|
let showReasoning = config.features?.showReasoning ?? true;
|
|
546
555
|
let showToolCalls = config.features?.showToolCalls ?? true;
|
|
547
556
|
let showEventStreamToggle = config.features?.showEventStreamToggle ?? false;
|
|
557
|
+
let scrollToBottomFeature = config.features?.scrollToBottom ?? {};
|
|
548
558
|
const persistKeyPrefix = (typeof config.persistState === 'object' ? config.persistState?.keyPrefix : undefined) ?? "persona-";
|
|
549
559
|
const eventStreamDbName = `${persistKeyPrefix}event-stream`;
|
|
550
560
|
let eventStreamStore = showEventStreamToggle ? new EventStreamStore(eventStreamDbName) : null;
|
|
@@ -655,6 +665,49 @@ export const createAgentExperience = (
|
|
|
655
665
|
let attachmentButtonWrapper: HTMLElement | null = panelElements.attachmentButtonWrapper;
|
|
656
666
|
let attachmentInput: HTMLInputElement | null = panelElements.attachmentInput;
|
|
657
667
|
let attachmentPreviewsContainer: HTMLElement | null = panelElements.attachmentPreviewsContainer;
|
|
668
|
+
container.classList.add("persona-relative");
|
|
669
|
+
body.classList.add("persona-relative");
|
|
670
|
+
const SCROLL_TO_BOTTOM_EDGE_OFFSET = 12;
|
|
671
|
+
|
|
672
|
+
const getScrollToBottomLabel = () => scrollToBottomFeature.label ?? "";
|
|
673
|
+
const getScrollToBottomIconName = () => scrollToBottomFeature.iconName ?? "arrow-down";
|
|
674
|
+
const isScrollToBottomEnabled = () => scrollToBottomFeature.enabled !== false;
|
|
675
|
+
const scrollToBottomButton = createElement(
|
|
676
|
+
"button",
|
|
677
|
+
"persona-scroll-to-bottom-indicator persona-absolute persona-bottom-3 persona-left-1/2 persona-z-10 persona-flex persona-items-center persona-gap-1 persona-text-xs persona-transform persona--translate-x-1/2 persona-cursor-pointer"
|
|
678
|
+
) as HTMLButtonElement;
|
|
679
|
+
scrollToBottomButton.type = "button";
|
|
680
|
+
scrollToBottomButton.style.display = "none";
|
|
681
|
+
scrollToBottomButton.setAttribute("data-persona-scroll-to-bottom", "true");
|
|
682
|
+
const scrollToBottomIcon = createElement("span", "persona-flex persona-items-center");
|
|
683
|
+
const scrollToBottomLabel = createElement("span", "");
|
|
684
|
+
scrollToBottomButton.append(scrollToBottomIcon, scrollToBottomLabel);
|
|
685
|
+
container.appendChild(scrollToBottomButton);
|
|
686
|
+
|
|
687
|
+
const updateScrollToBottomButtonOffset = () => {
|
|
688
|
+
const footerHidden = footer.style.display === "none";
|
|
689
|
+
const footerHeight = footerHidden ? 0 : footer.offsetHeight;
|
|
690
|
+
scrollToBottomButton.style.bottom = `${footerHeight + SCROLL_TO_BOTTOM_EDGE_OFFSET}px`;
|
|
691
|
+
};
|
|
692
|
+
updateScrollToBottomButtonOffset();
|
|
693
|
+
|
|
694
|
+
const renderScrollToBottomButton = () => {
|
|
695
|
+
const hasLabel = Boolean(getScrollToBottomLabel());
|
|
696
|
+
scrollToBottomButton.setAttribute("aria-label", getScrollToBottomLabel() || "Jump to latest");
|
|
697
|
+
scrollToBottomButton.title = getScrollToBottomLabel();
|
|
698
|
+
scrollToBottomButton.setAttribute("data-persona-scroll-to-bottom-has-label", hasLabel ? "true" : "false");
|
|
699
|
+
scrollToBottomIcon.innerHTML = "";
|
|
700
|
+
const icon = renderLucideIcon(getScrollToBottomIconName(), "14px", "currentColor", 2);
|
|
701
|
+
if (icon) {
|
|
702
|
+
scrollToBottomIcon.appendChild(icon);
|
|
703
|
+
scrollToBottomIcon.style.display = "";
|
|
704
|
+
} else {
|
|
705
|
+
scrollToBottomIcon.style.display = "none";
|
|
706
|
+
}
|
|
707
|
+
scrollToBottomLabel.textContent = getScrollToBottomLabel();
|
|
708
|
+
scrollToBottomLabel.style.display = hasLabel ? "" : "none";
|
|
709
|
+
};
|
|
710
|
+
renderScrollToBottomButton();
|
|
658
711
|
|
|
659
712
|
// Initialized after composer plugins rebind footer DOM (see `bindComposerRefsFromFooter`)
|
|
660
713
|
let attachmentManager: AttachmentManager | null = null;
|
|
@@ -719,6 +772,7 @@ export const createAgentExperience = (
|
|
|
719
772
|
};
|
|
720
773
|
eventStreamLastUpdate = 0;
|
|
721
774
|
eventStreamRAF = requestAnimationFrame(rafLoop);
|
|
775
|
+
syncScrollToBottomButton();
|
|
722
776
|
eventBus.emit("eventStream:opened", { timestamp: Date.now() });
|
|
723
777
|
};
|
|
724
778
|
|
|
@@ -739,6 +793,7 @@ export const createAgentExperience = (
|
|
|
739
793
|
cancelAnimationFrame(eventStreamRAF);
|
|
740
794
|
eventStreamRAF = null;
|
|
741
795
|
}
|
|
796
|
+
syncScrollToBottomButton();
|
|
742
797
|
eventBus.emit("eventStream:closed", { timestamp: Date.now() });
|
|
743
798
|
};
|
|
744
799
|
|
|
@@ -1493,7 +1548,7 @@ export const createAgentExperience = (
|
|
|
1493
1548
|
// Determine panel styling based on mode, with theme overrides
|
|
1494
1549
|
const position = config.launcher?.position ?? 'bottom-left';
|
|
1495
1550
|
const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
|
|
1496
|
-
const overlayZIndex = config.launcher?.zIndex ??
|
|
1551
|
+
const overlayZIndex = config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX;
|
|
1497
1552
|
|
|
1498
1553
|
// Default values based on mode
|
|
1499
1554
|
let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
|
|
@@ -1766,9 +1821,8 @@ export const createAgentExperience = (
|
|
|
1766
1821
|
if (!isInlineEmbed && !dockedMode) {
|
|
1767
1822
|
const maxHeightStyles = 'max-height: -moz-available !important; max-height: stretch !important;';
|
|
1768
1823
|
const paddingStyles = sidebarMode ? '' : 'padding-top: 1.25em !important;';
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
? `z-index: ${config.launcher.zIndex} !important;`
|
|
1824
|
+
const zIndexStyles = !sidebarMode
|
|
1825
|
+
? `z-index: ${config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX} !important;`
|
|
1772
1826
|
: '';
|
|
1773
1827
|
wrapper.style.cssText += maxHeightStyles + paddingStyles + zIndexStyles;
|
|
1774
1828
|
}
|
|
@@ -1781,6 +1835,16 @@ export const createAgentExperience = (
|
|
|
1781
1835
|
|
|
1782
1836
|
const destroyCallbacks: Array<() => void> = [];
|
|
1783
1837
|
|
|
1838
|
+
let teardownHostStacking: (() => void) | null = null;
|
|
1839
|
+
let releaseScrollLock: (() => void) | null = null;
|
|
1840
|
+
|
|
1841
|
+
destroyCallbacks.push(() => {
|
|
1842
|
+
teardownHostStacking?.();
|
|
1843
|
+
teardownHostStacking = null;
|
|
1844
|
+
releaseScrollLock?.();
|
|
1845
|
+
releaseScrollLock = null;
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1784
1848
|
if (artifactPanelResizeObs) {
|
|
1785
1849
|
destroyCallbacks.push(() => {
|
|
1786
1850
|
artifactPanelResizeObs?.disconnect();
|
|
@@ -1845,18 +1909,13 @@ export const createAgentExperience = (
|
|
|
1845
1909
|
let isStreaming = false;
|
|
1846
1910
|
const messageCache = createMessageCache();
|
|
1847
1911
|
let configVersion = 0;
|
|
1848
|
-
|
|
1912
|
+
const autoFollow = createFollowStateController();
|
|
1849
1913
|
let lastScrollTop = 0;
|
|
1850
|
-
let lastAutoScrollTime = 0;
|
|
1851
1914
|
let scrollRAF: number | null = null;
|
|
1852
|
-
let isAutoScrollBlocked = false;
|
|
1853
|
-
let blockUntilTime = 0;
|
|
1854
1915
|
let isAutoScrolling = false;
|
|
1855
1916
|
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
1858
|
-
const USER_SCROLL_THRESHOLD = 5;
|
|
1859
|
-
const BOTTOM_THRESHOLD = 50;
|
|
1917
|
+
const USER_SCROLL_THRESHOLD = 1;
|
|
1918
|
+
const BOTTOM_THRESHOLD = 8;
|
|
1860
1919
|
const messageState = new Map<
|
|
1861
1920
|
string,
|
|
1862
1921
|
{ streaming?: boolean; role: AgentWidgetMessage["role"] }
|
|
@@ -1946,75 +2005,92 @@ export const createAgentExperience = (
|
|
|
1946
2005
|
}
|
|
1947
2006
|
}
|
|
1948
2007
|
|
|
1949
|
-
|
|
1950
|
-
|
|
2008
|
+
// Track ongoing smooth scroll animation
|
|
2009
|
+
let smoothScrollRAF: number | null = null;
|
|
1951
2010
|
|
|
1952
|
-
|
|
2011
|
+
// Get the scrollable container using its unique ID
|
|
2012
|
+
const getScrollableContainer = (): HTMLElement => {
|
|
2013
|
+
// Use the unique ID for reliable selection
|
|
2014
|
+
const scrollable = wrapper.querySelector('#persona-scroll-container') as HTMLElement;
|
|
2015
|
+
// Fallback to body if ID not found (shouldn't happen, but safe fallback)
|
|
2016
|
+
return scrollable || body;
|
|
2017
|
+
};
|
|
1953
2018
|
|
|
1954
|
-
|
|
1955
|
-
|
|
2019
|
+
const cancelSmoothScroll = () => {
|
|
2020
|
+
if (smoothScrollRAF !== null) {
|
|
2021
|
+
cancelAnimationFrame(smoothScrollRAF);
|
|
2022
|
+
smoothScrollRAF = null;
|
|
1956
2023
|
}
|
|
2024
|
+
isAutoScrolling = false;
|
|
2025
|
+
};
|
|
1957
2026
|
|
|
1958
|
-
|
|
1959
|
-
|
|
2027
|
+
const cancelAutoScroll = () => {
|
|
2028
|
+
if (scrollRAF !== null) {
|
|
2029
|
+
cancelAnimationFrame(scrollRAF);
|
|
2030
|
+
scrollRAF = null;
|
|
1960
2031
|
}
|
|
2032
|
+
cancelSmoothScroll();
|
|
2033
|
+
};
|
|
1961
2034
|
|
|
1962
|
-
|
|
2035
|
+
const syncScrollToBottomButton = () => {
|
|
2036
|
+
if (!isScrollToBottomEnabled() || eventStreamVisible) {
|
|
2037
|
+
if (scrollToBottomButton.parentNode) {
|
|
2038
|
+
scrollToBottomButton.remove();
|
|
2039
|
+
}
|
|
2040
|
+
scrollToBottomButton.style.display = "none";
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (scrollToBottomButton.parentNode !== container) {
|
|
2044
|
+
container.appendChild(scrollToBottomButton);
|
|
2045
|
+
}
|
|
2046
|
+
updateScrollToBottomButtonOffset();
|
|
2047
|
+
const hasOverflow = getScrollBottomOffset(body) > 0;
|
|
2048
|
+
scrollToBottomButton.style.display = (autoFollow.isFollowing() || !hasOverflow) ? "none" : "";
|
|
2049
|
+
};
|
|
1963
2050
|
|
|
1964
|
-
|
|
1965
|
-
|
|
2051
|
+
const pauseAutoScroll = () => {
|
|
2052
|
+
if (!autoFollow.pause()) return;
|
|
2053
|
+
cancelAutoScroll();
|
|
2054
|
+
syncScrollToBottomButton();
|
|
2055
|
+
};
|
|
1966
2056
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2057
|
+
const resumeAutoScroll = () => {
|
|
2058
|
+
autoFollow.resume();
|
|
2059
|
+
syncScrollToBottomButton();
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
const scheduleAutoScroll = (force = false) => {
|
|
2063
|
+
if (!autoFollow.isFollowing()) return;
|
|
2064
|
+
|
|
2065
|
+
if (!force && !isStreaming) return;
|
|
2066
|
+
|
|
2067
|
+
cancelAutoScroll();
|
|
1970
2068
|
|
|
1971
2069
|
scrollRAF = requestAnimationFrame(() => {
|
|
1972
|
-
if (isAutoScrollBlocked || !shouldAutoScroll) return;
|
|
1973
|
-
isAutoScrolling = true;
|
|
1974
|
-
body.scrollTop = body.scrollHeight;
|
|
1975
|
-
lastScrollTop = body.scrollTop;
|
|
1976
|
-
requestAnimationFrame(() => {
|
|
1977
|
-
isAutoScrolling = false;
|
|
1978
|
-
});
|
|
1979
2070
|
scrollRAF = null;
|
|
2071
|
+
if (!autoFollow.isFollowing()) return;
|
|
2072
|
+
smoothScrollToBottom(getScrollableContainer(), force ? 220 : 140);
|
|
1980
2073
|
});
|
|
1981
2074
|
};
|
|
1982
2075
|
|
|
1983
|
-
// Track ongoing smooth scroll animation
|
|
1984
|
-
let smoothScrollRAF: number | null = null;
|
|
1985
|
-
|
|
1986
|
-
// Get the scrollable container using its unique ID
|
|
1987
|
-
const getScrollableContainer = (): HTMLElement => {
|
|
1988
|
-
// Use the unique ID for reliable selection
|
|
1989
|
-
const scrollable = wrapper.querySelector('#persona-scroll-container') as HTMLElement;
|
|
1990
|
-
// Fallback to body if ID not found (shouldn't happen, but safe fallback)
|
|
1991
|
-
return scrollable || body;
|
|
1992
|
-
};
|
|
1993
|
-
|
|
1994
2076
|
// Custom smooth scroll animation with easing
|
|
1995
2077
|
const smoothScrollToBottom = (element: HTMLElement, duration = 500) => {
|
|
1996
2078
|
const start = element.scrollTop;
|
|
1997
|
-
const clientHeight = element.clientHeight;
|
|
1998
2079
|
// Recalculate target dynamically to handle layout changes
|
|
1999
|
-
let target = element
|
|
2080
|
+
let target = getScrollBottomOffset(element);
|
|
2000
2081
|
let distance = target - start;
|
|
2001
2082
|
|
|
2002
|
-
// Check if already at bottom: scrollTop + clientHeight should be >= scrollHeight
|
|
2003
|
-
// Add a small threshold (2px) to account for rounding/subpixel differences
|
|
2004
|
-
const isAtBottom = start + clientHeight >= target - 2;
|
|
2005
|
-
|
|
2006
2083
|
// If already at bottom or very close, skip animation to prevent glitch
|
|
2007
|
-
if (
|
|
2084
|
+
if (Math.abs(distance) < 1) {
|
|
2085
|
+
lastScrollTop = element.scrollTop;
|
|
2008
2086
|
return;
|
|
2009
2087
|
}
|
|
2010
2088
|
|
|
2011
2089
|
// Cancel any ongoing smooth scroll animation
|
|
2012
|
-
|
|
2013
|
-
cancelAnimationFrame(smoothScrollRAF);
|
|
2014
|
-
smoothScrollRAF = null;
|
|
2015
|
-
}
|
|
2090
|
+
cancelSmoothScroll();
|
|
2016
2091
|
|
|
2017
2092
|
const startTime = performance.now();
|
|
2093
|
+
isAutoScrolling = true;
|
|
2018
2094
|
|
|
2019
2095
|
// Easing function: ease-out cubic for smooth deceleration
|
|
2020
2096
|
const easeOutCubic = (t: number): number => {
|
|
@@ -2022,8 +2098,13 @@ export const createAgentExperience = (
|
|
|
2022
2098
|
};
|
|
2023
2099
|
|
|
2024
2100
|
const animate = (currentTime: number) => {
|
|
2101
|
+
if (!autoFollow.isFollowing()) {
|
|
2102
|
+
cancelSmoothScroll();
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2025
2106
|
// Recalculate target each frame in case scrollHeight changed
|
|
2026
|
-
const currentTarget = element
|
|
2107
|
+
const currentTarget = getScrollBottomOffset(element);
|
|
2027
2108
|
if (currentTarget !== target) {
|
|
2028
2109
|
target = currentTarget;
|
|
2029
2110
|
distance = target - start;
|
|
@@ -2035,13 +2116,16 @@ export const createAgentExperience = (
|
|
|
2035
2116
|
|
|
2036
2117
|
const currentScroll = start + distance * eased;
|
|
2037
2118
|
element.scrollTop = currentScroll;
|
|
2119
|
+
lastScrollTop = element.scrollTop;
|
|
2038
2120
|
|
|
2039
2121
|
if (progress < 1) {
|
|
2040
2122
|
smoothScrollRAF = requestAnimationFrame(animate);
|
|
2041
2123
|
} else {
|
|
2042
2124
|
// Ensure we end exactly at the target
|
|
2043
|
-
element.scrollTop =
|
|
2125
|
+
element.scrollTop = target;
|
|
2126
|
+
lastScrollTop = element.scrollTop;
|
|
2044
2127
|
smoothScrollRAF = null;
|
|
2128
|
+
isAutoScrolling = false;
|
|
2045
2129
|
}
|
|
2046
2130
|
};
|
|
2047
2131
|
|
|
@@ -2481,20 +2565,6 @@ export const createAgentExperience = (
|
|
|
2481
2565
|
|
|
2482
2566
|
// Use idiomorph to morph the container contents
|
|
2483
2567
|
morphMessages(container, tempContainer);
|
|
2484
|
-
// Defer scroll to next frame for smoother animation and to prevent jolt
|
|
2485
|
-
// This allows the browser to update layout (e.g., typing indicator removal) before scrolling
|
|
2486
|
-
// Use double RAF to ensure layout has fully settled before starting scroll animation
|
|
2487
|
-
// Get the scrollable container using its unique ID (#persona-scroll-container)
|
|
2488
|
-
// Only smooth-scroll if auto-scroll hasn't been blocked by the user scrolling up
|
|
2489
|
-
if (shouldAutoScroll && !isAutoScrollBlocked) {
|
|
2490
|
-
requestAnimationFrame(() => {
|
|
2491
|
-
requestAnimationFrame(() => {
|
|
2492
|
-
if (!shouldAutoScroll || isAutoScrollBlocked) return;
|
|
2493
|
-
const scrollableContainer = getScrollableContainer();
|
|
2494
|
-
smoothScrollToBottom(scrollableContainer);
|
|
2495
|
-
});
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
2568
|
};
|
|
2499
2569
|
|
|
2500
2570
|
// Alias for clarity - the implementation handles flicker prevention via typing indicator logic
|
|
@@ -2562,12 +2632,46 @@ export const createAgentExperience = (
|
|
|
2562
2632
|
const prevOpen = open;
|
|
2563
2633
|
open = nextOpen;
|
|
2564
2634
|
updateOpenState();
|
|
2565
|
-
|
|
2635
|
+
|
|
2636
|
+
// Sync host stacking and scroll lock for viewport-covering modes
|
|
2637
|
+
const isViewportCovering = (() => {
|
|
2638
|
+
const sm = config.launcher?.sidebarMode ?? false;
|
|
2639
|
+
const ow = mount.ownerDocument.defaultView ?? window;
|
|
2640
|
+
const mf = config.launcher?.mobileFullscreen ?? true;
|
|
2641
|
+
const mb = config.launcher?.mobileBreakpoint ?? 640;
|
|
2642
|
+
const isMobile = ow.innerWidth <= mb;
|
|
2643
|
+
const dockedMF = isDockedMountMode(config) && mf && isMobile;
|
|
2644
|
+
return sm || (mf && isMobile && launcherEnabled) || dockedMF;
|
|
2645
|
+
})();
|
|
2646
|
+
|
|
2647
|
+
if (open && isViewportCovering) {
|
|
2648
|
+
if (!teardownHostStacking) {
|
|
2649
|
+
const root = mount.getRootNode();
|
|
2650
|
+
const hostEl = root instanceof ShadowRoot
|
|
2651
|
+
? (root.host as HTMLElement)
|
|
2652
|
+
: mount.closest<HTMLElement>(".persona-host");
|
|
2653
|
+
if (hostEl) {
|
|
2654
|
+
teardownHostStacking = syncOverlayHostStacking(
|
|
2655
|
+
hostEl,
|
|
2656
|
+
config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
if (!releaseScrollLock) {
|
|
2661
|
+
releaseScrollLock = acquireScrollLock(mount.ownerDocument);
|
|
2662
|
+
}
|
|
2663
|
+
} else if (!open) {
|
|
2664
|
+
teardownHostStacking?.();
|
|
2665
|
+
teardownHostStacking = null;
|
|
2666
|
+
releaseScrollLock?.();
|
|
2667
|
+
releaseScrollLock = null;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2566
2670
|
if (open) {
|
|
2567
2671
|
recalcPanelHeight();
|
|
2568
2672
|
scheduleAutoScroll(true);
|
|
2569
2673
|
}
|
|
2570
|
-
|
|
2674
|
+
|
|
2571
2675
|
// Emit widget state events
|
|
2572
2676
|
const stateEvent: AgentWidgetStateEvent = {
|
|
2573
2677
|
open,
|
|
@@ -3512,7 +3616,37 @@ export const createAgentExperience = (
|
|
|
3512
3616
|
} finally {
|
|
3513
3617
|
// applyFullHeightStyles() assigns wrapper.style.cssText (e.g. display:flex !important), which
|
|
3514
3618
|
// overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
|
|
3619
|
+
updateScrollToBottomButtonOffset();
|
|
3515
3620
|
updateOpenState();
|
|
3621
|
+
|
|
3622
|
+
// Sync scroll lock and host stacking when viewport mode changes (e.g. orientation change)
|
|
3623
|
+
if (open && launcherEnabled) {
|
|
3624
|
+
const ow = mount.ownerDocument.defaultView ?? window;
|
|
3625
|
+
const isMobile = ow.innerWidth <= (config.launcher?.mobileBreakpoint ?? 640);
|
|
3626
|
+
const sm = config.launcher?.sidebarMode ?? false;
|
|
3627
|
+
const mf = config.launcher?.mobileFullscreen ?? true;
|
|
3628
|
+
const dockedMF = isDockedMountMode(config) && mf && isMobile;
|
|
3629
|
+
const isVC = sm || (mf && isMobile && launcherEnabled) || dockedMF;
|
|
3630
|
+
|
|
3631
|
+
if (isVC && !releaseScrollLock) {
|
|
3632
|
+
const root = mount.getRootNode();
|
|
3633
|
+
const hostEl = root instanceof ShadowRoot
|
|
3634
|
+
? (root.host as HTMLElement)
|
|
3635
|
+
: mount.closest<HTMLElement>(".persona-host");
|
|
3636
|
+
if (hostEl && !teardownHostStacking) {
|
|
3637
|
+
teardownHostStacking = syncOverlayHostStacking(
|
|
3638
|
+
hostEl,
|
|
3639
|
+
config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
releaseScrollLock = acquireScrollLock(mount.ownerDocument);
|
|
3643
|
+
} else if (!isVC) {
|
|
3644
|
+
teardownHostStacking?.();
|
|
3645
|
+
teardownHostStacking = null;
|
|
3646
|
+
releaseScrollLock?.();
|
|
3647
|
+
releaseScrollLock = null;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3516
3650
|
}
|
|
3517
3651
|
};
|
|
3518
3652
|
|
|
@@ -3520,37 +3654,68 @@ export const createAgentExperience = (
|
|
|
3520
3654
|
const ownerWindow = mount.ownerDocument.defaultView ?? window;
|
|
3521
3655
|
ownerWindow.addEventListener("resize", recalcPanelHeight);
|
|
3522
3656
|
destroyCallbacks.push(() => ownerWindow.removeEventListener("resize", recalcPanelHeight));
|
|
3657
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
3658
|
+
const footerResizeObserver = new ResizeObserver(() => {
|
|
3659
|
+
updateScrollToBottomButtonOffset();
|
|
3660
|
+
});
|
|
3661
|
+
footerResizeObserver.observe(footer);
|
|
3662
|
+
destroyCallbacks.push(() => footerResizeObserver.disconnect());
|
|
3663
|
+
}
|
|
3523
3664
|
|
|
3524
3665
|
lastScrollTop = body.scrollTop;
|
|
3525
3666
|
|
|
3526
3667
|
const handleScroll = () => {
|
|
3527
3668
|
const scrollTop = body.scrollTop;
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3669
|
+
const { action, nextLastScrollTop } = resolveFollowStateFromScroll({
|
|
3670
|
+
following: autoFollow.isFollowing(),
|
|
3671
|
+
currentScrollTop: scrollTop,
|
|
3672
|
+
lastScrollTop,
|
|
3673
|
+
nearBottom: isElementNearBottom(body, BOTTOM_THRESHOLD),
|
|
3674
|
+
userScrollThreshold: USER_SCROLL_THRESHOLD,
|
|
3675
|
+
isAutoScrolling,
|
|
3676
|
+
pauseOnUpwardScroll: true,
|
|
3677
|
+
pauseWhenAwayFromBottom: false,
|
|
3678
|
+
resumeRequiresDownwardScroll: true
|
|
3679
|
+
});
|
|
3680
|
+
lastScrollTop = nextLastScrollTop;
|
|
3681
|
+
|
|
3682
|
+
if (action === "resume") {
|
|
3683
|
+
resumeAutoScroll();
|
|
3540
3684
|
return;
|
|
3541
3685
|
}
|
|
3542
3686
|
|
|
3543
|
-
if (
|
|
3544
|
-
|
|
3545
|
-
blockUntilTime = Date.now() + AUTO_SCROLL_BLOCK_TIME;
|
|
3546
|
-
shouldAutoScroll = false;
|
|
3687
|
+
if (action === "pause") {
|
|
3688
|
+
pauseAutoScroll();
|
|
3547
3689
|
}
|
|
3548
3690
|
};
|
|
3549
3691
|
|
|
3550
3692
|
body.addEventListener("scroll", handleScroll, { passive: true });
|
|
3551
3693
|
destroyCallbacks.push(() => body.removeEventListener("scroll", handleScroll));
|
|
3694
|
+
const handleWheel = (event: WheelEvent) => {
|
|
3695
|
+
const action = resolveFollowStateFromWheel({
|
|
3696
|
+
following: autoFollow.isFollowing(),
|
|
3697
|
+
deltaY: event.deltaY,
|
|
3698
|
+
nearBottom: isElementNearBottom(body, BOTTOM_THRESHOLD),
|
|
3699
|
+
resumeWhenNearBottom: true
|
|
3700
|
+
});
|
|
3701
|
+
|
|
3702
|
+
if (action === "pause") {
|
|
3703
|
+
pauseAutoScroll();
|
|
3704
|
+
} else if (action === "resume") {
|
|
3705
|
+
resumeAutoScroll();
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
body.addEventListener("wheel", handleWheel, { passive: true });
|
|
3709
|
+
destroyCallbacks.push(() => body.removeEventListener("wheel", handleWheel));
|
|
3710
|
+
scrollToBottomButton.addEventListener("click", () => {
|
|
3711
|
+
body.scrollTop = body.scrollHeight;
|
|
3712
|
+
lastScrollTop = body.scrollTop;
|
|
3713
|
+
resumeAutoScroll();
|
|
3714
|
+
scheduleAutoScroll(true);
|
|
3715
|
+
});
|
|
3716
|
+
destroyCallbacks.push(() => scrollToBottomButton.remove());
|
|
3552
3717
|
destroyCallbacks.push(() => {
|
|
3553
|
-
|
|
3718
|
+
cancelAutoScroll();
|
|
3554
3719
|
});
|
|
3555
3720
|
|
|
3556
3721
|
const refreshCloseButton = () => {
|
|
@@ -3581,6 +3746,7 @@ export const createAgentExperience = (
|
|
|
3581
3746
|
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
3582
3747
|
session.clearMessages();
|
|
3583
3748
|
messageCache.clear();
|
|
3749
|
+
resumeAutoScroll();
|
|
3584
3750
|
|
|
3585
3751
|
// Always clear the default localStorage key
|
|
3586
3752
|
try {
|
|
@@ -3697,6 +3863,9 @@ export const createAgentExperience = (
|
|
|
3697
3863
|
autoExpand = config.launcher?.autoExpand ?? false;
|
|
3698
3864
|
showReasoning = config.features?.showReasoning ?? true;
|
|
3699
3865
|
showToolCalls = config.features?.showToolCalls ?? true;
|
|
3866
|
+
scrollToBottomFeature = config.features?.scrollToBottom ?? {};
|
|
3867
|
+
renderScrollToBottomButton();
|
|
3868
|
+
syncScrollToBottomButton();
|
|
3700
3869
|
const prevShowEventStreamToggle = showEventStreamToggle;
|
|
3701
3870
|
showEventStreamToggle = config.features?.showEventStreamToggle ?? false;
|
|
3702
3871
|
|
|
@@ -3875,6 +4044,8 @@ export const createAgentExperience = (
|
|
|
3875
4044
|
if (footer) {
|
|
3876
4045
|
footer.style.display = showFooter ? "" : "none";
|
|
3877
4046
|
}
|
|
4047
|
+
updateScrollToBottomButtonOffset();
|
|
4048
|
+
syncScrollToBottomButton();
|
|
3878
4049
|
|
|
3879
4050
|
// Only update open state if launcher enabled state changed or autoExpand value changed
|
|
3880
4051
|
const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
|
|
@@ -4155,6 +4326,7 @@ export const createAgentExperience = (
|
|
|
4155
4326
|
|
|
4156
4327
|
// Position tooltip above button
|
|
4157
4328
|
portaledTooltip.style.position = "fixed";
|
|
4329
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
4158
4330
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
4159
4331
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
4160
4332
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -4369,6 +4541,7 @@ export const createAgentExperience = (
|
|
|
4369
4541
|
|
|
4370
4542
|
// Position tooltip above button
|
|
4371
4543
|
portaledTooltip.style.position = "fixed";
|
|
4544
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
4372
4545
|
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
4373
4546
|
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
4374
4547
|
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
@@ -4899,6 +5072,7 @@ export const createAgentExperience = (
|
|
|
4899
5072
|
artifactsPaneUserHidden = false;
|
|
4900
5073
|
session.clearMessages();
|
|
4901
5074
|
messageCache.clear();
|
|
5075
|
+
resumeAutoScroll();
|
|
4902
5076
|
|
|
4903
5077
|
// Always clear the default localStorage key
|
|
4904
5078
|
try {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createFollowStateController,
|
|
5
|
+
getScrollBottomOffset,
|
|
6
|
+
isElementNearBottom,
|
|
7
|
+
resolveFollowStateFromScroll,
|
|
8
|
+
resolveFollowStateFromWheel
|
|
9
|
+
} from "./auto-follow";
|
|
10
|
+
|
|
11
|
+
describe("auto-follow utilities", () => {
|
|
12
|
+
it("tracks pause and resume state", () => {
|
|
13
|
+
const state = createFollowStateController();
|
|
14
|
+
|
|
15
|
+
expect(state.isFollowing()).toBe(true);
|
|
16
|
+
expect(state.pause()).toBe(true);
|
|
17
|
+
expect(state.isFollowing()).toBe(false);
|
|
18
|
+
expect(state.pause()).toBe(false);
|
|
19
|
+
expect(state.resume()).toBe(true);
|
|
20
|
+
expect(state.isFollowing()).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("computes bottom offset and near-bottom status", () => {
|
|
24
|
+
const element = {
|
|
25
|
+
scrollTop: 590,
|
|
26
|
+
scrollHeight: 1000,
|
|
27
|
+
clientHeight: 400
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
expect(getScrollBottomOffset(element)).toBe(600);
|
|
31
|
+
expect(isElementNearBottom(element, 10)).toBe(true);
|
|
32
|
+
expect(isElementNearBottom({ ...element, scrollTop: 560 }, 10)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("pauses transcript-style auto-follow on upward scroll immediately", () => {
|
|
36
|
+
const result = resolveFollowStateFromScroll({
|
|
37
|
+
following: true,
|
|
38
|
+
currentScrollTop: 597,
|
|
39
|
+
lastScrollTop: 600,
|
|
40
|
+
nearBottom: true,
|
|
41
|
+
userScrollThreshold: 1,
|
|
42
|
+
pauseOnUpwardScroll: true,
|
|
43
|
+
pauseWhenAwayFromBottom: false
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.action).toBe("pause");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("resumes event-log-style auto-follow only when scrolling down near bottom", () => {
|
|
50
|
+
const stayPaused = resolveFollowStateFromScroll({
|
|
51
|
+
following: false,
|
|
52
|
+
currentScrollTop: 550,
|
|
53
|
+
lastScrollTop: 560,
|
|
54
|
+
nearBottom: true,
|
|
55
|
+
userScrollThreshold: 1,
|
|
56
|
+
resumeRequiresDownwardScroll: true
|
|
57
|
+
});
|
|
58
|
+
const resume = resolveFollowStateFromScroll({
|
|
59
|
+
following: false,
|
|
60
|
+
currentScrollTop: 590,
|
|
61
|
+
lastScrollTop: 550,
|
|
62
|
+
nearBottom: true,
|
|
63
|
+
userScrollThreshold: 1,
|
|
64
|
+
resumeRequiresDownwardScroll: true
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(stayPaused.action).toBe("none");
|
|
68
|
+
expect(resume.action).toBe("resume");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps transcript-style auto-follow paused near the bottom until scrolling down", () => {
|
|
72
|
+
const stayPaused = resolveFollowStateFromScroll({
|
|
73
|
+
following: false,
|
|
74
|
+
currentScrollTop: 597,
|
|
75
|
+
lastScrollTop: 600,
|
|
76
|
+
nearBottom: true,
|
|
77
|
+
userScrollThreshold: 1,
|
|
78
|
+
resumeRequiresDownwardScroll: true
|
|
79
|
+
});
|
|
80
|
+
const resume = resolveFollowStateFromScroll({
|
|
81
|
+
following: false,
|
|
82
|
+
currentScrollTop: 599,
|
|
83
|
+
lastScrollTop: 597,
|
|
84
|
+
nearBottom: true,
|
|
85
|
+
userScrollThreshold: 1,
|
|
86
|
+
resumeRequiresDownwardScroll: true
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(stayPaused.action).toBe("none");
|
|
90
|
+
expect(resume.action).toBe("resume");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("resolves wheel intent for pause and resume", () => {
|
|
94
|
+
expect(
|
|
95
|
+
resolveFollowStateFromWheel({
|
|
96
|
+
following: true,
|
|
97
|
+
deltaY: -12
|
|
98
|
+
})
|
|
99
|
+
).toBe("pause");
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
resolveFollowStateFromWheel({
|
|
103
|
+
following: false,
|
|
104
|
+
deltaY: 12,
|
|
105
|
+
nearBottom: true,
|
|
106
|
+
resumeWhenNearBottom: true
|
|
107
|
+
})
|
|
108
|
+
).toBe("resume");
|
|
109
|
+
});
|
|
110
|
+
});
|