From 5a49437ceab57584703832e07aa76c35b6ee0968 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Fri, 3 Apr 2026 15:10:04 -0700 Subject: [PATCH] feat(cli) Scrollbar for input prompt (#21992) --- packages/cli/src/test-utils/render.tsx | 2 + packages/cli/src/ui/AppContainer.tsx | 9 ++- .../ui/components/CopyModeWarning.test.tsx | 70 +++++++++++++++++-- .../cli/src/ui/components/CopyModeWarning.tsx | 10 ++- .../ui/components/shared/ScrollableList.tsx | 1 - .../shared/VirtualizedList.test.tsx | 5 +- .../ui/components/shared/VirtualizedList.tsx | 15 ++-- 7 files changed, 89 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index bf8ca468eb..1597fbceec 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -515,6 +515,8 @@ const baseMockUiState = { activePtyId: undefined, backgroundTasks: new Map(), backgroundTaskHeight: 0, + copyModeEnabled: false, + mouseMode: true, quota: { userTier: undefined, stats: undefined, diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index a0d995f323..88851f7980 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -2014,6 +2014,8 @@ Logging in with Google... Restarting Gemini CLI to continue. useKeypress(handleGlobalKeypress, { isActive: true, priority: true }); + const isSelectionMode = isAlternateBuffer && !mouseMode; + useKeypress( (key: Key) => { if ( @@ -2028,13 +2030,16 @@ Logging in with Google... Restarting Gemini CLI to continue. } setCopyModeEnabled(false); - if (mouseMode) { + + if (isSelectionMode) { + setMouseMode(true); + } else if (mouseMode) { enableMouseEvents(); } return true; }, { - isActive: copyModeEnabled, + isActive: copyModeEnabled || isSelectionMode, // We need to receive keypresses first so they do not bubble to other // handlers. priority: KeypressPriority.Critical, diff --git a/packages/cli/src/ui/components/CopyModeWarning.test.tsx b/packages/cli/src/ui/components/CopyModeWarning.test.tsx index c1b797ffd5..f7be1bad73 100644 --- a/packages/cli/src/ui/components/CopyModeWarning.test.tsx +++ b/packages/cli/src/ui/components/CopyModeWarning.test.tsx @@ -8,18 +8,56 @@ import { CopyModeWarning } from './CopyModeWarning.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderWithProviders } from '../../test-utils/render.js'; import { useInputState } from '../contexts/InputContext.js'; +import { useUIState, type UIState } from '../contexts/UIStateContext.js'; +import { useConfig } from '../contexts/ConfigContext.js'; +import type { Config } from '@google/gemini-cli-core'; vi.mock('../contexts/InputContext.js'); +vi.mock('../contexts/UIStateContext.js'); +vi.mock('../contexts/ConfigContext.js'); describe('CopyModeWarning', () => { + const mockUseUIState = vi.mocked(useUIState); + const mockUseConfig = vi.mocked(useConfig); + const mockUseInputState = vi.mocked(useInputState); + beforeEach(() => { vi.clearAllMocks(); - }); - - it('renders nothing when copy mode is disabled', async () => { - vi.mocked(useInputState).mockReturnValue({ + mockUseConfig.mockReturnValue({ + getUseAlternateBuffer: () => false, + } as unknown as Config); + mockUseInputState.mockReturnValue({ copyModeEnabled: false, } as unknown as ReturnType); + mockUseUIState.mockReturnValue({ + mouseMode: true, + } as unknown as UIState); + }); + + it('renders nothing when copy mode is disabled and not in alternate buffer', async () => { + mockUseInputState.mockReturnValue({ + copyModeEnabled: false, + } as unknown as ReturnType); + mockUseUIState.mockReturnValue({ + mouseMode: true, + } as unknown as UIState); + const { lastFrame, unmount } = await renderWithProviders( + , + ); + expect(lastFrame({ allowEmpty: true })).toBe(''); + unmount(); + }); + + it('renders nothing when copy mode is disabled and mouse mode is disabled but not in alternate buffer', async () => { + mockUseInputState.mockReturnValue({ + copyModeEnabled: false, + } as unknown as ReturnType); + mockUseUIState.mockReturnValue({ + mouseMode: false, + } as unknown as UIState); + mockUseConfig.mockReturnValue({ + getUseAlternateBuffer: () => false, + } as unknown as Config); const { lastFrame, unmount } = await renderWithProviders( , ); @@ -28,9 +66,31 @@ describe('CopyModeWarning', () => { }); it('renders warning when copy mode is enabled', async () => { - vi.mocked(useInputState).mockReturnValue({ + mockUseInputState.mockReturnValue({ copyModeEnabled: true, } as unknown as ReturnType); + mockUseUIState.mockReturnValue({ + mouseMode: true, + } as unknown as UIState); + const { lastFrame, unmount } = await renderWithProviders( + , + ); + expect(lastFrame()).toContain('In Copy Mode'); + expect(lastFrame()).toContain('Use Page Up/Down to scroll'); + expect(lastFrame()).toContain('Press Ctrl+S or any other key to exit'); + unmount(); + }); + + it('renders warning when in alternate buffer and mouse mode is disabled', async () => { + mockUseInputState.mockReturnValue({ + copyModeEnabled: false, + } as unknown as ReturnType); + mockUseUIState.mockReturnValue({ + mouseMode: false, + } as unknown as UIState); + mockUseConfig.mockReturnValue({ + getUseAlternateBuffer: () => true, + } as unknown as Config); const { lastFrame, unmount } = await renderWithProviders( , ); diff --git a/packages/cli/src/ui/components/CopyModeWarning.tsx b/packages/cli/src/ui/components/CopyModeWarning.tsx index 2eec1b62ae..546497a2e4 100644 --- a/packages/cli/src/ui/components/CopyModeWarning.tsx +++ b/packages/cli/src/ui/components/CopyModeWarning.tsx @@ -7,14 +7,22 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { useInputState } from '../contexts/InputContext.js'; +import { useUIState } from '../contexts/UIStateContext.js'; +import { useConfig } from '../contexts/ConfigContext.js'; import { theme } from '../semantic-colors.js'; export const CopyModeWarning: React.FC = () => { const { copyModeEnabled } = useInputState(); + const { mouseMode } = useUIState(); + const config = useConfig(); + const isTrueAlternateBuffer = config.getUseAlternateBuffer(); + + const isSelectionMode = isTrueAlternateBuffer && !mouseMode; + const showWarning = copyModeEnabled || isSelectionMode; return ( - {copyModeEnabled && ( + {showWarning && ( In Copy Mode. Use Page Up/Down to scroll. Press Ctrl+S or any other key to exit. diff --git a/packages/cli/src/ui/components/shared/ScrollableList.tsx b/packages/cli/src/ui/components/shared/ScrollableList.tsx index c857e97b70..8a020df0dc 100644 --- a/packages/cli/src/ui/components/shared/ScrollableList.tsx +++ b/packages/cli/src/ui/components/shared/ScrollableList.tsx @@ -33,7 +33,6 @@ interface ScrollableListProps extends VirtualizedListProps { width?: string | number; scrollbar?: boolean; stableScrollback?: boolean; - copyModeEnabled?: boolean; isStatic?: boolean; fixedItemHeight?: boolean; targetScrollIndex?: number; diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx index 98e7790538..2f58acabc0 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx @@ -316,9 +316,8 @@ describe('', () => { unmount(); }); - it('renders correctly in copyModeEnabled when scrolled', async () => { + it('renders correctly with scrollbar={false} when scrolled', async () => { const longData = Array.from({ length: 100 }, (_, i) => `Item ${i}`); - // Use copy mode const { lastFrame, unmount } = await render( ', () => { keyExtractor={(item) => item} estimatedItemHeight={() => 1} initialScrollIndex={50} - copyModeEnabled={true} + scrollbar={false} /> , ); diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index c3f888ba5f..9024c7767a 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -39,7 +39,6 @@ export type VirtualizedListProps = { overflowToBackbuffer?: boolean; scrollbar?: boolean; stableScrollback?: boolean; - copyModeEnabled?: boolean; fixedItemHeight?: boolean; containerHeight?: number; }; @@ -144,7 +143,6 @@ function VirtualizedList( overflowToBackbuffer, scrollbar = true, stableScrollback, - copyModeEnabled = false, fixedItemHeight = false, } = props; const dataRef = useRef(data); @@ -727,25 +725,20 @@ function VirtualizedList( return ( - + {renderedItems}