mirror of
https://github.com/google-gemini/gemini-cli
synced 2026-04-21 13:37:17 +00:00
feat(ui): dynamically generate all keybinding hints (#21346)
This commit is contained in:
parent
4669148a4c
commit
6d607a5953
24 changed files with 424 additions and 293 deletions
|
|
@ -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`<br />`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`<br />`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`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`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`<br />`Ctrl + F` |
|
||||
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
|
||||
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------- | ------------------------------------------ |
|
||||
| Move the cursor to the start of the line. | `Ctrl+A`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl+E`<br />`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`<br />`Ctrl+F` |
|
||||
| Move the cursor one word to the left. | `Ctrl+Left`<br />`Alt+Left`<br />`Alt+B` |
|
||||
| Move the cursor one word to the right. | `Ctrl+Right`<br />`Alt+Right`<br />`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`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete`<br />`Alt + D` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Cmd + Z`<br />`Alt + Z` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`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`<br />`Alt+Backspace`<br />`Ctrl+W` |
|
||||
| Delete the next word. | `Ctrl+Delete`<br />`Alt+Delete`<br />`Alt+D` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl+H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl+D` |
|
||||
| Undo the most recent text edit. | `Cmd/Win+Z`<br />`Alt+Z` |
|
||||
| Redo the most recent undone text edit. | `Ctrl+Shift+Z`<br />`Shift+Cmd/Win+Z`<br />`Alt+Shift+Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------ | --------------------------------- |
|
||||
| Scroll content up. | `Shift + Up Arrow` |
|
||||
| Scroll content down. | `Shift + Down Arrow` |
|
||||
| Scroll to the top. | `Ctrl + Home`<br />`Shift + Home` |
|
||||
| Scroll to the bottom. | `Ctrl + End`<br />`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`<br />`Shift+Home` |
|
||||
| Scroll to the bottom. | `Ctrl+End`<br />`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`<br />`K` |
|
||||
| Move down within dialog options. | `Down Arrow`<br />`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`<br />`K` |
|
||||
| Move down within dialog options. | `Down`<br />`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`<br />`Enter` |
|
||||
| Move to the previous completion option. | `Up Arrow`<br />`Ctrl + P` |
|
||||
| Move to the next completion option. | `Down Arrow`<br />`Ctrl + N` |
|
||||
| Expand an inline suggestion. | `Right Arrow` |
|
||||
| Collapse an inline suggestion. | `Left Arrow` |
|
||||
| Action | Keys |
|
||||
| --------------------------------------- | -------------------- |
|
||||
| Accept the inline suggestion. | `Tab`<br />`Enter` |
|
||||
| Move to the previous completion option. | `Up`<br />`Ctrl+P` |
|
||||
| Move to the next completion option. | `Down`<br />`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`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||
| Open the current prompt or the plan in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter` |
|
||||
| Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
|
||||
| Open the current prompt or the plan in an external editor. | `Ctrl+X` |
|
||||
| Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`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`<br />`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`<br />`Shift+R` |
|
||||
| Suspend the CLI and move it to the background. | `Ctrl+Z` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ApprovalModeIndicatorProps> = ({
|
||||
approvalMode,
|
||||
allowPlanMode,
|
||||
|
|
@ -32,29 +24,32 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ReviewViewProps> = ({
|
|||
</Box>
|
||||
<DialogFooter
|
||||
primaryAction="Enter to submit"
|
||||
navigationActions="Tab/Shift+Tab to edit answers"
|
||||
navigationActions={`${formatCommand(Command.DIALOG_NEXT)}/${formatCommand(Command.DIALOG_PREV)} to edit answers`}
|
||||
extraParts={extraParts}
|
||||
/>
|
||||
</Box>
|
||||
|
|
@ -1146,7 +1147,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Help> = ({ commands }) => (
|
|||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Alt+Left/Right
|
||||
{formatCommand(Command.MOVE_WORD_LEFT)}/
|
||||
{formatCommand(Command.MOVE_WORD_RIGHT)}
|
||||
</Text>{' '}
|
||||
- Jump through words in the input
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+C
|
||||
{formatCommand(Command.QUIT)}
|
||||
</Text>{' '}
|
||||
- Quit application
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{process.platform === 'win32' ? 'Ctrl+Enter' : 'Ctrl+J'}
|
||||
{formatCommand(Command.NEWLINE)}
|
||||
</Text>{' '}
|
||||
{process.platform === 'linux'
|
||||
? '- New line (Alt+Enter works for certain linux distros)'
|
||||
: '- New line'}
|
||||
- New line
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+L
|
||||
{formatCommand(Command.CLEAR_SCREEN)}
|
||||
</Text>{' '}
|
||||
- Clear the screen
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+S
|
||||
{formatCommand(Command.TOGGLE_COPY_MODE)}
|
||||
</Text>{' '}
|
||||
- Enter selection mode to copy text
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+X
|
||||
{formatCommand(Command.OPEN_EXTERNAL_EDITOR)}
|
||||
</Text>{' '}
|
||||
- Open input in external editor
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Ctrl+Y
|
||||
{formatCommand(Command.TOGGLE_YOLO)}
|
||||
</Text>{' '}
|
||||
- Toggle YOLO mode
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Enter
|
||||
{formatCommand(Command.SUBMIT)}
|
||||
</Text>{' '}
|
||||
- Send message
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Esc
|
||||
{formatCommand(Command.ESCAPE)}
|
||||
</Text>{' '}
|
||||
- Cancel operation / Clear input (double press)
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Page Up/Down
|
||||
{formatCommand(Command.PAGE_UP)}/{formatCommand(Command.PAGE_DOWN)}
|
||||
</Text>{' '}
|
||||
- Scroll page up/down
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Shift+Tab
|
||||
{formatCommand(Command.CYCLE_APPROVAL_MODE)}
|
||||
</Text>{' '}
|
||||
- Toggle auto-accepting edits
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Up/Down
|
||||
{formatCommand(Command.HISTORY_UP)}/
|
||||
{formatCommand(Command.HISTORY_DOWN)}
|
||||
</Text>{' '}
|
||||
- Cycle through your prompt history
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Box>
|
||||
<Text>
|
||||
|
|
|
|||
|
|
@ -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(<ShortcutsHelp />);
|
||||
await rendered.waitUntilReady();
|
||||
expect(rendered.lastFrame()).toContain('Tab Tab');
|
||||
expect(rendered.lastFrame()).toContain('Tab focus UI');
|
||||
rendered.unmount();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<Box flexDirection="row">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<
|
|||
<Box>
|
||||
<Text color={theme.border.default}>
|
||||
<Text bold>{REDIRECTION_WARNING_TIP_LABEL}</Text>
|
||||
{REDIRECTION_WARNING_TIP_TEXT}
|
||||
{tipText}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`<TodoTray /> (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[`<TodoTray /> (showFullTodos: false) > renders null when todo list is em
|
|||
|
||||
exports[`<TodoTray /> (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[`<TodoTray /> (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[`<TodoTray /> (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[`<TodoTray /> (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[`<TodoTray /> (showFullTodos: true) > renders a todo list with long desc
|
|||
|
||||
exports[`<TodoTray /> (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[`<TodoTray /> (showFullTodos: true) > renders null when todo list is emp
|
|||
|
||||
exports[`<TodoTray /> (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[`<TodoTray /> (showFullTodos: true) > renders the most recent todo list
|
|||
|
||||
exports[`<TodoTray /> (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[`<TodoTray /> (showFullTodos: true) > renders when todos exist and one i
|
|||
|
||||
exports[`<TodoTray /> (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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.`;
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
|||
end: 'End',
|
||||
tab: 'Tab',
|
||||
space: 'Space',
|
||||
'double escape': 'Double Esc',
|
||||
};
|
||||
|
||||
interface ModifierMap {
|
||||
ctrl: string;
|
||||
alt: string;
|
||||
shift: string;
|
||||
cmd: string;
|
||||
}
|
||||
|
||||
const MODIFIER_MAPS: Record<string, ModifierMap> = {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -24,36 +24,7 @@ const START_MARKER = '<!-- KEYBINDINGS-AUTOGEN:START -->';
|
|||
const END_MARKER = '<!-- KEYBINDINGS-AUTOGEN:END -->';
|
||||
const OUTPUT_RELATIVE_PATH = ['docs', 'reference', 'keyboard-shortcuts.md'];
|
||||
|
||||
const KEY_NAME_OVERRIDES: Record<string, string> = {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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`<br />`Ctrl+P`');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue