diff --git a/Agent b/Agent new file mode 100644 index 0000000..e69de29 diff --git a/apps/terminal/src/components/terminal-scrollbar.tsx b/apps/terminal/src/components/terminal-scrollbar.tsx index 4e829c0..cd8dbd5 100644 --- a/apps/terminal/src/components/terminal-scrollbar.tsx +++ b/apps/terminal/src/components/terminal-scrollbar.tsx @@ -46,6 +46,7 @@ export const TerminalScrollbar = ({ terminal, hostRef }: TerminalScrollbarProps) const scrollLingerTimerRef = useRef(null); const refreshFrameRef = useRef(null); const dragOriginRef = useRef<{ pointerY: number; viewportY: number } | null>(null); + const trackElementRef = useRef(null); const lastDistanceFromBottomRef = useRef(0); const trackObserverRef = useRef(null); @@ -59,6 +60,7 @@ export const TerminalScrollbar = ({ terminal, hostRef }: TerminalScrollbarProps) // with [] deps would miss it appearing later. A ref callback wires up the // ResizeObserver exactly when the track mounts and tears it down on unmount. const attachTrackElement = useCallback((trackElement: HTMLDivElement | null) => { + trackElementRef.current = trackElement; if (trackObserverRef.current) { trackObserverRef.current.disconnect(); trackObserverRef.current = null; @@ -193,27 +195,48 @@ export const TerminalScrollbar = ({ terminal, hostRef }: TerminalScrollbarProps) minThumbHeightPx: TERMINAL_SCROLLBAR_THUMB_MIN_HEIGHT_PX, }); - const handleThumbPointerDown = useCallback( + const handleTrackPointerDown = useCallback( (event: ReactPointerEvent) => { if (!terminal || !isScrollable) return; if (event.button !== 0) return; event.preventDefault(); - event.stopPropagation(); + const trackElement = trackElementRef.current; + if (!trackElement) return; + try { - event.currentTarget.setPointerCapture(event.pointerId); + trackElement.setPointerCapture(event.pointerId); } catch { /* pointer capture is unsupported in jsdom and a few exotic browsers; drag still works without it */ } - dragOriginRef.current = { - pointerY: event.clientY, - viewportY: terminal.buffer.active.viewportY, - }; + + const trackRect = trackElement.getBoundingClientRect(); + const clickOffsetPx = event.clientY - trackRect.top; + const isOnThumb = clickOffsetPx >= thumbTopPx && clickOffsetPx <= thumbTopPx + thumbHeightPx; + + if (isOnThumb) { + dragOriginRef.current = { + pointerY: event.clientY, + viewportY: terminal.buffer.active.viewportY, + }; + } else { + const currentBaseY = terminal.buffer.active.baseY; + const usableTrackPx = Math.max(trackHeightPx - thumbHeightPx, 1); + const scrollProgress = Math.max(0, Math.min(1, (clickOffsetPx - thumbHeightPx / 2) / usableTrackPx)); + const targetViewportY = Math.max(0, Math.min(currentBaseY, Math.round(scrollProgress * currentBaseY))); + const currentViewportY = terminal.buffer.active.viewportY; + const lineDelta = targetViewportY - currentViewportY; + if (lineDelta !== 0) terminal.scrollLines(lineDelta); + dragOriginRef.current = { + pointerY: event.clientY, + viewportY: targetViewportY, + }; + } setIsDragging(true); }, - [isScrollable, terminal], + [isScrollable, terminal, thumbTopPx, thumbHeightPx, trackHeightPx], ); - const handleThumbPointerMove = useCallback( + const handleTrackPointerMove = useCallback( (event: ReactPointerEvent) => { const dragOrigin = dragOriginRef.current; if (!dragOrigin || !terminal) return; @@ -232,13 +255,14 @@ export const TerminalScrollbar = ({ terminal, hostRef }: TerminalScrollbarProps) [terminal, thumbHeightPx, trackHeightPx], ); - const handleThumbPointerUp = useCallback((event: ReactPointerEvent) => { + const handleTrackPointerUp = useCallback((event: ReactPointerEvent) => { + const trackElement = trackElementRef.current; try { - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); + if (trackElement?.hasPointerCapture(event.pointerId)) { + trackElement.releasePointerCapture(event.pointerId); } } catch { - /* see handleThumbPointerDown */ + /* see handleTrackPointerDown */ } dragOriginRef.current = null; setIsDragging(false); @@ -252,17 +276,24 @@ export const TerminalScrollbar = ({ terminal, hostRef }: TerminalScrollbarProps)