mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
refactor: migrate to useKeyMatchers hook (#21753)
This commit is contained in:
committed by
GitHub
parent
e406dcc249
commit
ab64b15d51
@@ -119,7 +119,7 @@ import { type InitializationResult } from '../core/initializer.js';
|
||||
import { useFocus } from './hooks/useFocus.js';
|
||||
import { useKeypress, type Key } from './hooks/useKeypress.js';
|
||||
import { KeypressPriority } from './contexts/KeypressContext.js';
|
||||
import { keyMatchers, Command } from './keyMatchers.js';
|
||||
import { Command } from './keyMatchers.js';
|
||||
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
||||
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
|
||||
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||
@@ -164,7 +164,7 @@ import { NewAgentsChoice } from './components/NewAgentsNotification.js';
|
||||
import { isSlashCommand } from './utils/commandUtils.js';
|
||||
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
|
||||
import { useTimedMessage } from './hooks/useTimedMessage.js';
|
||||
import { shouldDismissShortcutsHelpOnHotkey } from './utils/shortcutsHelp.js';
|
||||
import { useIsHelpDismissKey } from './utils/shortcutsHelp.js';
|
||||
import { useSuspend } from './hooks/useSuspend.js';
|
||||
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
|
||||
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
|
||||
@@ -205,6 +205,7 @@ import {
|
||||
useVisibilityToggle,
|
||||
APPROVAL_MODE_REVEAL_DURATION_MS,
|
||||
} from './hooks/useVisibilityToggle.js';
|
||||
import { useKeyMatchers } from './hooks/useKeyMatchers.js';
|
||||
|
||||
/**
|
||||
* The fraction of the terminal width to allocate to the shell.
|
||||
@@ -219,6 +220,8 @@ const SHELL_WIDTH_FRACTION = 0.89;
|
||||
const SHELL_HEIGHT_PADDING = 10;
|
||||
|
||||
export const AppContainer = (props: AppContainerProps) => {
|
||||
const isHelpDismissKey = useIsHelpDismissKey();
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { config, initializationResult, resumedSessionData } = props;
|
||||
const settings = useSettings();
|
||||
const { reset } = useOverflowActions()!;
|
||||
@@ -1654,7 +1657,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
||||
}
|
||||
|
||||
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
|
||||
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
|
||||
setShortcutsHelpVisible(false);
|
||||
}
|
||||
|
||||
@@ -1848,6 +1851,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
settings.merged.general.devtools,
|
||||
showErrorDetails,
|
||||
triggerExpandHint,
|
||||
keyMatchers,
|
||||
isHelpDismissKey,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ import { useTextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { clearApiKey, debugLogger } from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
interface ApiAuthDialogProps {
|
||||
onSubmit: (apiKey: string) => void;
|
||||
@@ -28,6 +29,7 @@ export function ApiAuthDialog({
|
||||
error,
|
||||
defaultValue = '',
|
||||
}: ApiAuthDialogProps): React.JSX.Element {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { terminalWidth } = useUIState();
|
||||
const viewportWidth = terminalWidth - 8;
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
export const AdminSettingsChangedDialog = () => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { handleRestart } = useUIActions();
|
||||
|
||||
useKeypress(
|
||||
|
||||
@@ -20,7 +20,7 @@ import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
||||
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||
import { TabHeader, type Tab } from './shared/TabHeader.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { checkExhaustive } from '@google/gemini-cli-core';
|
||||
import { TextInput } from './shared/TextInput.js';
|
||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||
@@ -36,6 +36,7 @@ import { RenderInline } from '../utils/InlineMarkdownRenderer.js';
|
||||
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
||||
import { UIStateContext } from '../contexts/UIStateContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
/** Padding for dialog content to prevent text from touching edges. */
|
||||
const DIALOG_PADDING = 4;
|
||||
@@ -208,6 +209,7 @@ const ReviewView: React.FC<ReviewViewProps> = ({
|
||||
progressHeader,
|
||||
extraParts,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const unansweredCount = questions.length - Object.keys(answers).length;
|
||||
const hasUnanswered = unansweredCount > 0;
|
||||
|
||||
@@ -288,6 +290,7 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
||||
progressHeader,
|
||||
keyboardHints,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const prefix = '> ';
|
||||
const horizontalPadding = 1; // 1 for cursor
|
||||
@@ -325,7 +328,7 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[buffer, textValue],
|
||||
[buffer, textValue, keyMatchers],
|
||||
);
|
||||
|
||||
useKeypress(handleExtraKeys, { isActive: true, priority: true });
|
||||
@@ -487,6 +490,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
progressHeader,
|
||||
keyboardHints,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const numOptions =
|
||||
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
||||
@@ -680,6 +684,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
customBuffer,
|
||||
onEditingCustomOption,
|
||||
customOptionText,
|
||||
keyMatchers,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -950,6 +955,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
availableHeight: availableHeightProp,
|
||||
extraParts,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const uiState = useContext(UIStateContext);
|
||||
const availableHeight =
|
||||
availableHeightProp ??
|
||||
@@ -999,7 +1005,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[onCancel, submitted, isEditingCustomOption],
|
||||
[onCancel, submitted, isEditingCustomOption, keyMatchers],
|
||||
);
|
||||
|
||||
useKeypress(handleCancel, {
|
||||
@@ -1032,7 +1038,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[questions.length, submitted, goToNextTab, goToPrevTab],
|
||||
[questions.length, submitted, goToNextTab, goToPrevTab, keyMatchers],
|
||||
);
|
||||
|
||||
useKeypress(handleNavigation, {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { cpLen, cpSlice, getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
interface BackgroundShellDisplayProps {
|
||||
shells: Map<number, BackgroundShell>;
|
||||
@@ -60,6 +61,7 @@ export const BackgroundShellDisplay = ({
|
||||
isFocused,
|
||||
isListOpenProp,
|
||||
}: BackgroundShellDisplayProps) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const {
|
||||
dismissBackgroundShell,
|
||||
setActiveBackgroundShellPid,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { ExitPlanModeDialog } from './ExitPlanModeDialog.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
validatePlanContent,
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type FileSystemService,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
vi.mock('../utils/editorUtils.js', () => ({
|
||||
openFileInEditor: vi.fn(),
|
||||
@@ -402,6 +403,7 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (keyMatchers[Command.QUIT](key)) {
|
||||
|
||||
@@ -22,8 +22,9 @@ import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { AskUserDialog } from './AskUserDialog.js';
|
||||
import { openFileInEditor } from '../utils/editorUtils.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
export interface ExitPlanModeDialogProps {
|
||||
planPath: string;
|
||||
@@ -147,6 +148,7 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
|
||||
width,
|
||||
availableHeight,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const config = useConfig();
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const planState = usePlanContent(planPath, config);
|
||||
|
||||
@@ -11,13 +11,14 @@ import { theme } from '../semantic-colors.js';
|
||||
import { useSettingsStore } from '../contexts/SettingsContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { FooterRow, type FooterRowItem } from './Footer.js';
|
||||
import { ALL_ITEMS, resolveFooterState } from '../../config/footerItems.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
||||
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||
import { DialogFooter } from './shared/DialogFooter.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
interface FooterConfigDialogProps {
|
||||
onClose?: () => void;
|
||||
@@ -82,6 +83,7 @@ function footerConfigReducer(
|
||||
export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { settings, setSetting } = useSettingsStore();
|
||||
const { constrainHeight, terminalHeight, staticExtraHeight } = useUIState();
|
||||
const [state, dispatch] = useReducer(footerConfigReducer, undefined, () =>
|
||||
|
||||
@@ -9,7 +9,8 @@ import { useState, useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
/**
|
||||
* Hook entry type matching HookRegistryEntry from core
|
||||
@@ -49,6 +50,7 @@ export const HooksDialog: React.FC<HooksDialogProps> = ({
|
||||
onClose,
|
||||
maxVisibleHooks = DEFAULT_MAX_VISIBLE_HOOKS,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
|
||||
// Flatten hooks with their event names for easier scrolling
|
||||
|
||||
@@ -44,7 +44,7 @@ import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js
|
||||
import type { UIState } from '../contexts/UIStateContext.js';
|
||||
import { isLowColorDepth } from '../utils/terminalUtils.js';
|
||||
import { cpLen } from '../utils/textUtils.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { defaultKeyMatchers, Command } from '../keyMatchers.js';
|
||||
import type { Key } from '../hooks/useKeypress.js';
|
||||
import {
|
||||
appEvents,
|
||||
@@ -197,7 +197,7 @@ describe('InputPrompt', () => {
|
||||
visualCursor: [0, 0],
|
||||
visualScrollRow: 0,
|
||||
handleInput: vi.fn((key: Key) => {
|
||||
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
||||
if (defaultKeyMatchers[Command.CLEAR_INPUT](key)) {
|
||||
if (mockBuffer.text.length > 0) {
|
||||
mockBuffer.setText('');
|
||||
return true;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from '../hooks/useCommandCompletion.js';
|
||||
import type { Key } from '../hooks/useKeypress.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
@@ -72,8 +72,9 @@ 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';
|
||||
import { useIsHelpDismissKey } from '../utils/shortcutsHelp.js';
|
||||
import { useRepeatedKeyPress } from '../hooks/useRepeatedKeyPress.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
/**
|
||||
* Returns if the terminal can be trusted to handle paste events atomically
|
||||
@@ -207,6 +208,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
suggestionsPosition = 'below',
|
||||
setBannerVisible,
|
||||
}) => {
|
||||
const isHelpDismissKey = useIsHelpDismissKey();
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { stdout } = useStdout();
|
||||
const { merged: settings } = useSettings();
|
||||
const kittyProtocol = useKittyKeyboardProtocol();
|
||||
@@ -737,7 +740,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
|
||||
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
|
||||
setShortcutsHelpVisible(false);
|
||||
}
|
||||
|
||||
@@ -1265,6 +1268,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
shouldShowSuggestions,
|
||||
isShellSuggestionsVisible,
|
||||
forceShowShellSuggestions,
|
||||
keyMatchers,
|
||||
isHelpDismissKey,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import { theme } from '../semantic-colors.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
export enum PolicyUpdateChoice {
|
||||
ACCEPT = 'accept',
|
||||
@@ -34,6 +35,7 @@ export const PolicyUpdateDialog: React.FC<PolicyUpdateDialogProps> = ({
|
||||
request,
|
||||
onClose,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const isProcessing = useRef(false);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
|
||||
@@ -13,7 +13,8 @@ import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import type { FileChangeStats } from '../utils/rewindFileOps.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { formatTimeAgo } from '../utils/formatters.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
export enum RewindOutcome {
|
||||
RewindAndRevert = 'rewind_and_revert',
|
||||
@@ -58,6 +59,7 @@ export const RewindConfirmation: React.FC<RewindConfirmationProps> = ({
|
||||
terminalWidth,
|
||||
timestamp,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
useKeypress(
|
||||
(key) => {
|
||||
|
||||
@@ -19,9 +19,10 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { useRewind } from '../hooks/useRewind.js';
|
||||
import { RewindConfirmation, RewindOutcome } from './RewindConfirmation.js';
|
||||
import { stripReferenceContent } from '../utils/formatters.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { CliSpinner } from './CliSpinner.js';
|
||||
import { ExpandableText } from './shared/ExpandableText.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
interface RewindViewerProps {
|
||||
conversation: ConversationRecord;
|
||||
@@ -48,6 +49,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
|
||||
onExit,
|
||||
onRewind,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [isRewinding, setIsRewinding] = useState(false);
|
||||
const { terminalWidth, terminalHeight } = useUIState();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
|
||||
@@ -10,7 +10,8 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||
import { keyToAnsi, type Key } from '../hooks/keyToAnsi.js';
|
||||
import { ACTIVE_SHELL_MAX_LINES } from '../constants.js';
|
||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
export interface ShellInputPromptProps {
|
||||
activeShellPtyId: number | null;
|
||||
@@ -23,6 +24,7 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
||||
focus = true,
|
||||
scrollPageSize = ACTIVE_SHELL_MAX_LINES,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const handleShellInputSubmit = useCallback(
|
||||
(input: string) => {
|
||||
if (activeShellPtyId) {
|
||||
@@ -73,7 +75,13 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
||||
|
||||
return false;
|
||||
},
|
||||
[focus, handleShellInputSubmit, activeShellPtyId, scrollPageSize],
|
||||
[
|
||||
focus,
|
||||
handleShellInputSubmit,
|
||||
activeShellPtyId,
|
||||
scrollPageSize,
|
||||
keyMatchers,
|
||||
],
|
||||
);
|
||||
|
||||
useKeypress(handleInput, { isActive: focus });
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
type ValidationIntent,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
interface ValidationDialogProps {
|
||||
validationLink?: string;
|
||||
@@ -32,6 +33,7 @@ export function ValidationDialog({
|
||||
learnMoreUrl,
|
||||
onChoice,
|
||||
}: ValidationDialogProps): React.JSX.Element {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [state, setState] = useState<DialogState>('choosing');
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||
import { AskUserDialog } from '../AskUserDialog.js';
|
||||
import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js';
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
toUnicodeUrl,
|
||||
type DeceptiveUrlDetails,
|
||||
} from '../../utils/urlSecurityUtils.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
callId: string;
|
||||
@@ -67,6 +68,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { confirm, isDiffingEnabled } = useToolActions();
|
||||
const [mcpDetailsExpansionState, setMcpDetailsExpansionState] = useState<{
|
||||
callId: string;
|
||||
|
||||
@@ -19,10 +19,11 @@ import { TextInput } from './TextInput.js';
|
||||
import type { TextBuffer } from './text-buffer.js';
|
||||
import { cpSlice, cpLen, cpIndexToOffset } from '../../utils/textUtils.js';
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { useSettingsNavigation } from '../../hooks/useSettingsNavigation.js';
|
||||
import { useInlineEditBuffer } from '../../hooks/useInlineEditBuffer.js';
|
||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
/**
|
||||
* Represents a single item in the settings dialog.
|
||||
@@ -136,6 +137,7 @@ export function BaseSettingsDialog({
|
||||
availableHeight,
|
||||
footer,
|
||||
}: BaseSettingsDialogProps): React.JSX.Element {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
// Calculate effective max items and scope visibility based on terminal height
|
||||
const { effectiveMaxItemsToShow, finalShowScopeSelector } = useMemo(() => {
|
||||
const initialShowScope = showScopeSelector;
|
||||
|
||||
@@ -19,8 +19,9 @@ import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
interface ScrollableProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -45,6 +46,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
flexGrow,
|
||||
reportOverflow = false,
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const viewportRef = useRef<DOMElement | null>(null);
|
||||
const contentRef = useRef<DOMElement | null>(null);
|
||||
|
||||
@@ -22,7 +22,8 @@ import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||
import { Box, type DOMElement } from 'ink';
|
||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
const ANIMATION_FRAME_DURATION_MS = 33;
|
||||
|
||||
@@ -46,6 +47,7 @@ function ScrollableList<T>(
|
||||
props: ScrollableListProps<T>,
|
||||
ref: React.Ref<ScrollableListRef<T>>,
|
||||
) {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { hasFocus, width } = props;
|
||||
const virtualizedListRef = useRef<VirtualizedListRef<T>>(null);
|
||||
const containerRef = useRef<DOMElement>(null);
|
||||
|
||||
@@ -11,7 +11,8 @@ import { useSelectionList } from '../../hooks/useSelectionList.js';
|
||||
import { TextInput } from './TextInput.js';
|
||||
import type { TextBuffer } from './text-buffer.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
/**
|
||||
* Generic interface for items in a searchable list.
|
||||
@@ -85,6 +86,7 @@ export function SearchableList<T extends GenericListItem>({
|
||||
onSearch,
|
||||
resetSelectionOnItemsChange = false,
|
||||
}: SearchableListProps<T>): React.JSX.Element {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { filteredItems, searchBuffer, maxLabelWidth } = useSearch({
|
||||
items,
|
||||
onSearch,
|
||||
|
||||
@@ -14,7 +14,8 @@ import { theme } from '../../semantic-colors.js';
|
||||
import type { TextBuffer } from './text-buffer.js';
|
||||
import { expandPastePlaceholders } from './text-buffer.js';
|
||||
import { cpSlice, cpIndexToOffset } from '../../utils/textUtils.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
export interface TextInputProps {
|
||||
buffer: TextBuffer;
|
||||
@@ -31,6 +32,7 @@ export function TextInput({
|
||||
onCancel,
|
||||
focus = true,
|
||||
}: TextInputProps): React.JSX.Element {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const {
|
||||
text,
|
||||
handleInput,
|
||||
@@ -55,7 +57,7 @@ export function TextInput({
|
||||
const handled = handleInput(key);
|
||||
return handled;
|
||||
},
|
||||
[handleInput, onCancel, onSubmit, text, buffer.pastedContent],
|
||||
[handleInput, onCancel, onSubmit, text, buffer.pastedContent, keyMatchers],
|
||||
);
|
||||
|
||||
useKeypress(handleKeyPress, { isActive: focus, priority: true });
|
||||
|
||||
@@ -25,11 +25,12 @@ import {
|
||||
} from '../../utils/textUtils.js';
|
||||
import { parsePastedPaths } from '../../utils/clipboardUtils.js';
|
||||
import type { Key } from '../../contexts/KeypressContext.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import type { VimAction } from './vim-buffer-actions.js';
|
||||
import { handleVimAction } from './vim-buffer-actions.js';
|
||||
import { LRU_BUFFER_PERF_CACHE_LIMIT } from '../../constants.js';
|
||||
import { openFileInEditor } from '../../utils/editorUtils.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
export const LARGE_PASTE_LINE_THRESHOLD = 5;
|
||||
export const LARGE_PASTE_CHAR_THRESHOLD = 500;
|
||||
@@ -2708,6 +2709,7 @@ export function useTextBuffer({
|
||||
singleLine = false,
|
||||
getPreferredEditor,
|
||||
}: UseTextBufferProps): TextBuffer {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const initialState = useMemo((): TextBufferState => {
|
||||
const lines = initialText.split('\n');
|
||||
const [initialCursorRow, initialCursorCol] = calculateInitialCursorPosition(
|
||||
@@ -3270,6 +3272,7 @@ export function useTextBuffer({
|
||||
text,
|
||||
visualCursor,
|
||||
visualLines,
|
||||
keyMatchers,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import Spinner from 'ink-spinner';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
interface Issue {
|
||||
number: number;
|
||||
@@ -106,6 +107,7 @@ export const TriageDuplicates = ({
|
||||
onExit: () => void;
|
||||
initialLimit?: number;
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [state, setState] = useState<TriageState>({
|
||||
status: 'loading',
|
||||
issues: [],
|
||||
|
||||
@@ -10,9 +10,10 @@ import Spinner from 'ink-spinner';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { Command } from '../../keyMatchers.js';
|
||||
import { TextInput } from '../shared/TextInput.js';
|
||||
import { useTextBuffer } from '../shared/text-buffer.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
|
||||
interface Issue {
|
||||
number: number;
|
||||
@@ -69,6 +70,7 @@ export const TriageIssues = ({
|
||||
initialLimit?: number;
|
||||
until?: string;
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [state, setState] = useState<TriageState>({
|
||||
status: 'loading',
|
||||
issues: [],
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
getAdminErrorMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||
import type { HistoryItemWithoutId } from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
@@ -30,6 +31,7 @@ export function useApprovalModeIndicator({
|
||||
isActive = true,
|
||||
allowPlanMode = false,
|
||||
}: UseApprovalModeIndicatorArgs): ApprovalMode {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const currentConfigValue = config.getApprovalMode();
|
||||
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
|
||||
|
||||
|
||||
17
packages/cli/src/ui/hooks/useKeyMatchers.ts
Normal file
17
packages/cli/src/ui/hooks/useKeyMatchers.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { KeyMatchers } from '../keyMatchers.js';
|
||||
import { defaultKeyMatchers } from '../keyMatchers.js';
|
||||
|
||||
/**
|
||||
* Hook to retrieve the currently active key matchers.
|
||||
* This prepares the codebase for dynamic or custom key bindings in the future.
|
||||
*/
|
||||
export function useKeyMatchers(): KeyMatchers {
|
||||
return useMemo(() => defaultKeyMatchers, []);
|
||||
}
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
import { useReducer, useRef, useEffect, useCallback } from 'react';
|
||||
import { useKeypress, type Key } from './useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||
|
||||
export interface SelectionListItem<T> {
|
||||
key: string;
|
||||
@@ -290,6 +291,7 @@ export function useSelectionList<T>({
|
||||
focusKey,
|
||||
priority,
|
||||
}: UseSelectionListOptions<T>): UseSelectionListResult {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const baseItems = toBaseItems(items);
|
||||
|
||||
const [state, dispatch] = useReducer(selectionListReducer, {
|
||||
@@ -460,7 +462,7 @@ export function useSelectionList<T>({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[dispatch, itemsLength, showNumbers],
|
||||
[dispatch, itemsLength, showNumbers, keyMatchers],
|
||||
);
|
||||
|
||||
useKeypress(handleKeypress, {
|
||||
|
||||
@@ -9,12 +9,18 @@ import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useTabbedNavigation } from './useTabbedNavigation.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||
import type { KeyMatchers } from '../keyMatchers.js';
|
||||
import type { Key, KeypressHandler } from '../contexts/KeypressContext.js';
|
||||
|
||||
vi.mock('./useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./useKeyMatchers.js', () => ({
|
||||
useKeyMatchers: vi.fn(),
|
||||
}));
|
||||
|
||||
const createKey = (partial: Partial<Key>): Key => ({
|
||||
name: partial.name || '',
|
||||
sequence: partial.sequence || '',
|
||||
@@ -26,13 +32,14 @@ const createKey = (partial: Partial<Key>): Key => ({
|
||||
...partial,
|
||||
});
|
||||
|
||||
const mockKeyMatchers = {
|
||||
'cursor.left': vi.fn((key) => key.name === 'left'),
|
||||
'cursor.right': vi.fn((key) => key.name === 'right'),
|
||||
'dialog.next': vi.fn((key) => key.name === 'tab' && !key.shift),
|
||||
'dialog.previous': vi.fn((key) => key.name === 'tab' && key.shift),
|
||||
} as unknown as KeyMatchers;
|
||||
|
||||
vi.mock('../keyMatchers.js', () => ({
|
||||
keyMatchers: {
|
||||
'cursor.left': vi.fn((key) => key.name === 'left'),
|
||||
'cursor.right': vi.fn((key) => key.name === 'right'),
|
||||
'dialog.next': vi.fn((key) => key.name === 'tab' && !key.shift),
|
||||
'dialog.previous': vi.fn((key) => key.name === 'tab' && key.shift),
|
||||
},
|
||||
Command: {
|
||||
MOVE_LEFT: 'cursor.left',
|
||||
MOVE_RIGHT: 'cursor.right',
|
||||
@@ -45,6 +52,7 @@ describe('useTabbedNavigation', () => {
|
||||
let capturedHandler: KeypressHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useKeyMatchers).mockReturnValue(mockKeyMatchers);
|
||||
vi.mocked(useKeypress).mockImplementation((handler) => {
|
||||
capturedHandler = handler;
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import { useReducer, useCallback, useEffect, useRef } from 'react';
|
||||
import { useKeypress, type Key } from './useKeypress.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||
|
||||
/**
|
||||
* Options for the useTabbedNavigation hook.
|
||||
@@ -147,6 +148,7 @@ export function useTabbedNavigation({
|
||||
isActive = true,
|
||||
onTabChange,
|
||||
}: UseTabbedNavigationOptions): UseTabbedNavigationResult {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const [state, dispatch] = useReducer(tabbedNavigationReducer, {
|
||||
currentIndex: Math.max(0, Math.min(initialIndex, tabCount - 1)),
|
||||
tabCount,
|
||||
@@ -231,6 +233,7 @@ export function useTabbedNavigation({
|
||||
goToNextTab,
|
||||
goToPrevTab,
|
||||
isNavigationBlocked,
|
||||
keyMatchers,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import type { Key } from './useKeypress.js';
|
||||
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||
|
||||
export type VimMode = 'NORMAL' | 'INSERT';
|
||||
|
||||
@@ -152,6 +153,7 @@ const vimReducer = (state: VimState, action: VimAction): VimState => {
|
||||
* @returns Object with vim state and input handler
|
||||
*/
|
||||
export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const { vimEnabled, vimMode, setVimMode } = useVimMode();
|
||||
const [state, dispatch] = useReducer(vimReducer, initialVimState);
|
||||
|
||||
@@ -439,7 +441,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
|
||||
return buffer.handleInput(normalizedKey);
|
||||
},
|
||||
[buffer, dispatch, updateMode, onSubmit, checkDoubleEscape],
|
||||
[buffer, dispatch, updateMode, onSubmit, checkDoubleEscape, keyMatchers],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -1202,6 +1204,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
executeCommand,
|
||||
updateMode,
|
||||
checkDoubleEscape,
|
||||
keyMatchers,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { keyMatchers, Command, createKeyMatchers } from './keyMatchers.js';
|
||||
import {
|
||||
defaultKeyMatchers,
|
||||
Command,
|
||||
createKeyMatchers,
|
||||
} from './keyMatchers.js';
|
||||
import type { KeyBindingConfig } from '../config/keyBindings.js';
|
||||
import { defaultKeyBindings } from '../config/keyBindings.js';
|
||||
import type { Key } from './hooks/useKeypress.js';
|
||||
@@ -422,14 +426,14 @@ describe('keyMatchers', () => {
|
||||
it(`should match ${command} correctly`, () => {
|
||||
positive.forEach((key) => {
|
||||
expect(
|
||||
keyMatchers[command](key),
|
||||
defaultKeyMatchers[command](key),
|
||||
`Expected ${command} to match ${JSON.stringify(key)}`,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
negative.forEach((key) => {
|
||||
expect(
|
||||
keyMatchers[command](key),
|
||||
defaultKeyMatchers[command](key),
|
||||
`Expected ${command} to NOT match ${JSON.stringify(key)}`,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,8 @@ export function createKeyMatchers(
|
||||
/**
|
||||
* Default key binding matchers using the default configuration
|
||||
*/
|
||||
export const keyMatchers: KeyMatchers = createKeyMatchers(defaultKeyBindings);
|
||||
export const defaultKeyMatchers: KeyMatchers =
|
||||
createKeyMatchers(defaultKeyBindings);
|
||||
|
||||
// Re-export Command for convenience
|
||||
export { Command };
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
||||
import { Command } from '../keyMatchers.js';
|
||||
import type { Key } from '../hooks/useKeypress.js';
|
||||
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||
|
||||
export function shouldDismissShortcutsHelpOnHotkey(key: Key): boolean {
|
||||
return Object.values(Command).some((command) => keyMatchers[command](key));
|
||||
export function useIsHelpDismissKey(): (key: Key) => boolean {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
return (key: Key) =>
|
||||
Object.values(Command).some((command) => keyMatchers[command](key));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user