diff --git a/cli/src/chat.tsx b/cli/src/chat.tsx index 4ade6c252..1ad6db5b2 100644 --- a/cli/src/chat.tsx +++ b/cli/src/chat.tsx @@ -394,7 +394,7 @@ export const Chat = ({ return isUserCollapsingRef.current }, []) - const { scrollToLatest, scrollboxProps, isAtBottom } = useChatScrollbox( + const { scrollToLatest, scrollUp, scrollDown, scrollboxProps, isAtBottom } = useChatScrollbox( scrollRef, messages, isUserCollapsing, @@ -1275,6 +1275,8 @@ export const Chat = ({ } }) }, + onScrollUp: scrollUp, + onScrollDown: scrollDown, onOpenBuyCredits: () => { // If credits have been restored, just return to default mode if (areCreditsRestored()) { @@ -1314,6 +1316,8 @@ export const Chat = ({ inputRef, handleCtrlC, clearQueue, + scrollUp, + scrollDown, ], ) diff --git a/cli/src/hooks/use-chat-keyboard.ts b/cli/src/hooks/use-chat-keyboard.ts index 51574f66d..26ac9ecd8 100644 --- a/cli/src/hooks/use-chat-keyboard.ts +++ b/cli/src/hooks/use-chat-keyboard.ts @@ -75,6 +75,10 @@ export type ChatKeyboardHandlers = { onPasteImagePath: (imagePath: string) => void onPasteText: (text: string) => void + // Scroll handlers + onScrollUp: () => void + onScrollDown: () => void + // Out of credits handler onOpenBuyCredits: () => void } @@ -229,6 +233,12 @@ function dispatchAction( } return true } + case 'scroll-up': + handlers.onScrollUp() + return true + case 'scroll-down': + handlers.onScrollDown() + return true case 'open-buy-credits': handlers.onOpenBuyCredits() return true diff --git a/cli/src/hooks/use-scroll-management.ts b/cli/src/hooks/use-scroll-management.ts index 8687e16cf..0f55b280c 100644 --- a/cli/src/hooks/use-scroll-management.ts +++ b/cli/src/hooks/use-scroll-management.ts @@ -9,6 +9,9 @@ const SCROLL_NEAR_BOTTOM_THRESHOLD = 1 const ANIMATION_FRAME_INTERVAL_MS = 16 // ~60fps const DEFAULT_SCROLL_ANIMATION_DURATION_MS = 200 +// Page scroll amount (fraction of viewport height) +const PAGE_SCROLL_FRACTION = 0.8 + // Delay before auto-scrolling after content changes const AUTO_SCROLL_DELAY_MS = 50 @@ -86,6 +89,30 @@ export const useChatScrollbox = ( animateScrollTo(maxScroll) }, [scrollRef, animateScrollTo]) + const scrollUp = useCallback((): void => { + const scrollbox = scrollRef.current + if (!scrollbox) return + + const viewportHeight = scrollbox.viewport.height + const scrollAmount = Math.floor(viewportHeight * PAGE_SCROLL_FRACTION) + const targetScroll = Math.max(0, scrollbox.scrollTop - scrollAmount) + animateScrollTo(targetScroll) + }, [scrollRef, animateScrollTo]) + + const scrollDown = useCallback((): void => { + const scrollbox = scrollRef.current + if (!scrollbox) return + + const viewportHeight = scrollbox.viewport.height + const maxScroll = Math.max( + 0, + scrollbox.scrollHeight - viewportHeight, + ) + const scrollAmount = Math.floor(viewportHeight * PAGE_SCROLL_FRACTION) + const targetScroll = Math.min(maxScroll, scrollbox.scrollTop + scrollAmount) + animateScrollTo(targetScroll) + }, [scrollRef, animateScrollTo]) + useEffect(() => { const scrollbox = scrollRef.current if (!scrollbox) return @@ -149,6 +176,8 @@ export const useChatScrollbox = ( return { scrollToLatest, + scrollUp, + scrollDown, scrollboxProps: {}, isAtBottom, } diff --git a/cli/src/utils/keyboard-actions.ts b/cli/src/utils/keyboard-actions.ts index 452f77aee..52f986983 100644 --- a/cli/src/utils/keyboard-actions.ts +++ b/cli/src/utils/keyboard-actions.ts @@ -93,6 +93,10 @@ export type ChatKeyboardAction = | { type: 'bash-history-up' } | { type: 'bash-history-down' } + // Scroll actions + | { type: 'scroll-up' } + | { type: 'scroll-down' } + // Paste action (dispatcher checks clipboard content to route to image or text handler) | { type: 'paste' } @@ -126,6 +130,8 @@ export function resolveChatKeyboardAction( (key.name === 'return' || key.name === 'enter') && !key.shift && !hasModifier(key) + const isPageUp = key.name === 'pageup' && !hasModifier(key) + const isPageDown = key.name === 'pagedown' && !hasModifier(key) // Priority 0: Out of credits mode - Enter opens buy credits page if (state.inputMode === 'outOfCredits') { @@ -312,12 +318,20 @@ export function resolveChatKeyboardAction( return { type: 'unfocus-agent' } } - // Priority 13: Paste (ctrl-v) + // Priority 13: Scroll with PageUp/PageDown + if (isPageUp) { + return { type: 'scroll-up' } + } + if (isPageDown) { + return { type: 'scroll-down' } + } + + // Priority 14: Paste (ctrl-v) if (isCtrlV) { return { type: 'paste' } } - // Priority 14: Exit app (ctrl-c double-tap) + // Priority 15: Exit app (ctrl-c double-tap) if (isCtrlC) { if (state.nextCtrlCWillExit) { return { type: 'exit-app' }