fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) (#19933)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Nick Salerni
2026-03-11 00:16:25 -07:00
committed by GitHub
parent c2d38bac54
commit 88638c14f7
4 changed files with 72 additions and 4 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -35,7 +35,8 @@ describe('CopyModeWarning', () => {
const { lastFrame, waitUntilReady, unmount } = render(<CopyModeWarning />);
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();
});
});

View File

@@ -19,7 +19,8 @@ export const CopyModeWarning: React.FC = () => {
return (
<Box>
<Text color={theme.status.warning}>
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.
</Text>
</Box>
);