From 88638c14f797723bd5025b3e107486b03f7462c9 Mon Sep 17 00:00:00 2001 From: Nick Salerni Date: Wed, 11 Mar 2026 00:16:25 -0700 Subject: [PATCH] fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) (#19933) Co-authored-by: Jacob Richman --- packages/cli/src/ui/AppContainer.test.tsx | 57 ++++++++++++++++++- packages/cli/src/ui/AppContainer.tsx | 13 ++++- .../ui/components/CopyModeWarning.test.tsx | 3 +- .../cli/src/ui/components/CopyModeWarning.tsx | 3 +- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 355195d90d..13550d3f42 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -2770,7 +2770,7 @@ describe('AppContainer State Management', () => { unmount(); }); - it('should exit copy mode on any key press', async () => { + it('should exit copy mode on non-scroll key press', async () => { await setupCopyModeTest(isAlternateMode); // Enter copy mode @@ -2792,6 +2792,61 @@ describe('AppContainer State Management', () => { unmount(); }); + it('should not exit copy mode on PageDown and should pass it through', async () => { + const childHandler = vi.fn().mockReturnValue(false); + await setupCopyModeTest(true, childHandler); + + // Enter copy mode + act(() => { + stdin.write('\x13'); // Ctrl+S + }); + rerender(); + expect(disableMouseEvents).toHaveBeenCalled(); + + childHandler.mockClear(); + (enableMouseEvents as Mock).mockClear(); + + // PageDown should be passed through to lower-priority handlers. + act(() => { + stdin.write('\x1b[6~'); + }); + rerender(); + + expect(enableMouseEvents).not.toHaveBeenCalled(); + expect(childHandler).toHaveBeenCalled(); + expect(childHandler).toHaveBeenCalledWith( + expect.objectContaining({ name: 'pagedown' }), + ); + unmount(); + }); + + it('should not exit copy mode on Shift+Down and should pass it through', async () => { + const childHandler = vi.fn().mockReturnValue(false); + await setupCopyModeTest(true, childHandler); + + // Enter copy mode + act(() => { + stdin.write('\x13'); // Ctrl+S + }); + rerender(); + expect(disableMouseEvents).toHaveBeenCalled(); + + childHandler.mockClear(); + (enableMouseEvents as Mock).mockClear(); + + act(() => { + stdin.write('\x1b[1;2B'); // Shift+Down + }); + rerender(); + + expect(enableMouseEvents).not.toHaveBeenCalled(); + expect(childHandler).toHaveBeenCalled(); + expect(childHandler).toHaveBeenCalledWith( + expect.objectContaining({ name: 'down', shift: true }), + ); + unmount(); + }); + it('should have higher priority than other priority listeners when enabled', async () => { // 1. Initial state with a child component's priority listener (already subscribed) // It should NOT handle Ctrl+S so we can enter copy mode. diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 2e5e4554dd..03e001546b 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1864,7 +1864,18 @@ Logging in with Google... Restarting Gemini CLI to continue. useKeypress(handleGlobalKeypress, { isActive: true, priority: true }); useKeypress( - () => { + (key: Key) => { + if ( + keyMatchers[Command.SCROLL_UP](key) || + keyMatchers[Command.SCROLL_DOWN](key) || + keyMatchers[Command.PAGE_UP](key) || + keyMatchers[Command.PAGE_DOWN](key) || + keyMatchers[Command.SCROLL_HOME](key) || + keyMatchers[Command.SCROLL_END](key) + ) { + return false; + } + setCopyModeEnabled(false); enableMouseEvents(); return true; diff --git a/packages/cli/src/ui/components/CopyModeWarning.test.tsx b/packages/cli/src/ui/components/CopyModeWarning.test.tsx index de7cb3a888..6f202ced4a 100644 --- a/packages/cli/src/ui/components/CopyModeWarning.test.tsx +++ b/packages/cli/src/ui/components/CopyModeWarning.test.tsx @@ -35,7 +35,8 @@ describe('CopyModeWarning', () => { const { lastFrame, waitUntilReady, unmount } = render(); await waitUntilReady(); expect(lastFrame()).toContain('In Copy Mode'); - expect(lastFrame()).toContain('Press any key to exit'); + expect(lastFrame()).toContain('Use Page Up/Down to scroll'); + expect(lastFrame()).toContain('Press Ctrl+S or any other key to exit'); unmount(); }); }); diff --git a/packages/cli/src/ui/components/CopyModeWarning.tsx b/packages/cli/src/ui/components/CopyModeWarning.tsx index 8d5423bb89..4b6328274b 100644 --- a/packages/cli/src/ui/components/CopyModeWarning.tsx +++ b/packages/cli/src/ui/components/CopyModeWarning.tsx @@ -19,7 +19,8 @@ export const CopyModeWarning: React.FC = () => { return ( - In Copy Mode. Press any key to exit. + In Copy Mode. Use Page Up/Down to scroll. Press Ctrl+S or any other key + to exit. );