diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md
index 5ad55a2c74..7b396b73d4 100644
--- a/docs/reference/keyboard-shortcuts.md
+++ b/docs/reference/keyboard-shortcuts.md
@@ -8,119 +8,119 @@ available combinations.
#### Basic Controls
-| Action | Keys |
-| --------------------------------------------------------------- | --------------------- |
-| Confirm the current selection or choice. | `Enter` |
-| Dismiss dialogs or cancel the current focus. | `Esc`
`Ctrl + [` |
-| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` |
-| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
+| Action | Keys |
+| --------------------------------------------------------------- | ------------------- |
+| Confirm the current selection or choice. | `Enter` |
+| Dismiss dialogs or cancel the current focus. | `Esc`
`Ctrl+[` |
+| Cancel the current request or quit the CLI when input is empty. | `Ctrl+C` |
+| Exit the CLI when the input buffer is empty. | `Ctrl+D` |
#### Cursor Movement
-| Action | Keys |
-| ------------------------------------------- | ------------------------------------------------------------ |
-| Move the cursor to the start of the line. | `Ctrl + A`
`Home` |
-| Move the cursor to the end of the line. | `Ctrl + E`
`End` |
-| Move the cursor up one line. | `Up Arrow` |
-| Move the cursor down one line. | `Down Arrow` |
-| Move the cursor one character to the left. | `Left Arrow` |
-| Move the cursor one character to the right. | `Right Arrow`
`Ctrl + F` |
-| Move the cursor one word to the left. | `Ctrl + Left Arrow`
`Alt + Left Arrow`
`Alt + B` |
-| Move the cursor one word to the right. | `Ctrl + Right Arrow`
`Alt + Right Arrow`
`Alt + F` |
+| Action | Keys |
+| ------------------------------------------- | ------------------------------------------ |
+| Move the cursor to the start of the line. | `Ctrl+A`
`Home` |
+| Move the cursor to the end of the line. | `Ctrl+E`
`End` |
+| Move the cursor up one line. | `Up` |
+| Move the cursor down one line. | `Down` |
+| Move the cursor one character to the left. | `Left` |
+| Move the cursor one character to the right. | `Right`
`Ctrl+F` |
+| Move the cursor one word to the left. | `Ctrl+Left`
`Alt+Left`
`Alt+B` |
+| Move the cursor one word to the right. | `Ctrl+Right`
`Alt+Right`
`Alt+F` |
#### Editing
-| Action | Keys |
-| ------------------------------------------------ | ---------------------------------------------------------------- |
-| Delete from the cursor to the end of the line. | `Ctrl + K` |
-| Delete from the cursor to the start of the line. | `Ctrl + U` |
-| Clear all text in the input field. | `Ctrl + C` |
-| Delete the previous word. | `Ctrl + Backspace`
`Alt + Backspace`
`Ctrl + W` |
-| Delete the next word. | `Ctrl + Delete`
`Alt + Delete`
`Alt + D` |
-| Delete the character to the left. | `Backspace`
`Ctrl + H` |
-| Delete the character to the right. | `Delete`
`Ctrl + D` |
-| Undo the most recent text edit. | `Cmd + Z`
`Alt + Z` |
-| Redo the most recent undone text edit. | `Shift + Ctrl + Z`
`Shift + Cmd + Z`
`Shift + Alt + Z` |
+| Action | Keys |
+| ------------------------------------------------ | -------------------------------------------------------- |
+| Delete from the cursor to the end of the line. | `Ctrl+K` |
+| Delete from the cursor to the start of the line. | `Ctrl+U` |
+| Clear all text in the input field. | `Ctrl+C` |
+| Delete the previous word. | `Ctrl+Backspace`
`Alt+Backspace`
`Ctrl+W` |
+| Delete the next word. | `Ctrl+Delete`
`Alt+Delete`
`Alt+D` |
+| Delete the character to the left. | `Backspace`
`Ctrl+H` |
+| Delete the character to the right. | `Delete`
`Ctrl+D` |
+| Undo the most recent text edit. | `Cmd/Win+Z`
`Alt+Z` |
+| Redo the most recent undone text edit. | `Ctrl+Shift+Z`
`Shift+Cmd/Win+Z`
`Alt+Shift+Z` |
#### Scrolling
-| Action | Keys |
-| ------------------------ | --------------------------------- |
-| Scroll content up. | `Shift + Up Arrow` |
-| Scroll content down. | `Shift + Down Arrow` |
-| Scroll to the top. | `Ctrl + Home`
`Shift + Home` |
-| Scroll to the bottom. | `Ctrl + End`
`Shift + End` |
-| Scroll up by one page. | `Page Up` |
-| Scroll down by one page. | `Page Down` |
+| Action | Keys |
+| ------------------------ | ----------------------------- |
+| Scroll content up. | `Shift+Up` |
+| Scroll content down. | `Shift+Down` |
+| Scroll to the top. | `Ctrl+Home`
`Shift+Home` |
+| Scroll to the bottom. | `Ctrl+End`
`Shift+End` |
+| Scroll up by one page. | `Page Up` |
+| Scroll down by one page. | `Page Down` |
#### History & Search
| Action | Keys |
| -------------------------------------------- | ------------ |
-| Show the previous entry in history. | `Ctrl + P` |
-| Show the next entry in history. | `Ctrl + N` |
-| Start reverse search through history. | `Ctrl + R` |
+| Show the previous entry in history. | `Ctrl+P` |
+| Show the next entry in history. | `Ctrl+N` |
+| Start reverse search through history. | `Ctrl+R` |
| Submit the selected reverse-search match. | `Enter` |
| Accept a suggestion while reverse searching. | `Tab` |
| Browse and rewind previous interactions. | `Double Esc` |
#### Navigation
-| Action | Keys |
-| -------------------------------------------------- | --------------------- |
-| Move selection up in lists. | `Up Arrow` |
-| Move selection down in lists. | `Down Arrow` |
-| Move up within dialog options. | `Up Arrow`
`K` |
-| Move down within dialog options. | `Down Arrow`
`J` |
-| Move to the next item or question in a dialog. | `Tab` |
-| Move to the previous item or question in a dialog. | `Shift + Tab` |
+| Action | Keys |
+| -------------------------------------------------- | --------------- |
+| Move selection up in lists. | `Up` |
+| Move selection down in lists. | `Down` |
+| Move up within dialog options. | `Up`
`K` |
+| Move down within dialog options. | `Down`
`J` |
+| Move to the next item or question in a dialog. | `Tab` |
+| Move to the previous item or question in a dialog. | `Shift+Tab` |
#### Suggestions & Completions
-| Action | Keys |
-| --------------------------------------- | ---------------------------- |
-| Accept the inline suggestion. | `Tab`
`Enter` |
-| Move to the previous completion option. | `Up Arrow`
`Ctrl + P` |
-| Move to the next completion option. | `Down Arrow`
`Ctrl + N` |
-| Expand an inline suggestion. | `Right Arrow` |
-| Collapse an inline suggestion. | `Left Arrow` |
+| Action | Keys |
+| --------------------------------------- | -------------------- |
+| Accept the inline suggestion. | `Tab`
`Enter` |
+| Move to the previous completion option. | `Up`
`Ctrl+P` |
+| Move to the next completion option. | `Down`
`Ctrl+N` |
+| Expand an inline suggestion. | `Right` |
+| Collapse an inline suggestion. | `Left` |
#### Text Input
-| Action | Keys |
-| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
-| Submit the current prompt. | `Enter` |
-| Insert a newline without submitting. | `Ctrl + Enter`
`Cmd + Enter`
`Alt + Enter`
`Shift + Enter`
`Ctrl + J` |
-| Open the current prompt or the plan in an external editor. | `Ctrl + X` |
-| Paste from the clipboard. | `Ctrl + V`
`Cmd + V`
`Alt + V` |
+| Action | Keys |
+| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
+| Submit the current prompt. | `Enter` |
+| Insert a newline without submitting. | `Ctrl+Enter`
`Cmd/Win+Enter`
`Alt+Enter`
`Shift+Enter`
`Ctrl+J` |
+| Open the current prompt or the plan in an external editor. | `Ctrl+X` |
+| Paste from the clipboard. | `Ctrl+V`
`Cmd/Win+V`
`Alt+V` |
#### App Controls
-| Action | Keys |
-| -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
-| Toggle detailed error information. | `F12` |
-| Toggle the full TODO list. | `Ctrl + T` |
-| Show IDE context details. | `Ctrl + G` |
-| Toggle Markdown rendering. | `Alt + M` |
-| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
-| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
-| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift + Tab` |
-| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` |
-| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` |
-| Toggle current background shell visibility. | `Ctrl + B` |
-| Toggle background shell list. | `Ctrl + L` |
-| Kill the active background shell. | `Ctrl + K` |
-| Confirm selection in background shell list. | `Enter` |
-| Dismiss background shell list. | `Esc` |
-| Move focus from background shell to Gemini. | `Shift + Tab` |
-| Move focus from background shell list to Gemini. | `Tab` |
-| Show warning when trying to move focus away from background shell. | `Tab` |
-| Show warning when trying to move focus away from shell input. | `Tab` |
-| Move focus from Gemini to the active shell. | `Tab` |
-| Move focus from the shell back to Gemini. | `Shift + Tab` |
-| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
-| Restart the application. | `R`
`Shift + R` |
-| Suspend the CLI and move it to the background. | `Ctrl + Z` |
+| Action | Keys |
+| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
+| Toggle detailed error information. | `F12` |
+| Toggle the full TODO list. | `Ctrl+T` |
+| Show IDE context details. | `Ctrl+G` |
+| Toggle Markdown rendering. | `Alt+M` |
+| Toggle copy mode when in alternate buffer mode. | `Ctrl+S` |
+| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` |
+| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` |
+| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` |
+| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl+O` |
+| Toggle current background shell visibility. | `Ctrl+B` |
+| Toggle background shell list. | `Ctrl+L` |
+| Kill the active background shell. | `Ctrl+K` |
+| Confirm selection in background shell list. | `Enter` |
+| Dismiss background shell list. | `Esc` |
+| Move focus from background shell to Gemini. | `Shift+Tab` |
+| Move focus from background shell list to Gemini. | `Tab` |
+| Show warning when trying to move focus away from background shell. | `Tab` |
+| Show warning when trying to move focus away from shell input. | `Tab` |
+| Move focus from Gemini to the active shell. | `Tab` |
+| Move focus from the shell back to Gemini. | `Shift+Tab` |
+| Clear the terminal screen and redraw the UI. | `Ctrl+L` |
+| Restart the application. | `R`
`Shift+R` |
+| Suspend the CLI and move it to the background. | `Ctrl+Z` |
diff --git a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx
index b5a981ac7a..4eaf3f18a4 100644
--- a/packages/cli/src/ui/components/ApprovalModeIndicator.tsx
+++ b/packages/cli/src/ui/components/ApprovalModeIndicator.tsx
@@ -8,22 +8,14 @@ import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ApprovalMode } from '@google/gemini-cli-core';
+import { formatCommand } from '../utils/keybindingUtils.js';
+import { Command } from '../../config/keyBindings.js';
interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
allowPlanMode?: boolean;
}
-export const APPROVAL_MODE_TEXT = {
- AUTO_EDIT: 'auto-accept edits',
- PLAN: 'plan',
- YOLO: 'YOLO',
- HINT_SWITCH_TO_PLAN_MODE: 'shift+tab to plan',
- HINT_SWITCH_TO_MANUAL_MODE: 'shift+tab to manual',
- HINT_SWITCH_TO_AUTO_EDIT_MODE: 'shift+tab to accept edits',
- HINT_SWITCH_TO_YOLO_MODE: 'ctrl+y',
-};
-
export const ApprovalModeIndicator: React.FC = ({
approvalMode,
allowPlanMode,
@@ -32,29 +24,32 @@ export const ApprovalModeIndicator: React.FC = ({
let textContent = '';
let subText = '';
+ const cycleHint = formatCommand(Command.CYCLE_APPROVAL_MODE);
+ const yoloHint = formatCommand(Command.TOGGLE_YOLO);
+
switch (approvalMode) {
case ApprovalMode.AUTO_EDIT:
textColor = theme.status.warning;
- textContent = APPROVAL_MODE_TEXT.AUTO_EDIT;
+ textContent = 'auto-accept edits';
subText = allowPlanMode
- ? APPROVAL_MODE_TEXT.HINT_SWITCH_TO_PLAN_MODE
- : APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
+ ? `${cycleHint} to plan`
+ : `${cycleHint} to manual`;
break;
case ApprovalMode.PLAN:
textColor = theme.status.success;
- textContent = APPROVAL_MODE_TEXT.PLAN;
- subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
+ textContent = 'plan';
+ subText = `${cycleHint} to manual`;
break;
case ApprovalMode.YOLO:
textColor = theme.status.error;
- textContent = APPROVAL_MODE_TEXT.YOLO;
- subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_YOLO_MODE;
+ textContent = 'YOLO';
+ subText = yoloHint;
break;
case ApprovalMode.DEFAULT:
default:
textColor = theme.text.accent;
textContent = '';
- subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_AUTO_EDIT_MODE;
+ subText = `${cycleHint} to accept edits`;
break;
}
diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx
index 9606513510..488a00b45e 100644
--- a/packages/cli/src/ui/components/AskUserDialog.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.tsx
@@ -23,6 +23,7 @@ import { useKeypress, type Key } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import { checkExhaustive } from '@google/gemini-cli-core';
import { TextInput } from './shared/TextInput.js';
+import { formatCommand } from '../utils/keybindingUtils.js';
import { useTextBuffer } from './shared/text-buffer.js';
import { getCachedStringWidth } from '../utils/textUtils.js';
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
@@ -252,7 +253,7 @@ const ReviewView: React.FC = ({
@@ -1146,7 +1147,7 @@ export const AskUserDialog: React.FC = ({
navigationActions={
questions.length > 1
? currentQuestion.type === 'text' || isEditingCustomOption
- ? 'Tab/Shift+Tab to switch questions'
+ ? `${formatCommand(Command.DIALOG_NEXT)}/${formatCommand(Command.DIALOG_PREV)} to switch questions`
: '←/→ to switch questions'
: currentQuestion.type === 'text' || isEditingCustomOption
? undefined
diff --git a/packages/cli/src/ui/components/Help.test.tsx b/packages/cli/src/ui/components/Help.test.tsx
index e16364a7ea..666593f04f 100644
--- a/packages/cli/src/ui/components/Help.test.tsx
+++ b/packages/cli/src/ui/components/Help.test.tsx
@@ -77,7 +77,7 @@ describe('Help Component', () => {
expect(output).toContain('Keyboard Shortcuts:');
expect(output).toContain('Ctrl+C');
expect(output).toContain('Ctrl+S');
- expect(output).toContain('Page Up/Down');
+ expect(output).toContain('Page Up/Page Down');
unmount();
});
});
diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx
index 762b8e9ff3..7f032b4e47 100644
--- a/packages/cli/src/ui/components/Help.tsx
+++ b/packages/cli/src/ui/components/Help.tsx
@@ -10,6 +10,8 @@ import { theme } from '../semantic-colors.js';
import { type SlashCommand, CommandKind } from '../commands/types.js';
import { KEYBOARD_SHORTCUTS_URL } from '../constants.js';
import { sanitizeForDisplay } from '../utils/textUtils.js';
+import { formatCommand } from '../utils/keybindingUtils.js';
+import { Command } from '../../config/keyBindings.js';
interface Help {
commands: readonly SlashCommand[];
@@ -116,75 +118,75 @@ export const Help: React.FC = ({ commands }) => (
- Alt+Left/Right
+ {formatCommand(Command.MOVE_WORD_LEFT)}/
+ {formatCommand(Command.MOVE_WORD_RIGHT)}
{' '}
- Jump through words in the input
- Ctrl+C
+ {formatCommand(Command.QUIT)}
{' '}
- Quit application
- {process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
+ {formatCommand(Command.NEWLINE)}
{' '}
- {process.platform === 'linux'
- ? '- New line (Alt+Enter works for certain linux distros)'
- : '- New line'}
+ - New line
- Ctrl+L
+ {formatCommand(Command.CLEAR_SCREEN)}
{' '}
- Clear the screen
- Ctrl+S
+ {formatCommand(Command.TOGGLE_COPY_MODE)}
{' '}
- Enter selection mode to copy text
- Ctrl+X
+ {formatCommand(Command.OPEN_EXTERNAL_EDITOR)}
{' '}
- Open input in external editor
- Ctrl+Y
+ {formatCommand(Command.TOGGLE_YOLO)}
{' '}
- Toggle YOLO mode
- Enter
+ {formatCommand(Command.SUBMIT)}
{' '}
- Send message
- Esc
+ {formatCommand(Command.ESCAPE)}
{' '}
- Cancel operation / Clear input (double press)
- Page Up/Down
+ {formatCommand(Command.PAGE_UP)}/{formatCommand(Command.PAGE_DOWN)}
{' '}
- Scroll page up/down
- Shift+Tab
+ {formatCommand(Command.CYCLE_APPROVAL_MODE)}
{' '}
- Toggle auto-accepting edits
- Up/Down
+ {formatCommand(Command.HISTORY_UP)}/
+ {formatCommand(Command.HISTORY_DOWN)}
{' '}
- Cycle through your prompt history
diff --git a/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx b/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx
index fd74b9281e..0ae721ccd5 100644
--- a/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx
+++ b/packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx
@@ -6,15 +6,18 @@
import { render } from '../../test-utils/render.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
-import { describe, it, expect, afterEach } from 'vitest';
+import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
describe('RawMarkdownIndicator', () => {
const originalPlatform = process.platform;
+ beforeEach(() => vi.stubEnv('FORCE_GENERIC_KEYBINDING_HINTS', ''));
+
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
+ vi.unstubAllEnvs();
});
it('renders correct key binding for darwin', async () => {
@@ -26,7 +29,7 @@ describe('RawMarkdownIndicator', () => {
);
await waitUntilReady();
expect(lastFrame()).toContain('raw markdown mode');
- expect(lastFrame()).toContain('option+m to toggle');
+ expect(lastFrame()).toContain('Option+M to toggle');
unmount();
});
@@ -39,7 +42,7 @@ describe('RawMarkdownIndicator', () => {
);
await waitUntilReady();
expect(lastFrame()).toContain('raw markdown mode');
- expect(lastFrame()).toContain('alt+m to toggle');
+ expect(lastFrame()).toContain('Alt+M to toggle');
unmount();
});
});
diff --git a/packages/cli/src/ui/components/RawMarkdownIndicator.tsx b/packages/cli/src/ui/components/RawMarkdownIndicator.tsx
index c47b35f244..922c30a36d 100644
--- a/packages/cli/src/ui/components/RawMarkdownIndicator.tsx
+++ b/packages/cli/src/ui/components/RawMarkdownIndicator.tsx
@@ -7,9 +7,11 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
+import { formatCommand } from '../utils/keybindingUtils.js';
+import { Command } from '../../config/keyBindings.js';
export const RawMarkdownIndicator: React.FC = () => {
- const modKey = process.platform === 'darwin' ? 'option+m' : 'alt+m';
+ const modKey = formatCommand(Command.TOGGLE_MARKDOWN);
return (
diff --git a/packages/cli/src/ui/components/ShortcutsHelp.test.tsx b/packages/cli/src/ui/components/ShortcutsHelp.test.tsx
index 779907ce5a..dab39bfcbb 100644
--- a/packages/cli/src/ui/components/ShortcutsHelp.test.tsx
+++ b/packages/cli/src/ui/components/ShortcutsHelp.test.tsx
@@ -4,17 +4,20 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { describe, it, expect, afterEach, vi } from 'vitest';
+import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { ShortcutsHelp } from './ShortcutsHelp.js';
describe('ShortcutsHelp', () => {
const originalPlatform = process.platform;
+ beforeEach(() => vi.stubEnv('FORCE_GENERIC_KEYBINDING_HINTS', ''));
+
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
+ vi.unstubAllEnvs();
vi.restoreAllMocks();
});
@@ -52,10 +55,10 @@ describe('ShortcutsHelp', () => {
},
);
- it('always shows Tab Tab focus UI shortcut', async () => {
+ it('always shows Tab focus UI shortcut', async () => {
const rendered = renderWithProviders();
await rendered.waitUntilReady();
- expect(rendered.lastFrame()).toContain('Tab Tab');
+ expect(rendered.lastFrame()).toContain('Tab focus UI');
rendered.unmount();
});
});
diff --git a/packages/cli/src/ui/components/ShortcutsHelp.tsx b/packages/cli/src/ui/components/ShortcutsHelp.tsx
index 63183ab922..149e4ddea9 100644
--- a/packages/cli/src/ui/components/ShortcutsHelp.tsx
+++ b/packages/cli/src/ui/components/ShortcutsHelp.tsx
@@ -10,29 +10,41 @@ import { theme } from '../semantic-colors.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
import { SectionHeader } from './shared/SectionHeader.js';
import { useUIState } from '../contexts/UIStateContext.js';
+import { Command } from '../../config/keyBindings.js';
+import { formatCommand } from '../utils/keybindingUtils.js';
type ShortcutItem = {
key: string;
description: string;
};
-const buildShortcutItems = (): ShortcutItem[] => {
- const isMac = process.platform === 'darwin';
- const altLabel = isMac ? 'Option' : 'Alt';
-
- return [
- { key: '!', description: 'shell mode' },
- { key: '@', description: 'select file or folder' },
- { key: 'Esc Esc', description: 'clear & rewind' },
- { key: 'Tab Tab', description: 'focus UI' },
- { key: 'Ctrl+Y', description: 'YOLO mode' },
- { key: 'Shift+Tab', description: 'cycle mode' },
- { key: 'Ctrl+V', description: 'paste images' },
- { key: `${altLabel}+M`, description: 'raw markdown mode' },
- { key: 'Ctrl+R', description: 'reverse-search history' },
- { key: 'Ctrl+X', description: 'open external editor' },
- ];
-};
+const buildShortcutItems = (): ShortcutItem[] => [
+ { key: '!', description: 'shell mode' },
+ { key: '@', description: 'select file or folder' },
+ { key: formatCommand(Command.REWIND), description: 'clear & rewind' },
+ { key: formatCommand(Command.FOCUS_SHELL_INPUT), description: 'focus UI' },
+ { key: formatCommand(Command.TOGGLE_YOLO), description: 'YOLO mode' },
+ {
+ key: formatCommand(Command.CYCLE_APPROVAL_MODE),
+ description: 'cycle mode',
+ },
+ {
+ key: formatCommand(Command.PASTE_CLIPBOARD),
+ description: 'paste images',
+ },
+ {
+ key: formatCommand(Command.TOGGLE_MARKDOWN),
+ description: 'raw markdown mode',
+ },
+ {
+ key: formatCommand(Command.REVERSE_SEARCH),
+ description: 'reverse-search history',
+ },
+ {
+ key: formatCommand(Command.OPEN_EXTERNAL_EDITOR),
+ description: 'open external editor',
+ },
+];
const Shortcut: React.FC<{ item: ShortcutItem }> = ({ item }) => (
diff --git a/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap
index 2544f7322e..8ddb141478 100644
--- a/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ApprovalModeIndicator.test.tsx.snap
@@ -1,31 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode 1`] = `
-"auto-accept edits shift+tab to manual
+"auto-accept edits Shift+Tab to manual
"
`;
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode with plan enabled 1`] = `
-"auto-accept edits shift+tab to plan
+"auto-accept edits Shift+Tab to plan
"
`;
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode 1`] = `
-"shift+tab to accept edits
+"Shift+Tab to accept edits
"
`;
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode with plan enabled 1`] = `
-"shift+tab to accept edits
+"Shift+Tab to accept edits
"
`;
exports[`ApprovalModeIndicator > renders correctly for PLAN mode 1`] = `
-"plan shift+tab to manual
+"plan Shift+Tab to manual
"
`;
exports[`ApprovalModeIndicator > renders correctly for YOLO mode 1`] = `
-"YOLO ctrl+y
+"YOLO Ctrl+Y
"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
index 06f509f1f6..2e115ef12c 100644
--- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
@@ -115,6 +115,20 @@ Review your answers:
Tests → (not answered)
Docs → (not answered)
+Enter to submit · / to edit answers · Esc to cancel
+"
+`;
+
+exports[`AskUserDialog > allows navigating to Review tab and back 2`] = `
+"← □ Tests │ □ Docs │ ≡ Review →
+
+Review your answers:
+
+⚠ You have 2 unanswered questions
+
+Tests → (not answered)
+Docs → (not answered)
+
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
"
`;
@@ -198,6 +212,20 @@ Review your answers:
License → (not answered)
README → (not answered)
+Enter to submit · / to edit answers · Esc to cancel
+"
+`;
+
+exports[`AskUserDialog > shows warning for unanswered questions on Review tab 2`] = `
+"← □ License │ □ README │ ≡ Review →
+
+Review your answers:
+
+⚠ You have 2 unanswered questions
+
+License → (not answered)
+README → (not answered)
+
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap
index 70d2cba48d..9e65c72f69 100644
--- a/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ShortcutsHelp.test.tsx.snap
@@ -5,8 +5,8 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
Shortcuts See /help for more
! shell mode
@ select file or folder
- Esc Esc clear & rewind
- Tab Tab focus UI
+ Double Esc clear & rewind
+ Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
@@ -21,8 +21,8 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
Shortcuts See /help for more
! shell mode
@ select file or folder
- Esc Esc clear & rewind
- Tab Tab focus UI
+ Double Esc clear & rewind
+ Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
@@ -37,8 +37,8 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = `
Shortcuts See /help for more
! shell mode Shift+Tab cycle mode Ctrl+V paste images
@ select file or folder Ctrl+Y YOLO mode Alt+M raw markdown mode
- Esc Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
- Tab Tab focus UI
+ Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
+ Tab focus UI
"
`;
@@ -47,7 +47,7 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = `
Shortcuts See /help for more
! shell mode Shift+Tab cycle mode Ctrl+V paste images
@ select file or folder Ctrl+Y YOLO mode Option+M raw markdown mode
- Esc Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
- Tab Tab focus UI
+ Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
+ Tab focus UI
"
`;
diff --git a/packages/cli/src/ui/components/messages/Todo.tsx b/packages/cli/src/ui/components/messages/Todo.tsx
index 4f2b95fd3c..786fe5e2f1 100644
--- a/packages/cli/src/ui/components/messages/Todo.tsx
+++ b/packages/cli/src/ui/components/messages/Todo.tsx
@@ -11,6 +11,8 @@ import { useMemo } from 'react';
import type { HistoryItemToolGroup } from '../../types.js';
import { Checklist } from '../Checklist.js';
import type { ChecklistItemData } from '../ChecklistItem.js';
+import { formatCommand } from '../../utils/keybindingUtils.js';
+import { Command } from '../../../config/keyBindings.js';
export const TodoTray: React.FC = () => {
const uiState = useUIState();
@@ -55,7 +57,7 @@ export const TodoTray: React.FC = () => {
title="Todo"
items={checklistItems}
isExpanded={uiState.showFullTodos}
- toggleHint="ctrl+t to toggle"
+ toggleHint={`${formatCommand(Command.SHOW_FULL_TODOS)} to toggle`}
/>
);
};
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index b60dd4dc8b..b97a29565b 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -31,12 +31,6 @@ import { theme } from '../../semantic-colors.js';
import { useSettings } from '../../contexts/SettingsContext.js';
import { keyMatchers, Command } from '../../keyMatchers.js';
import { formatCommand } from '../../utils/keybindingUtils.js';
-import {
- REDIRECTION_WARNING_NOTE_LABEL,
- REDIRECTION_WARNING_NOTE_TEXT,
- REDIRECTION_WARNING_TIP_LABEL,
- REDIRECTION_WARNING_TIP_TEXT,
-} from '../../textConstants.js';
import { AskUserDialog } from '../AskUserDialog.js';
import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js';
import { WarningMessage } from './WarningMessage.js';
@@ -57,6 +51,11 @@ export interface ToolConfirmationMessageProps {
terminalWidth: number;
}
+const REDIRECTION_WARNING_NOTE_LABEL = 'Note: ';
+const REDIRECTION_WARNING_NOTE_TEXT =
+ 'Command contains redirection which can be undesirable.';
+const REDIRECTION_WARNING_TIP_LABEL = 'Tip: '; // Padded to align with "Note: "
+
export const ToolConfirmationMessage: React.FC<
ToolConfirmationMessageProps
> = ({
@@ -503,12 +502,12 @@ export const ToolConfirmationMessage: React.FC<
if (containsRedirection) {
// Calculate lines needed for Note and Tip
const safeWidth = Math.max(terminalWidth, 1);
+ const tipText = `Toggle auto-edit (${formatCommand(Command.CYCLE_APPROVAL_MODE)}) to allow redirection in the future.`;
+
const noteLength =
REDIRECTION_WARNING_NOTE_LABEL.length +
REDIRECTION_WARNING_NOTE_TEXT.length;
- const tipLength =
- REDIRECTION_WARNING_TIP_LABEL.length +
- REDIRECTION_WARNING_TIP_TEXT.length;
+ const tipLength = REDIRECTION_WARNING_TIP_LABEL.length + tipText.length;
const noteLines = Math.ceil(noteLength / safeWidth);
const tipLines = Math.ceil(tipLength / safeWidth);
@@ -534,7 +533,7 @@ export const ToolConfirmationMessage: React.FC<
{REDIRECTION_WARNING_TIP_LABEL}
- {REDIRECTION_WARNING_TIP_TEXT}
+ {tipText}
>
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap
index 86ba095192..554808e830 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/Todo.test.tsx.snap
@@ -2,7 +2,7 @@
exports[` (showFullTodos: false) > renders a todo list with long descriptions that wrap when full view is on 1`] = `
"──────────────────────────────────────────────────
- Todo 1/2 completed (ctrl+t to toggle) » This i…
+ Todo 1/2 completed (Ctrl+T to toggle) » This i…
"
`;
@@ -14,25 +14,25 @@ exports[` (showFullTodos: false) > renders null when todo list is em
exports[` (showFullTodos: false) > renders the most recent todo list when multiple write_todos calls are in history 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 0/2 completed (ctrl+t to toggle) » Newer Task 2
+ Todo 0/2 completed (Ctrl+T to toggle) » Newer Task 2
"
`;
exports[` (showFullTodos: false) > renders when todos exist and one is in progress 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 1/3 completed (ctrl+t to toggle) » Task 2
+ Todo 1/3 completed (Ctrl+T to toggle) » Task 2
"
`;
exports[` (showFullTodos: false) > renders when todos exist but none are in progress 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 1/2 completed (ctrl+t to toggle)
+ Todo 1/2 completed (Ctrl+T to toggle)
"
`;
exports[` (showFullTodos: true) > renders a todo list with long descriptions that wrap when full view is on 1`] = `
"──────────────────────────────────────────────────
- Todo 1/2 completed (ctrl+t to toggle)
+ Todo 1/2 completed (Ctrl+T to toggle)
» This is a very long description for a pending
task that should wrap around multiple lines
@@ -44,7 +44,7 @@ exports[` (showFullTodos: true) > renders a todo list with long desc
exports[` (showFullTodos: true) > renders full list when all todos are inactive 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 1/1 completed (ctrl+t to toggle)
+ Todo 1/1 completed (Ctrl+T to toggle)
✓ Task 1
✗ Task 2
@@ -57,7 +57,7 @@ exports[` (showFullTodos: true) > renders null when todo list is emp
exports[` (showFullTodos: true) > renders the most recent todo list when multiple write_todos calls are in history 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 0/2 completed (ctrl+t to toggle)
+ Todo 0/2 completed (Ctrl+T to toggle)
☐ Newer Task 1
» Newer Task 2
@@ -66,7 +66,7 @@ exports[` (showFullTodos: true) > renders the most recent todo list
exports[` (showFullTodos: true) > renders when todos exist and one is in progress 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 1/3 completed (ctrl+t to toggle)
+ Todo 1/3 completed (Ctrl+T to toggle)
☐ Pending Task
» Task 2
@@ -77,7 +77,7 @@ exports[` (showFullTodos: true) > renders when todos exist and one i
exports[` (showFullTodos: true) > renders when todos exist but none are in progress 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
- Todo 1/2 completed (ctrl+t to toggle)
+ Todo 1/2 completed (Ctrl+T to toggle)
☐ Pending Task
✗ In Progress Task
diff --git a/packages/cli/src/ui/components/shared/DialogFooter.tsx b/packages/cli/src/ui/components/shared/DialogFooter.tsx
index 7411a91611..ee16d43650 100644
--- a/packages/cli/src/ui/components/shared/DialogFooter.tsx
+++ b/packages/cli/src/ui/components/shared/DialogFooter.tsx
@@ -11,7 +11,7 @@ import { theme } from '../../semantic-colors.js';
export interface DialogFooterProps {
/** The main shortcut (e.g., "Enter to submit") */
primaryAction: string;
- /** Secondary navigation shortcuts (e.g., "Tab/Shift+Tab to switch questions") */
+ /** Secondary navigation shortcuts (e.g., "Tab to switch questions") */
navigationActions?: string;
/** Exit shortcut (defaults to "Esc to cancel") */
cancelAction?: string;
diff --git a/packages/cli/src/ui/hooks/useSuspend.test.ts b/packages/cli/src/ui/hooks/useSuspend.test.ts
index 9aa90d16b3..1d0b34b1a3 100644
--- a/packages/cli/src/ui/hooks/useSuspend.test.ts
+++ b/packages/cli/src/ui/hooks/useSuspend.test.ts
@@ -29,6 +29,8 @@ import {
cleanupTerminalOnExit,
terminalCapabilityManager,
} from '../utils/terminalCapabilityManager.js';
+import { formatCommand } from '../utils/keybindingUtils.js';
+import { Command } from '../../config/keyBindings.js';
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
@@ -99,8 +101,12 @@ describe('useSuspend', () => {
act(() => {
result.current.handleSuspend();
});
+
+ const suspendKey = formatCommand(Command.SUSPEND_APP);
+ const undoKey = formatCommand(Command.UNDO);
+
expect(handleWarning).toHaveBeenCalledWith(
- 'Press Ctrl+Z again to suspend. Undo has moved to Cmd + Z or Alt/Opt + Z.',
+ `Press ${suspendKey} again to suspend. Undo has moved to ${undoKey}.`,
);
act(() => {
@@ -190,8 +196,9 @@ describe('useSuspend', () => {
result.current.handleSuspend();
});
+ const suspendKey = formatCommand(Command.SUSPEND_APP);
expect(handleWarning).toHaveBeenCalledWith(
- 'Ctrl+Z suspend is not supported on Windows.',
+ `${suspendKey} suspend is not supported on Windows.`,
);
expect(killSpy).not.toHaveBeenCalled();
expect(cleanupTerminalOnExit).not.toHaveBeenCalled();
diff --git a/packages/cli/src/ui/hooks/useSuspend.ts b/packages/cli/src/ui/hooks/useSuspend.ts
index 9c986d30d6..7d295b4450 100644
--- a/packages/cli/src/ui/hooks/useSuspend.ts
+++ b/packages/cli/src/ui/hooks/useSuspend.ts
@@ -20,6 +20,8 @@ import {
terminalCapabilityManager,
} from '../utils/terminalCapabilityManager.js';
import { WARNING_PROMPT_DURATION_MS } from '../constants.js';
+import { formatCommand } from '../utils/keybindingUtils.js';
+import { Command } from '../../config/keyBindings.js';
interface UseSuspendProps {
handleWarning: (message: string) => void;
@@ -59,10 +61,11 @@ export function useSuspend({
clearTimeout(ctrlZTimerRef.current);
ctrlZTimerRef.current = null;
}
+ const suspendKey = formatCommand(Command.SUSPEND_APP);
if (ctrlZPressCount > 1) {
setCtrlZPressCount(0);
if (process.platform === 'win32') {
- handleWarning('Ctrl+Z suspend is not supported on Windows.');
+ handleWarning(`${suspendKey} suspend is not supported on Windows.`);
return;
}
@@ -130,8 +133,9 @@ export function useSuspend({
process.kill(0, 'SIGTSTP');
} else if (ctrlZPressCount > 0) {
+ const undoKey = formatCommand(Command.UNDO);
handleWarning(
- 'Press Ctrl+Z again to suspend. Undo has moved to Cmd + Z or Alt/Opt + Z.',
+ `Press ${suspendKey} again to suspend. Undo has moved to ${undoKey}.`,
);
ctrlZTimerRef.current = setTimeout(() => {
setCtrlZPressCount(0);
diff --git a/packages/cli/src/ui/textConstants.ts b/packages/cli/src/ui/textConstants.ts
index a7ea77de79..00be0623d2 100644
--- a/packages/cli/src/ui/textConstants.ts
+++ b/packages/cli/src/ui/textConstants.ts
@@ -16,5 +16,5 @@ export const REDIRECTION_WARNING_NOTE_LABEL = 'Note: ';
export const REDIRECTION_WARNING_NOTE_TEXT =
'Command contains redirection which can be undesirable.';
export const REDIRECTION_WARNING_TIP_LABEL = 'Tip: '; // Padded to align with "Note: "
-export const REDIRECTION_WARNING_TIP_TEXT =
- 'Toggle auto-edit (Shift+Tab) to allow redirection in the future.';
+export const getRedirectionWarningTipText = (shiftTabHint: string) =>
+ `Toggle auto-edit (${shiftTabHint}) to allow redirection in the future.`;
diff --git a/packages/cli/src/ui/utils/keybindingUtils.test.ts b/packages/cli/src/ui/utils/keybindingUtils.test.ts
index cdee917332..4dfe2f814c 100644
--- a/packages/cli/src/ui/utils/keybindingUtils.test.ts
+++ b/packages/cli/src/ui/utils/keybindingUtils.test.ts
@@ -7,47 +7,137 @@
import { describe, it, expect } from 'vitest';
import { formatKeyBinding, formatCommand } from './keybindingUtils.js';
import { Command } from '../../config/keyBindings.js';
+import type { KeyBinding } from '../../config/keyBindings.js';
describe('keybindingUtils', () => {
describe('formatKeyBinding', () => {
- it('formats simple keys', () => {
- expect(formatKeyBinding({ key: 'a' })).toBe('A');
- expect(formatKeyBinding({ key: 'return' })).toBe('Enter');
- expect(formatKeyBinding({ key: 'escape' })).toBe('Esc');
- });
+ const testCases: Array<{
+ name: string;
+ binding: KeyBinding;
+ expected: {
+ darwin: string;
+ win32: string;
+ linux: string;
+ default: string;
+ };
+ }> = [
+ {
+ name: 'simple key',
+ binding: { key: 'a' },
+ expected: { darwin: 'A', win32: 'A', linux: 'A', default: 'A' },
+ },
+ {
+ name: 'named key (return)',
+ binding: { key: 'return' },
+ expected: {
+ darwin: 'Enter',
+ win32: 'Enter',
+ linux: 'Enter',
+ default: 'Enter',
+ },
+ },
+ {
+ name: 'named key (escape)',
+ binding: { key: 'escape' },
+ expected: { darwin: 'Esc', win32: 'Esc', linux: 'Esc', default: 'Esc' },
+ },
+ {
+ name: 'ctrl modifier',
+ binding: { key: 'c', ctrl: true },
+ expected: {
+ darwin: 'Ctrl+C',
+ win32: 'Ctrl+C',
+ linux: 'Ctrl+C',
+ default: 'Ctrl+C',
+ },
+ },
+ {
+ name: 'cmd modifier',
+ binding: { key: 'z', cmd: true },
+ expected: {
+ darwin: 'Cmd+Z',
+ win32: 'Win+Z',
+ linux: 'Super+Z',
+ default: 'Cmd/Win+Z',
+ },
+ },
+ {
+ name: 'alt/option modifier',
+ binding: { key: 'left', alt: true },
+ expected: {
+ darwin: 'Option+Left',
+ win32: 'Alt+Left',
+ linux: 'Alt+Left',
+ default: 'Alt+Left',
+ },
+ },
+ {
+ name: 'shift modifier',
+ binding: { key: 'up', shift: true },
+ expected: {
+ darwin: 'Shift+Up',
+ win32: 'Shift+Up',
+ linux: 'Shift+Up',
+ default: 'Shift+Up',
+ },
+ },
+ {
+ name: 'multiple modifiers (ctrl+shift)',
+ binding: { key: 'z', ctrl: true, shift: true },
+ expected: {
+ darwin: 'Ctrl+Shift+Z',
+ win32: 'Ctrl+Shift+Z',
+ linux: 'Ctrl+Shift+Z',
+ default: 'Ctrl+Shift+Z',
+ },
+ },
+ {
+ name: 'all modifiers',
+ binding: { key: 'a', ctrl: true, alt: true, shift: true, cmd: true },
+ expected: {
+ darwin: 'Ctrl+Option+Shift+Cmd+A',
+ win32: 'Ctrl+Alt+Shift+Win+A',
+ linux: 'Ctrl+Alt+Shift+Super+A',
+ default: 'Ctrl+Alt+Shift+Cmd/Win+A',
+ },
+ },
+ ];
- it('formats modifiers', () => {
- expect(formatKeyBinding({ key: 'c', ctrl: true })).toBe('Ctrl+C');
- expect(formatKeyBinding({ key: 'z', cmd: true })).toBe('Cmd+Z');
- expect(formatKeyBinding({ key: 'up', shift: true })).toBe('Shift+Up');
- expect(formatKeyBinding({ key: 'left', alt: true })).toBe('Alt+Left');
- });
-
- it('formats multiple modifiers in order', () => {
- expect(formatKeyBinding({ key: 'z', ctrl: true, shift: true })).toBe(
- 'Ctrl+Shift+Z',
- );
- expect(
- formatKeyBinding({
- key: 'a',
- ctrl: true,
- alt: true,
- shift: true,
- cmd: true,
- }),
- ).toBe('Ctrl+Alt+Shift+Cmd+A');
+ testCases.forEach(({ name, binding, expected }) => {
+ describe(`${name}`, () => {
+ it('formats correctly for darwin', () => {
+ expect(formatKeyBinding(binding, 'darwin')).toBe(expected.darwin);
+ });
+ it('formats correctly for win32', () => {
+ expect(formatKeyBinding(binding, 'win32')).toBe(expected.win32);
+ });
+ it('formats correctly for linux', () => {
+ expect(formatKeyBinding(binding, 'linux')).toBe(expected.linux);
+ });
+ it('formats correctly for default', () => {
+ expect(formatKeyBinding(binding, 'default')).toBe(expected.default);
+ });
+ });
});
});
describe('formatCommand', () => {
- it('formats default commands', () => {
- expect(formatCommand(Command.QUIT)).toBe('Ctrl+C');
- expect(formatCommand(Command.SUBMIT)).toBe('Enter');
- expect(formatCommand(Command.TOGGLE_BACKGROUND_SHELL)).toBe('Ctrl+B');
+ it('formats default commands (using default platform behavior)', () => {
+ expect(formatCommand(Command.QUIT, undefined, 'default')).toBe('Ctrl+C');
+ expect(formatCommand(Command.SUBMIT, undefined, 'default')).toBe('Enter');
+ expect(
+ formatCommand(Command.TOGGLE_BACKGROUND_SHELL, undefined, 'default'),
+ ).toBe('Ctrl+B');
});
it('returns empty string for unknown commands', () => {
- expect(formatCommand('unknown.command' as unknown as Command)).toBe('');
+ expect(
+ formatCommand(
+ 'unknown.command' as unknown as Command,
+ undefined,
+ 'default',
+ ),
+ ).toBe('');
});
});
});
diff --git a/packages/cli/src/ui/utils/keybindingUtils.ts b/packages/cli/src/ui/utils/keybindingUtils.ts
index 43e3d4e1fd..a084b9c68c 100644
--- a/packages/cli/src/ui/utils/keybindingUtils.ts
+++ b/packages/cli/src/ui/utils/keybindingUtils.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import process from 'node:process';
import {
type Command,
type KeyBinding,
@@ -29,18 +30,62 @@ const KEY_NAME_MAP: Record = {
end: 'End',
tab: 'Tab',
space: 'Space',
+ 'double escape': 'Double Esc',
+};
+
+interface ModifierMap {
+ ctrl: string;
+ alt: string;
+ shift: string;
+ cmd: string;
+}
+
+const MODIFIER_MAPS: Record = {
+ darwin: {
+ ctrl: 'Ctrl',
+ alt: 'Option',
+ shift: 'Shift',
+ cmd: 'Cmd',
+ },
+ win32: {
+ ctrl: 'Ctrl',
+ alt: 'Alt',
+ shift: 'Shift',
+ cmd: 'Win',
+ },
+ linux: {
+ ctrl: 'Ctrl',
+ alt: 'Alt',
+ shift: 'Shift',
+ cmd: 'Super',
+ },
+ default: {
+ ctrl: 'Ctrl',
+ alt: 'Alt',
+ shift: 'Shift',
+ cmd: 'Cmd/Win',
+ },
};
/**
* Formats a single KeyBinding into a human-readable string (e.g., "Ctrl+C").
*/
-export function formatKeyBinding(binding: KeyBinding): string {
+export function formatKeyBinding(
+ binding: KeyBinding,
+ platform?: string,
+): string {
+ const activePlatform =
+ platform ??
+ (process.env['FORCE_GENERIC_KEYBINDING_HINTS']
+ ? 'default'
+ : process.platform);
+ const modMap = MODIFIER_MAPS[activePlatform] || MODIFIER_MAPS['default'];
const parts: string[] = [];
- if (binding.ctrl) parts.push('Ctrl');
- if (binding.alt) parts.push('Alt');
- if (binding.shift) parts.push('Shift');
- if (binding.cmd) parts.push('Cmd');
+ if (binding.ctrl) parts.push(modMap.ctrl);
+ if (binding.alt) parts.push(modMap.alt);
+ if (binding.shift) parts.push(modMap.shift);
+ if (binding.cmd) parts.push(modMap.cmd);
const keyName = KEY_NAME_MAP[binding.key] || binding.key.toUpperCase();
parts.push(keyName);
@@ -54,6 +99,7 @@ export function formatKeyBinding(binding: KeyBinding): string {
export function formatCommand(
command: Command,
config: KeyBindingConfig = defaultKeyBindings,
+ platform?: string,
): string {
const bindings = config[command];
if (!bindings || bindings.length === 0) {
@@ -61,5 +107,5 @@ export function formatCommand(
}
// Use the first binding as the primary one for display
- return formatKeyBinding(bindings[0]);
+ return formatKeyBinding(bindings[0], platform);
}
diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts
index 1b7645c3f4..8d055bc63d 100644
--- a/packages/cli/test-setup.ts
+++ b/packages/cli/test-setup.ts
@@ -27,6 +27,9 @@ if (process.env.NO_COLOR !== undefined) {
// Force true color output for ink so that snapshots always include color information.
process.env.FORCE_COLOR = '3';
+// Force generic keybinding hints to ensure stable snapshots across different operating systems.
+process.env.FORCE_GENERIC_KEYBINDING_HINTS = 'true';
+
import './src/test-utils/customMatchers.js';
let consoleErrorSpy: vi.SpyInstance;
diff --git a/scripts/generate-keybindings-doc.ts b/scripts/generate-keybindings-doc.ts
index eea7ef9af3..19f07198ac 100644
--- a/scripts/generate-keybindings-doc.ts
+++ b/scripts/generate-keybindings-doc.ts
@@ -24,36 +24,7 @@ const START_MARKER = '';
const END_MARKER = '';
const OUTPUT_RELATIVE_PATH = ['docs', 'reference', 'keyboard-shortcuts.md'];
-const KEY_NAME_OVERRIDES: Record = {
- return: 'Enter',
- escape: 'Esc',
- 'double escape': 'Double Esc',
- tab: 'Tab',
- backspace: 'Backspace',
- delete: 'Delete',
- up: 'Up Arrow',
- down: 'Down Arrow',
- left: 'Left Arrow',
- right: 'Right Arrow',
- home: 'Home',
- end: 'End',
- pageup: 'Page Up',
- pagedown: 'Page Down',
- clear: 'Clear',
- insert: 'Insert',
- f1: 'F1',
- f2: 'F2',
- f3: 'F3',
- f4: 'F4',
- f5: 'F5',
- f6: 'F6',
- f7: 'F7',
- f8: 'F8',
- f9: 'F9',
- f10: 'F10',
- f11: 'F11',
- f12: 'F12',
-};
+import { formatKeyBinding } from '../packages/cli/src/ui/utils/keybindingUtils.js';
export interface KeybindingDocCommand {
description: string;
@@ -143,52 +114,16 @@ function formatBindings(bindings: readonly KeyBinding[]): string[] {
const results: string[] = [];
for (const binding of bindings) {
- const label = formatBinding(binding);
+ const label = formatKeyBinding(binding, 'default');
if (label && !seen.has(label)) {
seen.add(label);
- results.push(label);
+ results.push(`\`${label}\``);
}
}
return results;
}
-function formatBinding(binding: KeyBinding): string {
- const modifiers: string[] = [];
- if (binding.shift) modifiers.push('Shift');
- if (binding.alt) modifiers.push('Alt');
- if (binding.ctrl) modifiers.push('Ctrl');
- if (binding.cmd) modifiers.push('Cmd');
-
- const keyName = formatKeyName(binding.key);
- if (!keyName) {
- return '';
- }
-
- const segments = [...modifiers, keyName].filter(Boolean);
- let combo = segments.join(' + ');
-
- const restrictions: string[] = [];
- if (binding.shift === false) restrictions.push('Shift');
- if (binding.alt === false) restrictions.push('Alt');
- if (binding.ctrl === false) restrictions.push('Ctrl');
- if (binding.cmd === false) restrictions.push('Cmd');
-
- if (restrictions.length > 0) {
- combo = `${combo} (no ${restrictions.join(', ')})`;
- }
-
- return combo ? `\`${combo}\`` : '';
-}
-
-function formatKeyName(key: string): string {
- const normalized = key.toLowerCase();
- if (KEY_NAME_OVERRIDES[normalized]) {
- return KEY_NAME_OVERRIDES[normalized];
- }
- return key.length === 1 ? key.toUpperCase() : key;
-}
-
if (process.argv[1]) {
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
if (entryUrl === import.meta.url) {
diff --git a/scripts/tests/generate-keybindings-doc.test.ts b/scripts/tests/generate-keybindings-doc.test.ts
index 68a166609b..c669fed02e 100644
--- a/scripts/tests/generate-keybindings-doc.test.ts
+++ b/scripts/tests/generate-keybindings-doc.test.ts
@@ -57,12 +57,11 @@ describe('generate-keybindings-doc', () => {
const markdown = renderDocumentation(sections);
expect(markdown).toContain('#### Custom Controls');
expect(markdown).toContain('Trigger custom action.');
- expect(markdown).toContain('`Ctrl + X`');
+ expect(markdown).toContain('`Ctrl+X`');
expect(markdown).toContain('Submit with Enter if no modifiers are held.');
- expect(markdown).toContain('`Enter (no Shift, Ctrl)`');
+ expect(markdown).toContain('`Enter`');
expect(markdown).toContain('#### Navigation');
expect(markdown).toContain('Move up through results.');
- expect(markdown).toContain('`Up Arrow (no Shift)`');
- expect(markdown).toContain('`Ctrl + P (no Shift)`');
+ expect(markdown).toContain('`Up`
`Ctrl+P`');
});
});