From 9804aa7443cc1c1e4a427cde34834f7a7ab9e36a Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 14 Apr 2026 12:50:22 -0500 Subject: [PATCH] fix: scrolling while selecting --- ui-tui/src/app.tsx | 53 ++++++++++++++++++++++++++++++-- ui-tui/src/types/hermes-ink.d.ts | 2 ++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index 13d00834e..86f5c7a2e 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -558,6 +558,53 @@ export function App({ gw }: { gw: GatewayClient }) { const virtualHistory = useVirtualHistory(scrollRef, virtualRows) + const scrollWithSelection = useCallback( + (delta: number) => { + const s = scrollRef.current + const sel = selection.getState() as + | { anchor?: { row: number }; focus?: { row: number }; isDragging?: boolean } + | null + + if (!s || !sel?.anchor || !sel.focus) { + s?.scrollBy(delta) + return + } + + const top = s.getViewportTop() + const bottom = top + s.getViewportHeight() - 1 + + if (sel.anchor.row < top || sel.anchor.row > bottom) { + s.scrollBy(delta) + return + } + + if (!sel.isDragging && (sel.focus.row < top || sel.focus.row > bottom)) { + s.scrollBy(delta) + return + } + + const max = Math.max(0, s.getScrollHeight() - s.getViewportHeight()) + const cur = s.getScrollTop() + s.getPendingDelta() + const actual = Math.max(0, Math.min(max, cur + delta)) - cur + + if (actual === 0) { + return + } + + if (actual > 0) { + selection.captureScrolledRows(top, top + actual - 1, 'above') + sel.isDragging ? selection.shiftAnchor(-actual, top, bottom) : selection.shiftSelection(-actual, top, bottom) + } else { + const amount = -actual + selection.captureScrolledRows(bottom - amount + 1, bottom, 'below') + sel.isDragging ? selection.shiftAnchor(amount, top, bottom) : selection.shiftSelection(amount, top, bottom) + } + + s.scrollBy(delta) + }, + [selection] + ) + // ── Resize RPC ─────────────────────────────────────────────────── useEffect(() => { @@ -1326,13 +1373,13 @@ export function App({ gw }: { gw: GatewayClient }) { } if (key.wheelUp) { - scrollRef.current?.scrollBy(-WHEEL_SCROLL_STEP) + scrollWithSelection(-WHEEL_SCROLL_STEP) return } if (key.wheelDown) { - scrollRef.current?.scrollBy(WHEEL_SCROLL_STEP) + scrollWithSelection(WHEEL_SCROLL_STEP) return } @@ -1340,7 +1387,7 @@ export function App({ gw }: { gw: GatewayClient }) { if (key.pageUp || key.pageDown) { const viewport = scrollRef.current?.getViewportHeight() ?? Math.max(6, (stdout?.rows ?? 24) - 8) const step = Math.max(4, viewport - 2) - scrollRef.current?.scrollBy(key.pageUp ? -step : step) + scrollWithSelection(key.pageUp ? -step : step) return } diff --git a/ui-tui/src/types/hermes-ink.d.ts b/ui-tui/src/types/hermes-ink.d.ts index 6b3001a85..d144656b3 100644 --- a/ui-tui/src/types/hermes-ink.d.ts +++ b/ui-tui/src/types/hermes-ink.d.ts @@ -53,7 +53,9 @@ declare module '@hermes/ink' { readonly scrollToBottom: () => void readonly getScrollTop: () => number readonly getPendingDelta: () => number + readonly getScrollHeight: () => number readonly getViewportHeight: () => number + readonly getViewportTop: () => number readonly isSticky: () => boolean readonly subscribe: (listener: () => void) => () => void }