mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 06:12:50 -07:00
feat(cli) Scrollbar for input prompt (#21992)
This commit is contained in:
@@ -515,6 +515,8 @@ const baseMockUiState = {
|
||||
activePtyId: undefined,
|
||||
backgroundTasks: new Map(),
|
||||
backgroundTaskHeight: 0,
|
||||
copyModeEnabled: false,
|
||||
mouseMode: true,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof useInputState>);
|
||||
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<typeof useInputState>);
|
||||
mockUseUIState.mockReturnValue({
|
||||
mouseMode: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<CopyModeWarning />,
|
||||
);
|
||||
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<typeof useInputState>);
|
||||
mockUseUIState.mockReturnValue({
|
||||
mouseMode: false,
|
||||
} as unknown as UIState);
|
||||
mockUseConfig.mockReturnValue({
|
||||
getUseAlternateBuffer: () => false,
|
||||
} as unknown as Config);
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<CopyModeWarning />,
|
||||
);
|
||||
@@ -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<typeof useInputState>);
|
||||
mockUseUIState.mockReturnValue({
|
||||
mouseMode: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<CopyModeWarning />,
|
||||
);
|
||||
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<typeof useInputState>);
|
||||
mockUseUIState.mockReturnValue({
|
||||
mouseMode: false,
|
||||
} as unknown as UIState);
|
||||
mockUseConfig.mockReturnValue({
|
||||
getUseAlternateBuffer: () => true,
|
||||
} as unknown as Config);
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<CopyModeWarning />,
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Box height={1}>
|
||||
{copyModeEnabled && (
|
||||
{showWarning && (
|
||||
<Text color={theme.status.warning}>
|
||||
In Copy Mode. Use Page Up/Down to scroll. Press Ctrl+S or any other
|
||||
key to exit.
|
||||
|
||||
@@ -33,7 +33,6 @@ interface ScrollableListProps<T> extends VirtualizedListProps<T> {
|
||||
width?: string | number;
|
||||
scrollbar?: boolean;
|
||||
stableScrollback?: boolean;
|
||||
copyModeEnabled?: boolean;
|
||||
isStatic?: boolean;
|
||||
fixedItemHeight?: boolean;
|
||||
targetScrollIndex?: number;
|
||||
|
||||
@@ -316,9 +316,8 @@ describe('<VirtualizedList />', () => {
|
||||
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(
|
||||
<Box height={10} width={100}>
|
||||
<VirtualizedList
|
||||
@@ -331,7 +330,7 @@ describe('<VirtualizedList />', () => {
|
||||
keyExtractor={(item) => item}
|
||||
estimatedItemHeight={() => 1}
|
||||
initialScrollIndex={50}
|
||||
copyModeEnabled={true}
|
||||
scrollbar={false}
|
||||
/>
|
||||
</Box>,
|
||||
);
|
||||
|
||||
@@ -39,7 +39,6 @@ export type VirtualizedListProps<T> = {
|
||||
overflowToBackbuffer?: boolean;
|
||||
scrollbar?: boolean;
|
||||
stableScrollback?: boolean;
|
||||
copyModeEnabled?: boolean;
|
||||
fixedItemHeight?: boolean;
|
||||
containerHeight?: number;
|
||||
};
|
||||
@@ -144,7 +143,6 @@ function VirtualizedList<T>(
|
||||
overflowToBackbuffer,
|
||||
scrollbar = true,
|
||||
stableScrollback,
|
||||
copyModeEnabled = false,
|
||||
fixedItemHeight = false,
|
||||
} = props;
|
||||
const dataRef = useRef(data);
|
||||
@@ -727,25 +725,20 @@ function VirtualizedList<T>(
|
||||
return (
|
||||
<Box
|
||||
ref={containerRefCallback}
|
||||
overflowY={copyModeEnabled ? 'hidden' : 'scroll'}
|
||||
overflowY="scroll"
|
||||
overflowX="hidden"
|
||||
scrollTop={copyModeEnabled ? 0 : scrollTop}
|
||||
scrollTop={scrollTop}
|
||||
scrollbarThumbColor={props.scrollbarThumbColor ?? theme.text.secondary}
|
||||
backgroundColor={props.backgroundColor}
|
||||
width="100%"
|
||||
height="100%"
|
||||
flexDirection="column"
|
||||
paddingRight={copyModeEnabled ? 0 : 1}
|
||||
paddingRight={1}
|
||||
overflowToBackbuffer={overflowToBackbuffer}
|
||||
scrollbar={scrollbar}
|
||||
stableScrollback={stableScrollback}
|
||||
>
|
||||
<Box
|
||||
flexShrink={0}
|
||||
width="100%"
|
||||
flexDirection="column"
|
||||
marginTop={copyModeEnabled ? -actualScrollTop : 0}
|
||||
>
|
||||
<Box flexShrink={0} width="100%" flexDirection="column">
|
||||
<Box height={topSpacerHeight} flexShrink={0} />
|
||||
{renderedItems}
|
||||
<Box height={bottomSpacerHeight} flexShrink={0} />
|
||||
|
||||
Reference in New Issue
Block a user