diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md index 0117a652a2..1402422c6b 100644 --- a/docs/cli/keyboard-shortcuts.md +++ b/docs/cli/keyboard-shortcuts.md @@ -61,7 +61,7 @@ available combinations. | Show the next entry in history. | `Ctrl + N (no Shift)` | | Start reverse search through history. | `Ctrl + R` | | Submit the selected reverse-search match. | `Enter (no Ctrl)` | -| Accept a suggestion while reverse searching. | `Tab` | +| Accept a suggestion while reverse searching. | `Tab (no Shift)` | | Browse and rewind previous interactions. | `Double Esc` | #### Navigation @@ -79,7 +79,7 @@ available combinations. | Action | Keys | | --------------------------------------- | -------------------------------------------------- | -| Accept the inline suggestion. | `Tab`
`Enter (no Ctrl)` | +| Accept the inline suggestion. | `Tab (no Shift)`
`Enter (no Ctrl)` | | Move to the previous completion option. | `Up Arrow (no Shift)`
`Ctrl + P (no Shift)` | | Move to the next completion option. | `Down Arrow (no Shift)`
`Ctrl + N (no Shift)` | | Expand an inline suggestion. | `Right Arrow` | diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index b6b8cb9534..4813abd368 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -212,7 +212,7 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }], [Command.REWIND]: [{ key: 'double escape' }], [Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }], - [Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }], + [Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab', shift: false }], // Navigation [Command.NAVIGATION_UP]: [{ key: 'up', shift: false }], @@ -231,7 +231,10 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.DIALOG_PREV]: [{ key: 'tab', shift: true }], // Suggestions & Completions - [Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }], + [Command.ACCEPT_SUGGESTION]: [ + { key: 'tab', shift: false }, + { key: 'return', ctrl: false }, + ], [Command.COMPLETION_UP]: [ { key: 'up', shift: false }, { key: 'p', shift: false, ctrl: true }, diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 8257cd8acc..6a6424c3c7 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -1221,6 +1221,36 @@ describe('InputPrompt', () => { unmount(); }); + it('should NOT autocomplete on Shift+Tab', async () => { + const suggestion = { label: 'about', value: 'about' }; + + mockedUseCommandCompletion.mockReturnValue({ + ...mockCommandCompletion, + showSuggestions: true, + suggestions: [suggestion], + activeSuggestionIndex: 0, + getCompletedText: vi.fn().mockReturnValue('/about'), + }); + + props.buffer.setText('/ab'); + props.buffer.lines = ['/ab']; + props.buffer.cursor = [0, 3]; + + const { stdin, unmount } = renderWithProviders(, { + uiActions, + }); + + await act(async () => { + stdin.write('\x1b[Z'); // Shift+Tab + }); + + // We need to wait a bit to ensure handleAutocomplete was NOT called + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockCommandCompletion.handleAutocomplete).not.toHaveBeenCalled(); + unmount(); + }); + it('should autocomplete custom commands from .toml files on Enter', async () => { const customCommand: SlashCommand = { name: 'find-capital', @@ -2659,6 +2689,38 @@ describe('InputPrompt', () => { unmount(); }, 15000); + it('should NOT autocomplete on Shift+Tab in reverse search', async () => { + const mockHandleAutocomplete = vi.fn(); + + mockedUseReverseSearchCompletion.mockReturnValue({ + ...mockReverseSearchCompletion, + suggestions: [{ label: 'echo hello', value: 'echo hello' }], + showSuggestions: true, + activeSuggestionIndex: 0, + handleAutocomplete: mockHandleAutocomplete, + }); + + const { stdin, unmount } = renderWithProviders( + , + { + uiActions, + }, + ); + + await act(async () => { + stdin.write('\x12'); // Ctrl+R + }); + + await act(async () => { + stdin.write('\x1b[Z'); // Shift+Tab + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockHandleAutocomplete).not.toHaveBeenCalled(); + unmount(); + }); + it('submits the highlighted entry on Enter and exits reverse-search', async () => { // Mock the reverse search completion to return suggestions mockedUseReverseSearchCompletion.mockReturnValue({ @@ -3035,6 +3097,39 @@ describe('InputPrompt', () => { }, ); + it('should NOT accept ghost text on Shift+Tab', async () => { + const mockAccept = vi.fn(); + mockedUseCommandCompletion.mockReturnValue({ + ...mockCommandCompletion, + showSuggestions: false, + suggestions: [], + promptCompletion: { + text: 'ghost text', + accept: mockAccept, + clear: vi.fn(), + isLoading: false, + isActive: true, + markSelected: vi.fn(), + }, + }); + + const { stdin, unmount } = renderWithProviders( + , + { + uiActions, + }, + ); + + await act(async () => { + stdin.write('\x1b[Z'); // Shift+Tab + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockAccept).not.toHaveBeenCalled(); + unmount(); + }); + it('should not reveal clean UI details on Shift+Tab when hidden', async () => { mockedUseCommandCompletion.mockReturnValue({ ...mockCommandCompletion, diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 7e9ec74ac4..8a4f068df1 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -975,6 +975,7 @@ export const InputPrompt: React.FC = ({ // Handle Tab key for ghost text acceptance if ( key.name === 'tab' && + !key.shift && !completion.showSuggestions && completion.promptCompletion.text ) {