Refactor git actions and update React to 19.1.4
This commit is contained in:
@@ -24,6 +24,7 @@ const USER_SCROLL_DELTA_EPSILON = 1
|
||||
const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64
|
||||
const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1
|
||||
const WEB_STREAM_SCROLLBAR_STYLE_ID = 'web-stream-viewport-scrollbar-style'
|
||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__)
|
||||
const WEB_STREAM_SCROLLBAR_STYLE = `
|
||||
#agent-chat-scroll-web-dom-scroll,
|
||||
#agent-chat-scroll-web-dom-virtualized {
|
||||
@@ -39,6 +40,20 @@ const WEB_STREAM_SCROLLBAR_STYLE = `
|
||||
}
|
||||
`
|
||||
|
||||
function logWebStickyBottom(event: string, details: Record<string, unknown>): void {
|
||||
if (!IS_DEV) {
|
||||
return
|
||||
}
|
||||
console.log('[WebStickyBottom]', event, details)
|
||||
}
|
||||
|
||||
function getDebugNow(): number | null {
|
||||
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
||||
return Number(performance.now().toFixed(3))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isScrollContainerNearBottom(
|
||||
scrollContainer: Pick<HTMLElement, 'scrollTop' | 'clientHeight' | 'scrollHeight'>,
|
||||
thresholdPx = AUTO_SCROLL_BOTTOM_THRESHOLD_PX
|
||||
@@ -119,6 +134,13 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
}
|
||||
const followOutputRef = useRef(followOutput)
|
||||
const lastKnownScrollTopRef = useRef(0)
|
||||
const lastLoggedMetricsRef = useRef<{
|
||||
scrollTop: number
|
||||
clientWidth: number
|
||||
clientHeight: number
|
||||
scrollWidth: number
|
||||
scrollHeight: number
|
||||
} | null>(null)
|
||||
const pendingUserScrollUpIntentRef = useRef(false)
|
||||
const isPointerScrollActiveRef = useRef(false)
|
||||
const lastTouchClientYRef = useRef<number | null>(null)
|
||||
@@ -156,6 +178,17 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
const viewportHeight = instance.scrollRect?.height ?? 0
|
||||
const scrollOffset = instance.scrollOffset ?? 0
|
||||
const remainingDistance = instance.getTotalSize() - (scrollOffset + viewportHeight)
|
||||
logWebStickyBottom('virtualizer_item_size_change', {
|
||||
agentId: props.agentId,
|
||||
delta: _delta,
|
||||
itemIndex: _item.index,
|
||||
itemStart: _item.start,
|
||||
itemSize: _item.size,
|
||||
viewportHeight,
|
||||
scrollOffset,
|
||||
totalSize: instance.getTotalSize(),
|
||||
remainingDistance,
|
||||
})
|
||||
return remainingDistance > AUTO_SCROLL_BOTTOM_THRESHOLD_PX
|
||||
}
|
||||
return () => {
|
||||
@@ -187,11 +220,21 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
if (isScrollContainerOverscrolledPastBottom(scrollContainer)) {
|
||||
return
|
||||
}
|
||||
logWebStickyBottom('viewport_scroll_to_bottom', {
|
||||
agentId: props.agentId,
|
||||
behavior,
|
||||
followOutput: followOutputRef.current,
|
||||
scrollTop: scrollContainer.scrollTop,
|
||||
clientWidth: scrollContainer.clientWidth,
|
||||
clientHeight: scrollContainer.clientHeight,
|
||||
scrollWidth: scrollContainer.scrollWidth,
|
||||
scrollHeight: scrollContainer.scrollHeight,
|
||||
})
|
||||
scrollElementToBottom(scrollContainer, behavior)
|
||||
lastKnownScrollTopRef.current = scrollContainer.scrollTop
|
||||
syncNearBottom(scrollContainer, onNearBottomChange)
|
||||
},
|
||||
[onNearBottomChange]
|
||||
[onNearBottomChange, props.agentId]
|
||||
)
|
||||
|
||||
const scheduleStickToBottom = useCallback(
|
||||
@@ -203,6 +246,15 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
if (pendingAutoScrollFrameRef.current !== null) {
|
||||
return
|
||||
}
|
||||
logWebStickyBottom('viewport_schedule_stick_to_bottom', {
|
||||
agentId: props.agentId,
|
||||
followOutput: followOutputRef.current,
|
||||
scrollTop: scrollContainer?.scrollTop ?? null,
|
||||
clientWidth: scrollContainer?.clientWidth ?? null,
|
||||
clientHeight: scrollContainer?.clientHeight ?? null,
|
||||
scrollWidth: scrollContainer?.scrollWidth ?? null,
|
||||
scrollHeight: scrollContainer?.scrollHeight ?? null,
|
||||
})
|
||||
pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => {
|
||||
pendingAutoScrollFrameRef.current = null
|
||||
if (!followOutputRef.current) {
|
||||
@@ -211,7 +263,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
scrollMessagesToBottom('auto')
|
||||
})
|
||||
},
|
||||
[scrollMessagesToBottom]
|
||||
[props.agentId, scrollMessagesToBottom]
|
||||
)
|
||||
|
||||
const forceStickToBottom = useCallback(() => {
|
||||
@@ -254,7 +306,31 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
},
|
||||
} as never)
|
||||
syncNearBottom(scrollContainer, onNearBottomChange)
|
||||
}, [onNearBottomChange, streamScrollbarMetrics])
|
||||
const currentMetrics = {
|
||||
scrollTop: scrollContainer.scrollTop,
|
||||
clientWidth: scrollContainer.clientWidth,
|
||||
clientHeight: scrollContainer.clientHeight,
|
||||
scrollWidth: scrollContainer.scrollWidth,
|
||||
scrollHeight: scrollContainer.scrollHeight,
|
||||
}
|
||||
const previousMetrics = lastLoggedMetricsRef.current
|
||||
const shouldLog =
|
||||
!previousMetrics ||
|
||||
previousMetrics.scrollTop !== currentMetrics.scrollTop ||
|
||||
previousMetrics.clientWidth !== currentMetrics.clientWidth ||
|
||||
previousMetrics.clientHeight !== currentMetrics.clientHeight ||
|
||||
previousMetrics.scrollWidth !== currentMetrics.scrollWidth ||
|
||||
previousMetrics.scrollHeight !== currentMetrics.scrollHeight
|
||||
if (shouldLog) {
|
||||
lastLoggedMetricsRef.current = currentMetrics
|
||||
logWebStickyBottom('viewport_metrics_updated', {
|
||||
agentId: props.agentId,
|
||||
followOutput: followOutputRef.current,
|
||||
distanceFromBottom: getScrollContainerDistanceFromBottom(scrollContainer),
|
||||
...currentMetrics,
|
||||
})
|
||||
}
|
||||
}, [onNearBottomChange, props.agentId, streamScrollbarMetrics])
|
||||
|
||||
const handleDomScroll = useCallback(() => {
|
||||
const scrollContainer = scrollContainerRef.current
|
||||
@@ -283,6 +359,19 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
}
|
||||
|
||||
lastKnownScrollTopRef.current = currentScrollTop
|
||||
logWebStickyBottom('viewport_dom_scroll', {
|
||||
agentId: props.agentId,
|
||||
now: getDebugNow(),
|
||||
scrollTop: currentScrollTop,
|
||||
clientHeight: scrollContainer.clientHeight,
|
||||
scrollHeight: scrollContainer.scrollHeight,
|
||||
activeElementTag:
|
||||
typeof document !== 'undefined' ? document.activeElement?.tagName?.toLowerCase() ?? null : null,
|
||||
activeElementRole:
|
||||
typeof document !== 'undefined'
|
||||
? document.activeElement?.getAttribute?.('aria-label') ?? null
|
||||
: null,
|
||||
})
|
||||
updateScrollMetrics()
|
||||
}, [cancelPendingStickToBottom, updateScrollMetrics])
|
||||
|
||||
@@ -348,6 +437,15 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
|
||||
updateScrollMetrics()
|
||||
const observer = new ResizeObserver(() => {
|
||||
logWebStickyBottom('viewport_resize_observed', {
|
||||
agentId: props.agentId,
|
||||
followOutput: followOutputRef.current,
|
||||
scrollTop: scrollContainer.scrollTop,
|
||||
clientWidth: scrollContainer.clientWidth,
|
||||
clientHeight: scrollContainer.clientHeight,
|
||||
scrollWidth: scrollContainer.scrollWidth,
|
||||
scrollHeight: scrollContainer.scrollHeight,
|
||||
})
|
||||
updateScrollMetrics()
|
||||
if (!followOutputRef.current) {
|
||||
return
|
||||
@@ -361,7 +459,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [scheduleStickToBottom, updateScrollMetrics])
|
||||
}, [props.agentId, scheduleStickToBottom, updateScrollMetrics])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = scrollContainerRef.current
|
||||
@@ -369,6 +467,59 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
return
|
||||
}
|
||||
|
||||
const originalScrollTo = scrollContainer.scrollTo.bind(scrollContainer)
|
||||
const scrollTopDescriptor =
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(scrollContainer), 'scrollTop') ??
|
||||
Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop')
|
||||
scrollContainer.scrollTo = ((...args: Parameters<HTMLElement['scrollTo']>) => {
|
||||
const firstArg = args[0] as ScrollToOptions | number | undefined
|
||||
const target =
|
||||
typeof firstArg === 'object' && firstArg !== null
|
||||
? {
|
||||
top: firstArg.top ?? null,
|
||||
left: firstArg.left ?? null,
|
||||
behavior: firstArg.behavior ?? null,
|
||||
}
|
||||
: {
|
||||
top: typeof args[1] === 'number' ? args[1] : null,
|
||||
left: typeof firstArg === 'number' ? firstArg : null,
|
||||
behavior: null,
|
||||
}
|
||||
logWebStickyBottom('viewport_scroll_to_called', {
|
||||
agentId: props.agentId,
|
||||
now: getDebugNow(),
|
||||
currentScrollTop: scrollContainer.scrollTop,
|
||||
target,
|
||||
stack:
|
||||
typeof Error !== 'undefined'
|
||||
? new Error().stack?.split('\n').slice(1, 6).join('\n') ?? null
|
||||
: null,
|
||||
})
|
||||
return originalScrollTo(...args)
|
||||
}) as typeof scrollContainer.scrollTo
|
||||
if (scrollTopDescriptor?.get && scrollTopDescriptor?.set) {
|
||||
Object.defineProperty(scrollContainer, 'scrollTop', {
|
||||
configurable: true,
|
||||
enumerable: scrollTopDescriptor.enumerable ?? false,
|
||||
get() {
|
||||
return scrollTopDescriptor.get?.call(scrollContainer)
|
||||
},
|
||||
set(value: number) {
|
||||
logWebStickyBottom('viewport_scroll_top_set', {
|
||||
agentId: props.agentId,
|
||||
now: getDebugNow(),
|
||||
currentScrollTop: scrollTopDescriptor.get?.call(scrollContainer) ?? null,
|
||||
nextScrollTop: value,
|
||||
stack:
|
||||
typeof Error !== 'undefined'
|
||||
? new Error().stack?.split('\n').slice(1, 6).join('\n') ?? null
|
||||
: null,
|
||||
})
|
||||
return scrollTopDescriptor.set?.call(scrollContainer, value)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
if (event.deltaY < 0) {
|
||||
pendingUserScrollUpIntentRef.current = true
|
||||
@@ -403,6 +554,25 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
const handleTouchEnd = () => {
|
||||
lastTouchClientYRef.current = null
|
||||
}
|
||||
const handleSelectionChange = () => {
|
||||
const activeElement =
|
||||
typeof document !== 'undefined' ? (document.activeElement as HTMLTextAreaElement | null) : null
|
||||
logWebStickyBottom('document_selection_changed', {
|
||||
agentId: props.agentId,
|
||||
now: getDebugNow(),
|
||||
activeElementTag: activeElement?.tagName?.toLowerCase() ?? null,
|
||||
activeElementRole: activeElement?.getAttribute?.('aria-label') ?? null,
|
||||
selectionStart:
|
||||
activeElement && typeof activeElement.selectionStart === 'number'
|
||||
? activeElement.selectionStart
|
||||
: null,
|
||||
selectionEnd:
|
||||
activeElement && typeof activeElement.selectionEnd === 'number'
|
||||
? activeElement.selectionEnd
|
||||
: null,
|
||||
scrollTop: scrollContainer.scrollTop,
|
||||
})
|
||||
}
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleDomScroll, { passive: true })
|
||||
scrollContainer.addEventListener('wheel', handleWheel, { passive: true })
|
||||
@@ -413,6 +583,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
scrollContainer.addEventListener('touchmove', handleTouchMove, { passive: true })
|
||||
scrollContainer.addEventListener('touchend', handleTouchEnd, { passive: true })
|
||||
scrollContainer.addEventListener('touchcancel', handleTouchEnd, { passive: true })
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('selectionchange', handleSelectionChange, { passive: true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleDomScroll)
|
||||
@@ -424,8 +597,15 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
scrollContainer.removeEventListener('touchmove', handleTouchMove)
|
||||
scrollContainer.removeEventListener('touchend', handleTouchEnd)
|
||||
scrollContainer.removeEventListener('touchcancel', handleTouchEnd)
|
||||
scrollContainer.scrollTo = originalScrollTo
|
||||
if (scrollTopDescriptor) {
|
||||
Reflect.deleteProperty(scrollContainer, 'scrollTop')
|
||||
}
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('selectionchange', handleSelectionChange)
|
||||
}
|
||||
}
|
||||
}, [cancelPendingStickToBottom, handleDomScroll])
|
||||
}, [cancelPendingStickToBottom, handleDomScroll, props.agentId])
|
||||
|
||||
useEffect(() => {
|
||||
const handle: StreamViewportHandle = {
|
||||
@@ -438,6 +618,16 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
if (!followOutputRef.current) {
|
||||
return
|
||||
}
|
||||
const scrollContainer = scrollContainerRef.current
|
||||
logWebStickyBottom('viewport_prepare_for_change', {
|
||||
agentId: props.agentId,
|
||||
followOutput: followOutputRef.current,
|
||||
scrollTop: scrollContainer?.scrollTop ?? null,
|
||||
clientWidth: scrollContainer?.clientWidth ?? null,
|
||||
clientHeight: scrollContainer?.clientHeight ?? null,
|
||||
scrollWidth: scrollContainer?.scrollWidth ?? null,
|
||||
scrollHeight: scrollContainer?.scrollHeight ?? null,
|
||||
})
|
||||
scheduleStickToBottom()
|
||||
},
|
||||
}
|
||||
@@ -448,7 +638,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
}
|
||||
cancelPendingStickToBottom()
|
||||
}
|
||||
}, [cancelPendingStickToBottom, forceStickToBottom, scheduleStickToBottom, viewportRef])
|
||||
}, [cancelPendingStickToBottom, forceStickToBottom, props.agentId, scheduleStickToBottom, viewportRef])
|
||||
|
||||
const contentContainerStyle = useMemo(
|
||||
(): CSSProperties => ({
|
||||
|
||||
Reference in New Issue
Block a user