fix(cli): dismiss '?' shortcuts help on hotkeys and active states (#18583)

Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
Dmitry Lyalin
2026-02-12 11:35:40 -05:00
committed by GitHub
parent 2ca183ffc9
commit f603f4a12b
8 changed files with 280 additions and 15 deletions
@@ -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');
});
});
});
+45 -9
View File
@@ -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);