|
|
@@ -150,7 +150,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
|
|
const prevExpandedRowsRef = useRef<Record<number, boolean>>()
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
|
- const disableAutoScrollRef = useRef(false)
|
|
|
+ const stickyFollowRef = useRef<boolean>(false)
|
|
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
|
const [isAtBottom, setIsAtBottom] = useState(false)
|
|
|
const lastTtsRef = useRef<string>("")
|
|
|
@@ -480,9 +480,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Expanding a row indicates the user is browsing; disable sticky follow
|
|
|
if (wasAnyRowExpandedByUser) {
|
|
|
- disableAutoScrollRef.current = true
|
|
|
+ stickyFollowRef.current = false
|
|
|
}
|
|
|
+
|
|
|
prevExpandedRowsRef.current = expandedRows // Store current state for next comparison
|
|
|
}, [expandedRows])
|
|
|
|
|
|
@@ -555,7 +557,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
// Do not reset mode here as it should persist.
|
|
|
// setPrimaryButtonText(undefined)
|
|
|
// setSecondaryButtonText(undefined)
|
|
|
- disableAutoScrollRef.current = false
|
|
|
}, [])
|
|
|
|
|
|
/**
|
|
|
@@ -1112,7 +1113,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
|
|
|
const handleRowHeightChange = useCallback(
|
|
|
(isTaller: boolean) => {
|
|
|
- if (!disableAutoScrollRef.current) {
|
|
|
+ if (isAtBottom) {
|
|
|
if (isTaller) {
|
|
|
scrollToBottomSmooth()
|
|
|
} else {
|
|
|
@@ -1120,34 +1121,35 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- [scrollToBottomSmooth, scrollToBottomAuto],
|
|
|
+ [scrollToBottomSmooth, scrollToBottomAuto, isAtBottom],
|
|
|
)
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- let timer: ReturnType<typeof setTimeout> | undefined
|
|
|
- if (!disableAutoScrollRef.current) {
|
|
|
- timer = setTimeout(() => scrollToBottomSmooth(), 50)
|
|
|
- }
|
|
|
- return () => {
|
|
|
- if (timer) {
|
|
|
- clearTimeout(timer)
|
|
|
- }
|
|
|
- }
|
|
|
- }, [groupedMessages.length, scrollToBottomSmooth])
|
|
|
-
|
|
|
+ // Disable sticky follow when user scrolls up inside the chat container
|
|
|
const handleWheel = useCallback((event: Event) => {
|
|
|
const wheelEvent = event as WheelEvent
|
|
|
+ if (wheelEvent.deltaY < 0 && scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
|
|
|
+ stickyFollowRef.current = false
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+ useEvent("wheel", handleWheel, window, { passive: true })
|
|
|
|
|
|
- if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
|
|
|
- if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
|
|
|
- // User scrolled up
|
|
|
- disableAutoScrollRef.current = true
|
|
|
+ // Also disable sticky follow when the chat container is scrolled away from bottom
|
|
|
+ useEffect(() => {
|
|
|
+ const el = scrollContainerRef.current
|
|
|
+ if (!el) return
|
|
|
+ const onScroll = () => {
|
|
|
+ // Consider near-bottom within a small threshold consistent with Virtuoso settings
|
|
|
+ const nearBottom = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 10
|
|
|
+ if (!nearBottom) {
|
|
|
+ stickyFollowRef.current = false
|
|
|
}
|
|
|
+ // Keep UI button state in sync with scroll position
|
|
|
+ setShowScrollToBottom(!nearBottom)
|
|
|
}
|
|
|
+ el.addEventListener("scroll", onScroll, { passive: true })
|
|
|
+ return () => el.removeEventListener("scroll", onScroll)
|
|
|
}, [])
|
|
|
|
|
|
- useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
|
|
|
-
|
|
|
// Effect to clear checkpoint warning when messages appear or task changes
|
|
|
useEffect(() => {
|
|
|
if (isHidden || !task) {
|
|
|
@@ -1443,26 +1445,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
|
|
|
{task && (
|
|
|
<>
|
|
|
- <div className="grow flex flex-col min-h-0" ref={scrollContainerRef}>
|
|
|
- <div className="flex-auto min-h-0">
|
|
|
- <Virtuoso
|
|
|
- ref={virtuosoRef}
|
|
|
- key={task.ts}
|
|
|
- className="h-full overflow-y-auto mb-1"
|
|
|
- increaseViewportBy={{ top: 3_000, bottom: 1000 }}
|
|
|
- data={groupedMessages}
|
|
|
- itemContent={itemContent}
|
|
|
- atBottomStateChange={(isAtBottom: boolean) => {
|
|
|
- setIsAtBottom(isAtBottom)
|
|
|
- if (isAtBottom) {
|
|
|
- disableAutoScrollRef.current = false
|
|
|
- }
|
|
|
- setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
|
|
|
- }}
|
|
|
- atBottomThreshold={10}
|
|
|
- initialTopMostItemIndex={groupedMessages.length - 1}
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <div className="grow flex" ref={scrollContainerRef}>
|
|
|
+ <Virtuoso
|
|
|
+ ref={virtuosoRef}
|
|
|
+ key={task.ts}
|
|
|
+ className="scrollable grow overflow-y-scroll mb-1"
|
|
|
+ increaseViewportBy={{ top: 3_000, bottom: 1000 }}
|
|
|
+ data={groupedMessages}
|
|
|
+ itemContent={itemContent}
|
|
|
+ followOutput={(isAtBottom: boolean) => isAtBottom || stickyFollowRef.current}
|
|
|
+ atBottomStateChange={(isAtBottom: boolean) => {
|
|
|
+ setIsAtBottom(isAtBottom)
|
|
|
+ // Only show the scroll-to-bottom button if not at bottom
|
|
|
+ setShowScrollToBottom(!isAtBottom)
|
|
|
+ }}
|
|
|
+ atBottomThreshold={10}
|
|
|
+ initialTopMostItemIndex={groupedMessages.length - 1}
|
|
|
+ />
|
|
|
</div>
|
|
|
{areButtonsVisible && (
|
|
|
<div
|
|
|
@@ -1479,8 +1478,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
variant="secondary"
|
|
|
className="flex-[2]"
|
|
|
onClick={() => {
|
|
|
- scrollToBottomSmooth()
|
|
|
- disableAutoScrollRef.current = false
|
|
|
+ // Engage sticky follow until user scrolls up
|
|
|
+ stickyFollowRef.current = true
|
|
|
+ // Pin immediately to avoid lag during fast streaming
|
|
|
+ scrollToBottomAuto()
|
|
|
+ // Hide button immediately to prevent flash
|
|
|
+ setShowScrollToBottom(false)
|
|
|
}}>
|
|
|
<span className="codicon codicon-chevron-down"></span>
|
|
|
</Button>
|