mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 19:44:30 -07:00
fix(cli): dismiss '?' shortcuts help on hotkeys and active states (#18583)
Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
@@ -189,6 +189,7 @@ const createMockUIActions = (): UIActions =>
|
||||
setShellModeActive: vi.fn(),
|
||||
onEscapePromptChange: vi.fn(),
|
||||
vimHandleInput: vi.fn(),
|
||||
setShortcutsHelpVisible: vi.fn(),
|
||||
}) as Partial<UIActions> as UIActions;
|
||||
|
||||
const createMockConfig = (overrides = {}): Config =>
|
||||
@@ -337,7 +338,7 @@ describe('Composer', () => {
|
||||
expect(output).toContain('LoadingIndicator: Thinking ...');
|
||||
});
|
||||
|
||||
it('keeps shortcuts hint visible while loading', () => {
|
||||
it('hides shortcuts hint while loading', () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
elapsedTime: 1,
|
||||
@@ -347,7 +348,7 @@ describe('Composer', () => {
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).toContain('ShortcutsHint');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('renders LoadingIndicator without thought when accessibility disables loading phrases', () => {
|
||||
@@ -686,4 +687,43 @@ describe('Composer', () => {
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shortcuts Help', () => {
|
||||
it('shows shortcuts help in passive state', () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
streamingState: StreamingState.Idle,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHelp');
|
||||
});
|
||||
|
||||
it('hides shortcuts help while streaming', () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
});
|
||||
|
||||
it('hides shortcuts help when action is required', () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
customDialog: (
|
||||
<Box>
|
||||
<Text>Dialog content</Text>
|
||||
</Box>
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, useIsScreenReaderEnabled } from 'ink';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { StatusDisplay } from './StatusDisplay.js';
|
||||
@@ -28,7 +28,11 @@ import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { StreamingState, ToolCallStatus } from '../types.js';
|
||||
import {
|
||||
StreamingState,
|
||||
type HistoryItemToolGroup,
|
||||
ToolCallStatus,
|
||||
} from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { TodoTray } from './messages/Todo.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
@@ -51,11 +55,19 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
||||
const hideContextSummary =
|
||||
suggestionsVisible && suggestionsPosition === 'above';
|
||||
const hasPendingToolConfirmation = (uiState.pendingHistoryItems ?? []).some(
|
||||
(item) =>
|
||||
item.type === 'tool_group' &&
|
||||
item.tools.some((tool) => tool.status === ToolCallStatus.Confirming),
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() =>
|
||||
(uiState.pendingHistoryItems ?? [])
|
||||
.filter(
|
||||
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
|
||||
)
|
||||
.some((item) =>
|
||||
item.tools.some((tool) => tool.status === ToolCallStatus.Confirming),
|
||||
),
|
||||
[uiState.pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasPendingActionRequired =
|
||||
hasPendingToolConfirmation ||
|
||||
Boolean(uiState.commandConfirmationRequest) ||
|
||||
@@ -65,6 +77,31 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
Boolean(uiState.quota.proQuotaRequest) ||
|
||||
Boolean(uiState.quota.validationRequest) ||
|
||||
Boolean(uiState.customDialog);
|
||||
const isPassiveShortcutsHelpState =
|
||||
uiState.isInputActive &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
const { setShortcutsHelpVisible } = uiActions;
|
||||
|
||||
useEffect(() => {
|
||||
if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) {
|
||||
setShortcutsHelpVisible(false);
|
||||
}
|
||||
}, [
|
||||
uiState.shortcutsHelpVisible,
|
||||
isPassiveShortcutsHelpState,
|
||||
setShortcutsHelpVisible,
|
||||
]);
|
||||
|
||||
const showShortcutsHelp =
|
||||
uiState.shortcutsHelpVisible &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
const showShortcutsHint =
|
||||
settings.merged.ui.showShortcutsHint &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
const hasToast = shouldShowToast(uiState);
|
||||
const showLoadingIndicator =
|
||||
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
||||
@@ -133,11 +170,10 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
flexDirection="column"
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
>
|
||||
{settings.merged.ui.showShortcutsHint &&
|
||||
!hasPendingActionRequired && <ShortcutsHint />}
|
||||
{showShortcutsHint && <ShortcutsHint />}
|
||||
</Box>
|
||||
</Box>
|
||||
{uiState.shortcutsHelpVisible && <ShortcutsHelp />}
|
||||
{showShortcutsHelp && <ShortcutsHelp />}
|
||||
<HorizontalLine />
|
||||
<Box
|
||||
justifyContent={
|
||||
|
||||
@@ -4342,6 +4342,18 @@ describe('InputPrompt', () => {
|
||||
vi.mocked(clipboardy.read).mockResolvedValue('clipboard text');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Ctrl+R hotkey is pressed',
|
||||
input: '\x12',
|
||||
},
|
||||
{
|
||||
name: 'Ctrl+X hotkey is pressed',
|
||||
input: '\x18',
|
||||
},
|
||||
{
|
||||
name: 'F12 hotkey is pressed',
|
||||
input: '\x1b[24~',
|
||||
},
|
||||
])(
|
||||
'should close shortcuts help when a $name',
|
||||
async ({ input, setupMocks, mouseEventsEnabled }) => {
|
||||
|
||||
@@ -75,6 +75,7 @@ import { useMouseClick } from '../hooks/useMouseClick.js';
|
||||
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { shouldDismissShortcutsHelpOnHotkey } from '../utils/shortcutsHelp.js';
|
||||
|
||||
/**
|
||||
* Returns if the terminal can be trusted to handle paste events atomically
|
||||
@@ -661,6 +662,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
|
||||
setShortcutsHelpVisible(false);
|
||||
}
|
||||
|
||||
if (shortcutsHelpVisible) {
|
||||
if (key.sequence === '?' && key.insertable) {
|
||||
setShortcutsHelpVisible(false);
|
||||
|
||||
Reference in New Issue
Block a user