Skip to content

Commit 28299d8

Browse files
scidominoliamhelmer
authored andcommitted
feat(ui): dynamically generate all keybinding hints (google-gemini#21346)
1 parent 27b7cc4 commit 28299d8

24 files changed

+424
-293
lines changed

docs/reference/keyboard-shortcuts.md

Lines changed: 84 additions & 84 deletions
Large diffs are not rendered by default.

packages/cli/src/ui/components/ApprovalModeIndicator.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,14 @@ import type React from 'react';
88
import { Box, Text } from 'ink';
99
import { theme } from '../semantic-colors.js';
1010
import { ApprovalMode } from '@google/gemini-cli-core';
11+
import { formatCommand } from '../utils/keybindingUtils.js';
12+
import { Command } from '../../config/keyBindings.js';
1113

1214
interface ApprovalModeIndicatorProps {
1315
approvalMode: ApprovalMode;
1416
allowPlanMode?: boolean;
1517
}
1618

17-
export const APPROVAL_MODE_TEXT = {
18-
AUTO_EDIT: 'auto-accept edits',
19-
PLAN: 'plan',
20-
YOLO: 'YOLO',
21-
HINT_SWITCH_TO_PLAN_MODE: 'shift+tab to plan',
22-
HINT_SWITCH_TO_MANUAL_MODE: 'shift+tab to manual',
23-
HINT_SWITCH_TO_AUTO_EDIT_MODE: 'shift+tab to accept edits',
24-
HINT_SWITCH_TO_YOLO_MODE: 'ctrl+y',
25-
};
26-
2719
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
2820
approvalMode,
2921
allowPlanMode,
@@ -32,29 +24,32 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
3224
let textContent = '';
3325
let subText = '';
3426

27+
const cycleHint = formatCommand(Command.CYCLE_APPROVAL_MODE);
28+
const yoloHint = formatCommand(Command.TOGGLE_YOLO);
29+
3530
switch (approvalMode) {
3631
case ApprovalMode.AUTO_EDIT:
3732
textColor = theme.status.warning;
38-
textContent = APPROVAL_MODE_TEXT.AUTO_EDIT;
33+
textContent = 'auto-accept edits';
3934
subText = allowPlanMode
40-
? APPROVAL_MODE_TEXT.HINT_SWITCH_TO_PLAN_MODE
41-
: APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
35+
? `${cycleHint} to plan`
36+
: `${cycleHint} to manual`;
4237
break;
4338
case ApprovalMode.PLAN:
4439
textColor = theme.status.success;
45-
textContent = APPROVAL_MODE_TEXT.PLAN;
46-
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
40+
textContent = 'plan';
41+
subText = `${cycleHint} to manual`;
4742
break;
4843
case ApprovalMode.YOLO:
4944
textColor = theme.status.error;
50-
textContent = APPROVAL_MODE_TEXT.YOLO;
51-
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_YOLO_MODE;
45+
textContent = 'YOLO';
46+
subText = yoloHint;
5247
break;
5348
case ApprovalMode.DEFAULT:
5449
default:
5550
textColor = theme.text.accent;
5651
textContent = '';
57-
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_AUTO_EDIT_MODE;
52+
subText = `${cycleHint} to accept edits`;
5853
break;
5954
}
6055

packages/cli/src/ui/components/AskUserDialog.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useKeypress, type Key } from '../hooks/useKeypress.js';
2323
import { keyMatchers, Command } from '../keyMatchers.js';
2424
import { checkExhaustive } from '@google/gemini-cli-core';
2525
import { TextInput } from './shared/TextInput.js';
26+
import { formatCommand } from '../utils/keybindingUtils.js';
2627
import { useTextBuffer } from './shared/text-buffer.js';
2728
import { getCachedStringWidth } from '../utils/textUtils.js';
2829
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
@@ -252,7 +253,7 @@ const ReviewView: React.FC<ReviewViewProps> = ({
252253
</Box>
253254
<DialogFooter
254255
primaryAction="Enter to submit"
255-
navigationActions="Tab/Shift+Tab to edit answers"
256+
navigationActions={`${formatCommand(Command.DIALOG_NEXT)}/${formatCommand(Command.DIALOG_PREV)} to edit answers`}
256257
extraParts={extraParts}
257258
/>
258259
</Box>
@@ -1146,7 +1147,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
11461147
navigationActions={
11471148
questions.length > 1
11481149
? currentQuestion.type === 'text' || isEditingCustomOption
1149-
? 'Tab/Shift+Tab to switch questions'
1150+
? `${formatCommand(Command.DIALOG_NEXT)}/${formatCommand(Command.DIALOG_PREV)} to switch questions`
11501151
: '←/→ to switch questions'
11511152
: currentQuestion.type === 'text' || isEditingCustomOption
11521153
? undefined

packages/cli/src/ui/components/Help.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('Help Component', () => {
7777
expect(output).toContain('Keyboard Shortcuts:');
7878
expect(output).toContain('Ctrl+C');
7979
expect(output).toContain('Ctrl+S');
80-
expect(output).toContain('Page Up/Down');
80+
expect(output).toContain('Page Up/Page Down');
8181
unmount();
8282
});
8383
});

packages/cli/src/ui/components/Help.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { theme } from '../semantic-colors.js';
1010
import { type SlashCommand, CommandKind } from '../commands/types.js';
1111
import { KEYBOARD_SHORTCUTS_URL } from '../constants.js';
1212
import { sanitizeForDisplay } from '../utils/textUtils.js';
13+
import { formatCommand } from '../utils/keybindingUtils.js';
14+
import { Command } from '../../config/keyBindings.js';
1315

1416
interface Help {
1517
commands: readonly SlashCommand[];
@@ -116,75 +118,75 @@ export const Help: React.FC<Help> = ({ commands }) => (
116118
</Text>
117119
<Text color={theme.text.primary}>
118120
<Text bold color={theme.text.accent}>
119-
Alt+Left/Right
121+
{formatCommand(Command.MOVE_WORD_LEFT)}/
122+
{formatCommand(Command.MOVE_WORD_RIGHT)}
120123
</Text>{' '}
121124
- Jump through words in the input
122125
</Text>
123126
<Text color={theme.text.primary}>
124127
<Text bold color={theme.text.accent}>
125-
Ctrl+C
128+
{formatCommand(Command.QUIT)}
126129
</Text>{' '}
127130
- Quit application
128131
</Text>
129132
<Text color={theme.text.primary}>
130133
<Text bold color={theme.text.accent}>
131-
{process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
134+
{formatCommand(Command.NEWLINE)}
132135
</Text>{' '}
133-
{process.platform === 'linux'
134-
? '- New line (Alt+Enter works for certain linux distros)'
135-
: '- New line'}
136+
- New line
136137
</Text>
137138
<Text color={theme.text.primary}>
138139
<Text bold color={theme.text.accent}>
139-
Ctrl+L
140+
{formatCommand(Command.CLEAR_SCREEN)}
140141
</Text>{' '}
141142
- Clear the screen
142143
</Text>
143144
<Text color={theme.text.primary}>
144145
<Text bold color={theme.text.accent}>
145-
Ctrl+S
146+
{formatCommand(Command.TOGGLE_COPY_MODE)}
146147
</Text>{' '}
147148
- Enter selection mode to copy text
148149
</Text>
149150
<Text color={theme.text.primary}>
150151
<Text bold color={theme.text.accent}>
151-
Ctrl+X
152+
{formatCommand(Command.OPEN_EXTERNAL_EDITOR)}
152153
</Text>{' '}
153154
- Open input in external editor
154155
</Text>
155156
<Text color={theme.text.primary}>
156157
<Text bold color={theme.text.accent}>
157-
Ctrl+Y
158+
{formatCommand(Command.TOGGLE_YOLO)}
158159
</Text>{' '}
159160
- Toggle YOLO mode
160161
</Text>
161162
<Text color={theme.text.primary}>
162163
<Text bold color={theme.text.accent}>
163-
Enter
164+
{formatCommand(Command.SUBMIT)}
164165
</Text>{' '}
165166
- Send message
166167
</Text>
167168
<Text color={theme.text.primary}>
168169
<Text bold color={theme.text.accent}>
169-
Esc
170+
{formatCommand(Command.ESCAPE)}
170171
</Text>{' '}
171172
- Cancel operation / Clear input (double press)
172173
</Text>
173174
<Text color={theme.text.primary}>
174175
<Text bold color={theme.text.accent}>
175-
Page Up/Down
176+
{formatCommand(Command.PAGE_UP)}/{formatCommand(Command.PAGE_DOWN)}
176177
</Text>{' '}
177178
- Scroll page up/down
178179
</Text>
179180
<Text color={theme.text.primary}>
180181
<Text bold color={theme.text.accent}>
181-
Shift+Tab
182+
{formatCommand(Command.CYCLE_APPROVAL_MODE)}
182183
</Text>{' '}
183184
- Toggle auto-accepting edits
184185
</Text>
185186
<Text color={theme.text.primary}>
186187
<Text bold color={theme.text.accent}>
187-
Up/Down
188+
{formatCommand(Command.HISTORY_UP)}/
189+
{formatCommand(Command.HISTORY_DOWN)}
188190
</Text>{' '}
189191
- Cycle through your prompt history
190192
</Text>

packages/cli/src/ui/components/RawMarkdownIndicator.test.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66

77
import { render } from '../../test-utils/render.js';
88
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
9-
import { describe, it, expect, afterEach } from 'vitest';
9+
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
1010

1111
describe('RawMarkdownIndicator', () => {
1212
const originalPlatform = process.platform;
1313

14+
beforeEach(() => vi.stubEnv('FORCE_GENERIC_KEYBINDING_HINTS', ''));
15+
1416
afterEach(() => {
1517
Object.defineProperty(process, 'platform', {
1618
value: originalPlatform,
1719
});
20+
vi.unstubAllEnvs();
1821
});
1922

2023
it('renders correct key binding for darwin', async () => {
@@ -26,7 +29,7 @@ describe('RawMarkdownIndicator', () => {
2629
);
2730
await waitUntilReady();
2831
expect(lastFrame()).toContain('raw markdown mode');
29-
expect(lastFrame()).toContain('option+m to toggle');
32+
expect(lastFrame()).toContain('Option+M to toggle');
3033
unmount();
3134
});
3235

@@ -39,7 +42,7 @@ describe('RawMarkdownIndicator', () => {
3942
);
4043
await waitUntilReady();
4144
expect(lastFrame()).toContain('raw markdown mode');
42-
expect(lastFrame()).toContain('alt+m to toggle');
45+
expect(lastFrame()).toContain('Alt+M to toggle');
4346
unmount();
4447
});
4548
});

packages/cli/src/ui/components/RawMarkdownIndicator.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import type React from 'react';
88
import { Box, Text } from 'ink';
99
import { theme } from '../semantic-colors.js';
10+
import { formatCommand } from '../utils/keybindingUtils.js';
11+
import { Command } from '../../config/keyBindings.js';
1012

1113
export const RawMarkdownIndicator: React.FC = () => {
12-
const modKey = process.platform === 'darwin' ? 'option+m' : 'alt+m';
14+
const modKey = formatCommand(Command.TOGGLE_MARKDOWN);
1315
return (
1416
<Box>
1517
<Text>

packages/cli/src/ui/components/ShortcutsHelp.test.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { describe, it, expect, afterEach, vi } from 'vitest';
7+
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
88
import { renderWithProviders } from '../../test-utils/render.js';
99
import { ShortcutsHelp } from './ShortcutsHelp.js';
1010

1111
describe('ShortcutsHelp', () => {
1212
const originalPlatform = process.platform;
1313

14+
beforeEach(() => vi.stubEnv('FORCE_GENERIC_KEYBINDING_HINTS', ''));
15+
1416
afterEach(() => {
1517
Object.defineProperty(process, 'platform', {
1618
value: originalPlatform,
1719
});
20+
vi.unstubAllEnvs();
1821
vi.restoreAllMocks();
1922
});
2023

@@ -52,10 +55,10 @@ describe('ShortcutsHelp', () => {
5255
},
5356
);
5457

55-
it('always shows Tab Tab focus UI shortcut', async () => {
58+
it('always shows Tab focus UI shortcut', async () => {
5659
const rendered = renderWithProviders(<ShortcutsHelp />);
5760
await rendered.waitUntilReady();
58-
expect(rendered.lastFrame()).toContain('Tab Tab');
61+
expect(rendered.lastFrame()).toContain('Tab focus UI');
5962
rendered.unmount();
6063
});
6164
});

packages/cli/src/ui/components/ShortcutsHelp.tsx

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,41 @@ import { theme } from '../semantic-colors.js';
1010
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
1111
import { SectionHeader } from './shared/SectionHeader.js';
1212
import { useUIState } from '../contexts/UIStateContext.js';
13+
import { Command } from '../../config/keyBindings.js';
14+
import { formatCommand } from '../utils/keybindingUtils.js';
1315

1416
type ShortcutItem = {
1517
key: string;
1618
description: string;
1719
};
1820

19-
const buildShortcutItems = (): ShortcutItem[] => {
20-
const isMac = process.platform === 'darwin';
21-
const altLabel = isMac ? 'Option' : 'Alt';
22-
23-
return [
24-
{ key: '!', description: 'shell mode' },
25-
{ key: '@', description: 'select file or folder' },
26-
{ key: 'Esc Esc', description: 'clear & rewind' },
27-
{ key: 'Tab Tab', description: 'focus UI' },
28-
{ key: 'Ctrl+Y', description: 'YOLO mode' },
29-
{ key: 'Shift+Tab', description: 'cycle mode' },
30-
{ key: 'Ctrl+V', description: 'paste images' },
31-
{ key: `${altLabel}+M`, description: 'raw markdown mode' },
32-
{ key: 'Ctrl+R', description: 'reverse-search history' },
33-
{ key: 'Ctrl+X', description: 'open external editor' },
34-
];
35-
};
21+
const buildShortcutItems = (): ShortcutItem[] => [
22+
{ key: '!', description: 'shell mode' },
23+
{ key: '@', description: 'select file or folder' },
24+
{ key: formatCommand(Command.REWIND), description: 'clear & rewind' },
25+
{ key: formatCommand(Command.FOCUS_SHELL_INPUT), description: 'focus UI' },
26+
{ key: formatCommand(Command.TOGGLE_YOLO), description: 'YOLO mode' },
27+
{
28+
key: formatCommand(Command.CYCLE_APPROVAL_MODE),
29+
description: 'cycle mode',
30+
},
31+
{
32+
key: formatCommand(Command.PASTE_CLIPBOARD),
33+
description: 'paste images',
34+
},
35+
{
36+
key: formatCommand(Command.TOGGLE_MARKDOWN),
37+
description: 'raw markdown mode',
38+
},
39+
{
40+
key: formatCommand(Command.REVERSE_SEARCH),
41+
description: 'reverse-search history',
42+
},
43+
{
44+
key: formatCommand(Command.OPEN_EXTERNAL_EDITOR),
45+
description: 'open external editor',
46+
},
47+
];
3648

3749
const Shortcut: React.FC<{ item: ShortcutItem }> = ({ item }) => (
3850
<Box flexDirection="row">
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode 1`] = `
4-
"auto-accept edits shift+tab to manual
4+
"auto-accept edits Shift+Tab to manual
55
"
66
`;
77

88
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode with plan enabled 1`] = `
9-
"auto-accept edits shift+tab to plan
9+
"auto-accept edits Shift+Tab to plan
1010
"
1111
`;
1212

1313
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode 1`] = `
14-
"shift+tab to accept edits
14+
"Shift+Tab to accept edits
1515
"
1616
`;
1717

1818
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode with plan enabled 1`] = `
19-
"shift+tab to accept edits
19+
"Shift+Tab to accept edits
2020
"
2121
`;
2222

2323
exports[`ApprovalModeIndicator > renders correctly for PLAN mode 1`] = `
24-
"plan shift+tab to manual
24+
"plan Shift+Tab to manual
2525
"
2626
`;
2727

2828
exports[`ApprovalModeIndicator > renders correctly for YOLO mode 1`] = `
29-
"YOLO ctrl+y
29+
"YOLO Ctrl+Y
3030
"
3131
`;

0 commit comments

Comments
 (0)