From f7b4e749326e6e17dcd55d4bc78ae7cd681037bd Mon Sep 17 00:00:00 2001 From: Seydulla Narkulyyev Date: Tue, 22 Jul 2025 01:43:23 +0400 Subject: [PATCH] feat(cli):suggestion-navigation-shortcut (#3641) Co-authored-by: N. Taylor Mullen --- .../src/ui/components/InputPrompt.test.tsx | 77 +++++++++++++++++++ .../cli/src/ui/components/InputPrompt.tsx | 4 +- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 886a6235..3f646cc6 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -221,6 +221,83 @@ describe('InputPrompt', () => { unmount(); }); + it('should call completion.navigateUp for both up arrow and Ctrl+P when suggestions are showing', async () => { + mockedUseCompletion.mockReturnValue({ + ...mockCompletion, + showSuggestions: true, + suggestions: [ + { label: 'memory', value: 'memory' }, + { label: 'memcache', value: 'memcache' }, + ], + }); + + props.buffer.setText('/mem'); + + const { stdin, unmount } = render(); + await wait(); + + // Test up arrow + stdin.write('\u001B[A'); // Up arrow + await wait(); + + stdin.write('\u0010'); // Ctrl+P + await wait(); + expect(mockCompletion.navigateUp).toHaveBeenCalledTimes(2); + expect(mockCompletion.navigateDown).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should call completion.navigateDown for both down arrow and Ctrl+N when suggestions are showing', async () => { + mockedUseCompletion.mockReturnValue({ + ...mockCompletion, + showSuggestions: true, + suggestions: [ + { label: 'memory', value: 'memory' }, + { label: 'memcache', value: 'memcache' }, + ], + }); + props.buffer.setText('/mem'); + + const { stdin, unmount } = render(); + await wait(); + + // Test down arrow + stdin.write('\u001B[B'); // Down arrow + await wait(); + + stdin.write('\u000E'); // Ctrl+N + await wait(); + expect(mockCompletion.navigateDown).toHaveBeenCalledTimes(2); + expect(mockCompletion.navigateUp).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should NOT call completion navigation when suggestions are not showing', async () => { + mockedUseCompletion.mockReturnValue({ + ...mockCompletion, + showSuggestions: false, + }); + props.buffer.setText('some text'); + + const { stdin, unmount } = render(); + await wait(); + + stdin.write('\u001B[A'); // Up arrow + await wait(); + stdin.write('\u001B[B'); // Down arrow + await wait(); + stdin.write('\u0010'); // Ctrl+P + await wait(); + stdin.write('\u000E'); // Ctrl+N + await wait(); + + expect(mockCompletion.navigateUp).not.toHaveBeenCalled(); + expect(mockCompletion.navigateDown).not.toHaveBeenCalled(); + unmount(); + }); + describe('clipboard image paste', () => { beforeEach(() => { vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false); diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index b7c53196..a713c889 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -320,11 +320,11 @@ export const InputPrompt: React.FC = ({ if (completion.showSuggestions) { if (completion.suggestions.length > 1) { - if (key.name === 'up') { + if (key.name === 'up' || (key.ctrl && key.name === 'p')) { completion.navigateUp(); return; } - if (key.name === 'down') { + if (key.name === 'down' || (key.ctrl && key.name === 'n')) { completion.navigateDown(); return; }