diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md index c496b416c5..1c5faccffa 100644 --- a/docs/cli/keyboard-shortcuts.md +++ b/docs/cli/keyboard-shortcuts.md @@ -34,7 +34,7 @@ available combinations. | ------------------------------------------------ | --------------------------------------------------------- | | 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` | +| Clear all text in the input field. | `Esc` | | Delete the previous word. | `Ctrl + Backspace`
`Cmd + Backspace`
`Ctrl + W` | | Delete the next word. | `Ctrl + Delete`
`Cmd + Delete` | | Delete the character to the left. | `Backspace`
`Ctrl + H` | @@ -117,7 +117,8 @@ available combinations. - `!` on an empty prompt: Enter or exit shell mode. - `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line mode. -- `Esc` pressed twice quickly: Browse and rewind previous interactions. +- `Esc` pressed twice quickly: Clear the input prompt if it is not empty, + otherwise browse and rewind previous interactions. - `Up Arrow` / `Down Arrow`: When the cursor is at the top or bottom of a single-line input, navigate backward or forward through prompt history. - `Number keys (1-9, multi-digit)` inside selection dialogs: Jump directly to diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index 465225f3b4..36d25a6243 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -143,7 +143,7 @@ export const defaultKeyBindings: KeyBindingConfig = { // Editing [Command.KILL_LINE_RIGHT]: [{ key: 'k', ctrl: true }], [Command.KILL_LINE_LEFT]: [{ key: 'u', ctrl: true }], - [Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true }], + [Command.CLEAR_INPUT]: [{ key: 'escape' }], // Added command (meta/alt/option) for mac compatibility [Command.DELETE_WORD_BACKWARD]: [ { key: 'backspace', ctrl: true }, diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 0a801cba87..b38c49df42 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -1253,25 +1253,6 @@ describe('InputPrompt', () => { unmount(); }); - it('should clear the buffer on Ctrl+C if it has text', async () => { - await act(async () => { - props.buffer.setText('some text to clear'); - }); - const { stdin, unmount } = renderWithProviders(, { - uiActions, - }); - - await act(async () => { - stdin.write('\x03'); // Ctrl+C character - }); - await waitFor(() => { - expect(props.buffer.setText).toHaveBeenCalledWith(''); - expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); - }); - expect(props.onSubmit).not.toHaveBeenCalled(); - unmount(); - }); - it('should NOT clear the buffer on Ctrl+C if it is empty', async () => { props.buffer.text = ''; const { stdin, unmount } = renderWithProviders(, { @@ -1874,7 +1855,7 @@ describe('InputPrompt', () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); - it('should clear buffer on Ctrl-C', async () => { + it('should NOT clear buffer on Ctrl-C', async () => { const onEscapePromptChange = vi.fn(); props.onEscapePromptChange = onEscapePromptChange; props.buffer.setText('text to clear'); @@ -1887,16 +1868,16 @@ describe('InputPrompt', () => { stdin.write('\x03'); vi.advanceTimersByTime(100); - expect(props.buffer.setText).toHaveBeenCalledWith(''); - expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled(); + expect(props.buffer.setText).not.toHaveBeenCalledWith(''); }); unmount(); }); - it('should submit /rewind on double ESC', async () => { + it('should submit /rewind on double ESC when buffer is empty', async () => { const onEscapePromptChange = vi.fn(); props.onEscapePromptChange = onEscapePromptChange; - props.buffer.setText('some text'); + props.buffer.setText(''); + vi.mocked(props.buffer.setText).mockClear(); const { stdin, unmount } = renderWithProviders( , @@ -1911,6 +1892,26 @@ describe('InputPrompt', () => { unmount(); }); + it('should clear the buffer on esc esc if it has text', async () => { + const onEscapePromptChange = vi.fn(); + props.onEscapePromptChange = onEscapePromptChange; + props.buffer.setText('some text'); + vi.mocked(props.buffer.setText).mockClear(); + + const { stdin, unmount } = renderWithProviders( + , + ); + + await act(async () => { + stdin.write('\x1B\x1B'); + vi.advanceTimersByTime(100); + + expect(props.buffer.setText).toHaveBeenCalledWith(''); + expect(props.onSubmit).not.toHaveBeenCalledWith('/rewind'); + }); + unmount(); + }); + it('should reset escape state on any non-ESC key', async () => { const onEscapePromptChange = vi.fn(); props.onEscapePromptChange = onEscapePromptChange; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index f2445d4061..6740302894 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -495,7 +495,7 @@ export const InputPrompt: React.FC = ({ return; } - // Handle double ESC for rewind + // Handle double ESC if (escPressCount.current === 0) { escPressCount.current = 1; setShowEscapePrompt(true); @@ -506,9 +506,14 @@ export const InputPrompt: React.FC = ({ resetEscapeState(); }, 500); } else { - // Second ESC triggers rewind + // Second ESC resetEscapeState(); - onSubmit('/rewind'); + if (keyMatchers[Command.CLEAR_INPUT](key) && buffer.text.length > 0) { + buffer.setText(''); + resetCompletionState(); + } else { + onSubmit('/rewind'); + } } return; } @@ -790,15 +795,6 @@ export const InputPrompt: React.FC = ({ buffer.move('end'); return; } - // Ctrl+C (Clear input) - if (keyMatchers[Command.CLEAR_INPUT](key)) { - if (buffer.text.length > 0) { - buffer.setText(''); - resetCompletionState(); - } - return; - } - // Kill line commands if (keyMatchers[Command.KILL_LINE_RIGHT](key)) { buffer.killLineRight(); diff --git a/packages/cli/src/ui/components/StatusDisplay.tsx b/packages/cli/src/ui/components/StatusDisplay.tsx index c9e9e414e5..29c29275dd 100644 --- a/packages/cli/src/ui/components/StatusDisplay.tsx +++ b/packages/cli/src/ui/components/StatusDisplay.tsx @@ -45,7 +45,12 @@ export const StatusDisplay: React.FC = ({ } if (uiState.showEscapePrompt) { - return Press Esc again to rewind.; + const isPromptEmpty = uiState.buffer.text.trim().length === 0; + return ( + + Press Esc again to {isPromptEmpty ? 'rewind' : 'clear prompt'}. + + ); } if (uiState.queueErrorMessage) { diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index e8d9da4434..e7d7d4b523 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -96,8 +96,12 @@ describe('keyMatchers', () => { }, { command: Command.CLEAR_INPUT, - positive: [createKey('c', { ctrl: true })], - negative: [createKey('c'), createKey('k', { ctrl: true })], + positive: [createKey('escape')], + negative: [ + createKey('c', { ctrl: true }), + createKey('c'), + createKey('k', { ctrl: true }), + ], }, { command: Command.DELETE_CHAR_LEFT,