diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 2bcfb6b54..7568509c3 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -703,7 +703,6 @@ export function AgentStreamView({ offset: nextOffset, animated: false, }); - streamScrollbarMetrics.setOffset(nextOffset); }} /> diff --git a/packages/app/src/components/draggable-list.web.tsx b/packages/app/src/components/draggable-list.web.tsx index 2f545e51f..5a2649d4e 100644 --- a/packages/app/src/components/draggable-list.web.tsx +++ b/packages/app/src/components/draggable-list.web.tsx @@ -210,7 +210,6 @@ export function DraggableList({ metrics={scrollbarMetrics} onScrollToOffset={(nextOffset) => { scrollViewRef.current?.scrollTo({ y: nextOffset, animated: false }); - scrollbarMetrics.setOffset(nextOffset); }} /> diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 7a87e4fc7..bd33013b5 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -764,7 +764,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) { offset: nextOffset, animated: false, }); - treeScrollbarMetrics.setOffset(nextOffset); }} /> @@ -827,7 +826,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) { offset: nextOffset, animated: false, }); - treeScrollbarMetrics.setOffset(nextOffset); }} /> diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index c9a2c25aa..49f2318f3 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -1413,7 +1413,6 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) { offset: nextOffset, animated: false, }); - diffScrollbarMetrics.setOffset(nextOffset); }} /> diff --git a/packages/app/src/components/web-desktop-scrollbar.tsx b/packages/app/src/components/web-desktop-scrollbar.tsx index 5cfe99881..f3394e709 100644 --- a/packages/app/src/components/web-desktop-scrollbar.tsx +++ b/packages/app/src/components/web-desktop-scrollbar.tsx @@ -21,9 +21,11 @@ const HANDLE_GRAB_VERTICAL_PADDING = 8; const HANDLE_OPACITY_VISIBLE = 0.62; const HANDLE_OPACITY_HOVERED = 0.78; const HANDLE_OPACITY_DRAGGING = 0.9; +const HANDLE_TRAVEL_TRANSITION_DURATION_MS = 90; const HANDLE_FADE_DURATION_MS = 220; const HANDLE_WIDTH_TRANSITION_DURATION_MS = 240; const HANDLE_SCROLL_VISIBILITY_MS = 1200; +const HANDLE_SCROLL_ACTIVE_MS = 110; function readClientY(event: any): number | null { const value = @@ -128,9 +130,11 @@ export function WebDesktopScrollbarOverlay({ const [isHandleHovered, setIsHandleHovered] = useState(false); const [isDragging, setIsDragging] = useState(false); const [isScrollVisible, setIsScrollVisible] = useState(false); + const [isScrollActive, setIsScrollActive] = useState(false); const dragStartOffsetRef = useRef(0); const dragStartClientYRef = useRef(0); const scrollVisibilityTimeoutRef = useRef | null>(null); + const scrollActiveTimeoutRef = useRef | null>(null); const lastObservedOffsetRef = useRef(null); const geometryRef = useRef({ maxHandleOffset: 0, @@ -143,6 +147,7 @@ export function WebDesktopScrollbarOverlay({ const normalizedOffset = inverted ? Math.max(0, maxScrollOffset - clamp(metrics.offset, 0, maxScrollOffset)) : clamp(metrics.offset, 0, maxScrollOffset); + const normalizedOffsetRef = useRef(normalizedOffset); const geometry = useMemo( () => @@ -165,6 +170,10 @@ export function WebDesktopScrollbarOverlay({ onScrollToOffsetRef.current = onScrollToOffset; }, [onScrollToOffset]); + useEffect(() => { + normalizedOffsetRef.current = normalizedOffset; + }, [normalizedOffset]); + const clearScrollVisibilityTimeout = useCallback(() => { if (scrollVisibilityTimeoutRef.current === null) { return; @@ -173,6 +182,14 @@ export function WebDesktopScrollbarOverlay({ scrollVisibilityTimeoutRef.current = null; }, []); + const clearScrollActiveTimeout = useCallback(() => { + if (scrollActiveTimeoutRef.current === null) { + return; + } + clearTimeout(scrollActiveTimeoutRef.current); + scrollActiveTimeoutRef.current = null; + }, []); + const revealScrollbarFromScroll = useCallback(() => { setIsScrollVisible(true); clearScrollVisibilityTimeout(); @@ -182,10 +199,21 @@ export function WebDesktopScrollbarOverlay({ }, HANDLE_SCROLL_VISIBILITY_MS); }, [clearScrollVisibilityTimeout]); + const markScrollActivity = useCallback(() => { + setIsScrollActive(true); + clearScrollActiveTimeout(); + scrollActiveTimeoutRef.current = setTimeout(() => { + setIsScrollActive(false); + scrollActiveTimeoutRef.current = null; + }, HANDLE_SCROLL_ACTIVE_MS); + }, [clearScrollActiveTimeout]); + useEffect(() => { if (!enabled || !geometry.isVisible) { setIsScrollVisible(false); + setIsScrollActive(false); clearScrollVisibilityTimeout(); + clearScrollActiveTimeout(); lastObservedOffsetRef.current = null; return; } @@ -199,19 +227,23 @@ export function WebDesktopScrollbarOverlay({ return; } revealScrollbarFromScroll(); + markScrollActivity(); }, [ + clearScrollActiveTimeout, clearScrollVisibilityTimeout, enabled, geometry.isVisible, + markScrollActivity, normalizedOffset, revealScrollbarFromScroll, ]); useEffect( () => () => { + clearScrollActiveTimeout(); clearScrollVisibilityTimeout(); }, - [clearScrollVisibilityTimeout] + [clearScrollActiveTimeout, clearScrollVisibilityTimeout] ); const applyDragDelta = useCallback( @@ -241,7 +273,7 @@ export function WebDesktopScrollbarOverlay({ onMoveShouldSetPanResponder: () => true, onPanResponderTerminationRequest: () => false, onPanResponderGrant: () => { - dragStartOffsetRef.current = normalizedOffset; + dragStartOffsetRef.current = normalizedOffsetRef.current; setIsDragging(true); }, onPanResponderMove: (_event, gestureState) => { @@ -254,7 +286,7 @@ export function WebDesktopScrollbarOverlay({ setIsDragging(false); }, }); - }, [applyDragDelta, isWeb, normalizedOffset]); + }, [applyDragDelta, isWeb]); const startWebDrag = useCallback( (event: any) => { @@ -268,11 +300,11 @@ export function WebDesktopScrollbarOverlay({ event?.preventDefault?.(); event?.stopPropagation?.(); event?.nativeEvent?.preventDefault?.(); - dragStartOffsetRef.current = normalizedOffset; + dragStartOffsetRef.current = normalizedOffsetRef.current; dragStartClientYRef.current = clientY; setIsDragging(true); }, - [isWeb, normalizedOffset] + [isWeb] ); const handleGrabHoverIn = useCallback(() => { @@ -329,26 +361,33 @@ export function WebDesktopScrollbarOverlay({ ? theme.colors.palette.zinc[500] : theme.colors.palette.zinc[700]; const handleCursor = isDragging ? "grabbing" : "grab"; - const grabAreaTop = Math.max(0, geometry.handleOffset - HANDLE_GRAB_VERTICAL_PADDING); - const grabAreaHeight = Math.min( - metrics.viewportSize - grabAreaTop, + const handleTravelDurationMs = + isDragging || isScrollActive ? 0 : HANDLE_TRAVEL_TRANSITION_DURATION_MS; + const thumbRegionOffset = Math.max(0, geometry.handleOffset - HANDLE_GRAB_VERTICAL_PADDING); + const thumbRegionHeight = Math.min( + metrics.viewportSize - thumbRegionOffset, geometry.handleSize + HANDLE_GRAB_VERTICAL_PADDING * 2 ); + const handleInsetTop = Math.max(0, (thumbRegionHeight - geometry.handleSize) / 2); return ( - + > + + ); } @@ -402,11 +439,11 @@ const styles = StyleSheet.create(() => ({ zIndex: 10, }, handle: { - position: "absolute", width: HANDLE_WIDTH_IDLE, borderRadius: 999, + alignSelf: "center", }, - handleGrabArea: { + thumbRegion: { position: "absolute", right: -3, width: HANDLE_GRAB_WIDTH,