From 072d8ba2899f2601dad6d4b0333fdcb80555a7dd Mon Sep 17 00:00:00 2001
From: Ayesha Shafique <79274585+Aisha630@users.noreply.github.com>
Date: Mon, 4 Aug 2025 00:53:24 +0500
Subject: feat: Add reverse search capability for shell commands (#4793)

---
 .../cli/src/ui/components/InputPrompt.test.tsx     | 282 ++++++++++++++-------
 1 file changed, 195 insertions(+), 87 deletions(-)

(limited to 'packages/cli/src/ui/components/InputPrompt.test.tsx')

diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index e0d967da..6b7bc7ce 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -19,7 +19,10 @@ import {
   useShellHistory,
   UseShellHistoryReturn,
 } from '../hooks/useShellHistory.js';
-import { useCompletion, UseCompletionReturn } from '../hooks/useCompletion.js';
+import {
+  useSlashCompletion,
+  UseSlashCompletionReturn,
+} from '../hooks/useSlashCompletion.js';
 import {
   useInputHistory,
   UseInputHistoryReturn,
@@ -28,7 +31,7 @@ import * as clipboardUtils from '../utils/clipboardUtils.js';
 import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
 
 vi.mock('../hooks/useShellHistory.js');
-vi.mock('../hooks/useCompletion.js');
+vi.mock('../hooks/useSlashCompletion.js');
 vi.mock('../hooks/useInputHistory.js');
 vi.mock('../utils/clipboardUtils.js');
 
@@ -83,13 +86,13 @@ const mockSlashCommands: SlashCommand[] = [
 describe('InputPrompt', () => {
   let props: InputPromptProps;
   let mockShellHistory: UseShellHistoryReturn;
-  let mockCompletion: UseCompletionReturn;
+  let mockSlashCompletion: UseSlashCompletionReturn;
   let mockInputHistory: UseInputHistoryReturn;
   let mockBuffer: TextBuffer;
   let mockCommandContext: CommandContext;
 
   const mockedUseShellHistory = vi.mocked(useShellHistory);
-  const mockedUseCompletion = vi.mocked(useCompletion);
+  const mockedUseSlashCompletion = vi.mocked(useSlashCompletion);
   const mockedUseInputHistory = vi.mocked(useInputHistory);
 
   beforeEach(() => {
@@ -115,7 +118,9 @@ describe('InputPrompt', () => {
       visualScrollRow: 0,
       handleInput: vi.fn(),
       move: vi.fn(),
-      moveToOffset: vi.fn(),
+      moveToOffset: (offset: number) => {
+        mockBuffer.cursor = [0, offset];
+      },
       killLineRight: vi.fn(),
       killLineLeft: vi.fn(),
       openInExternalEditor: vi.fn(),
@@ -133,6 +138,7 @@ describe('InputPrompt', () => {
     } as unknown as TextBuffer;
 
     mockShellHistory = {
+      history: [],
       addCommandToHistory: vi.fn(),
       getPreviousCommand: vi.fn().mockReturnValue(null),
       getNextCommand: vi.fn().mockReturnValue(null),
@@ -140,7 +146,7 @@ describe('InputPrompt', () => {
     };
     mockedUseShellHistory.mockReturnValue(mockShellHistory);
 
-    mockCompletion = {
+    mockSlashCompletion = {
       suggestions: [],
       activeSuggestionIndex: -1,
       isLoadingSuggestions: false,
@@ -154,7 +160,7 @@ describe('InputPrompt', () => {
       setShowSuggestions: vi.fn(),
       handleAutocomplete: vi.fn(),
     };
-    mockedUseCompletion.mockReturnValue(mockCompletion);
+    mockedUseSlashCompletion.mockReturnValue(mockSlashCompletion);
 
     mockInputHistory = {
       navigateUp: vi.fn(),
@@ -265,8 +271,8 @@ describe('InputPrompt', () => {
   });
 
   it('should call completion.navigateUp for both up arrow and Ctrl+P when suggestions are showing', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [
         { label: 'memory', value: 'memory' },
@@ -285,15 +291,15 @@ describe('InputPrompt', () => {
 
     stdin.write('\u0010'); // Ctrl+P
     await wait();
-    expect(mockCompletion.navigateUp).toHaveBeenCalledTimes(2);
-    expect(mockCompletion.navigateDown).not.toHaveBeenCalled();
+    expect(mockSlashCompletion.navigateUp).toHaveBeenCalledTimes(2);
+    expect(mockSlashCompletion.navigateDown).not.toHaveBeenCalled();
 
     unmount();
   });
 
   it('should call completion.navigateDown for both down arrow and Ctrl+N when suggestions are showing', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [
         { label: 'memory', value: 'memory' },
@@ -311,15 +317,15 @@ describe('InputPrompt', () => {
 
     stdin.write('\u000E'); // Ctrl+N
     await wait();
-    expect(mockCompletion.navigateDown).toHaveBeenCalledTimes(2);
-    expect(mockCompletion.navigateUp).not.toHaveBeenCalled();
+    expect(mockSlashCompletion.navigateDown).toHaveBeenCalledTimes(2);
+    expect(mockSlashCompletion.navigateUp).not.toHaveBeenCalled();
 
     unmount();
   });
 
   it('should NOT call completion navigation when suggestions are not showing', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: false,
     });
     props.buffer.setText('some text');
@@ -336,8 +342,8 @@ describe('InputPrompt', () => {
     stdin.write('\u000E'); // Ctrl+N
     await wait();
 
-    expect(mockCompletion.navigateUp).not.toHaveBeenCalled();
-    expect(mockCompletion.navigateDown).not.toHaveBeenCalled();
+    expect(mockSlashCompletion.navigateUp).not.toHaveBeenCalled();
+    expect(mockSlashCompletion.navigateDown).not.toHaveBeenCalled();
     unmount();
   });
 
@@ -466,8 +472,8 @@ describe('InputPrompt', () => {
 
   it('should complete a partial parent command', async () => {
     // SCENARIO: /mem -> Tab
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [{ label: 'memory', value: 'memory', description: '...' }],
       activeSuggestionIndex: 0,
@@ -480,14 +486,14 @@ describe('InputPrompt', () => {
     stdin.write('\t'); // Press Tab
     await wait();
 
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
     unmount();
   });
 
   it('should append a sub-command when the parent command is already complete', async () => {
     // SCENARIO: /memory -> Tab (to accept 'add')
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [
         { label: 'show', value: 'show' },
@@ -503,14 +509,14 @@ describe('InputPrompt', () => {
     stdin.write('\t'); // Press Tab
     await wait();
 
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
     unmount();
   });
 
   it('should handle the "backspace" edge case correctly', async () => {
     // SCENARIO: /memory -> Backspace -> /memory -> Tab (to accept 'show')
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [
         { label: 'show', value: 'show' },
@@ -528,14 +534,14 @@ describe('InputPrompt', () => {
     await wait();
 
     // It should NOT become '/show'. It should correctly become '/memory show'.
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
     unmount();
   });
 
   it('should complete a partial argument for a command', async () => {
     // SCENARIO: /chat resume fi- -> Tab
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [{ label: 'fix-foo', value: 'fix-foo' }],
       activeSuggestionIndex: 0,
@@ -548,13 +554,13 @@ describe('InputPrompt', () => {
     stdin.write('\t'); // Press Tab
     await wait();
 
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
     unmount();
   });
 
   it('should autocomplete on Enter when suggestions are active, without submitting', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [{ label: 'memory', value: 'memory' }],
       activeSuggestionIndex: 0,
@@ -568,7 +574,7 @@ describe('InputPrompt', () => {
     await wait();
 
     // The app should autocomplete the text, NOT submit.
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
 
     expect(props.onSubmit).not.toHaveBeenCalled();
     unmount();
@@ -584,8 +590,8 @@ describe('InputPrompt', () => {
       },
     ];
 
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [{ label: 'help', value: 'help' }],
       activeSuggestionIndex: 0,
@@ -598,7 +604,7 @@ describe('InputPrompt', () => {
     stdin.write('\t'); // Press Tab for autocomplete
     await wait();
 
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
     unmount();
   });
 
@@ -616,8 +622,8 @@ describe('InputPrompt', () => {
   });
 
   it('should submit directly on Enter when isPerfectMatch is true', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: false,
       isPerfectMatch: true,
     });
@@ -634,8 +640,8 @@ describe('InputPrompt', () => {
   });
 
   it('should submit directly on Enter when a complete leaf command is typed', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: false,
       isPerfectMatch: false, // Added explicit isPerfectMatch false
     });
@@ -652,8 +658,8 @@ describe('InputPrompt', () => {
   });
 
   it('should autocomplete an @-path on Enter without submitting', async () => {
-    mockedUseCompletion.mockReturnValue({
-      ...mockCompletion,
+    mockedUseSlashCompletion.mockReturnValue({
+      ...mockSlashCompletion,
       showSuggestions: true,
       suggestions: [{ label: 'index.ts', value: 'index.ts' }],
       activeSuggestionIndex: 0,
@@ -666,7 +672,7 @@ describe('InputPrompt', () => {
     stdin.write('\r');
     await wait();
 
-    expect(mockCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
+    expect(mockSlashCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
     expect(props.onSubmit).not.toHaveBeenCalled();
     unmount();
   });
@@ -698,7 +704,7 @@ describe('InputPrompt', () => {
     await wait();
 
     expect(props.buffer.setText).toHaveBeenCalledWith('');
-    expect(mockCompletion.resetCompletionState).toHaveBeenCalled();
+    expect(mockSlashCompletion.resetCompletionState).toHaveBeenCalled();
     expect(props.onSubmit).not.toHaveBeenCalled();
     unmount();
   });
@@ -722,8 +728,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@src/components'];
       mockBuffer.cursor = [0, 15];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }],
       });
@@ -732,12 +738,13 @@ describe('InputPrompt', () => {
       await wait();
 
       // Verify useCompletion was called with correct signature
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -749,8 +756,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['/memory'];
       mockBuffer.cursor = [0, 7];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [{ label: 'show', value: 'show' }],
       });
@@ -758,12 +765,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -775,8 +783,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@src/file.ts hello'];
       mockBuffer.cursor = [0, 18];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: false,
         suggestions: [],
       });
@@ -784,12 +792,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -801,8 +810,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['/memory add'];
       mockBuffer.cursor = [0, 11];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: false,
         suggestions: [],
       });
@@ -810,12 +819,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -827,8 +837,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['hello world'];
       mockBuffer.cursor = [0, 5];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: false,
         suggestions: [],
       });
@@ -836,12 +846,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -853,8 +864,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['first line', '/memory'];
       mockBuffer.cursor = [1, 7];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: false,
         suggestions: [],
       });
@@ -863,12 +874,13 @@ describe('InputPrompt', () => {
       await wait();
 
       // Verify useCompletion was called with the buffer
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -880,8 +892,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['/memory'];
       mockBuffer.cursor = [0, 7];
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [{ label: 'show', value: 'show' }],
       });
@@ -889,12 +901,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -907,8 +920,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@src/file👍.txt'];
       mockBuffer.cursor = [0, 14]; // After the emoji character
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [{ label: 'file👍.txt', value: 'file👍.txt' }],
       });
@@ -916,12 +929,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -934,8 +948,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@src/file👍.txt hello'];
       mockBuffer.cursor = [0, 20]; // After the space
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: false,
         suggestions: [],
       });
@@ -943,12 +957,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -961,8 +976,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@src/my\\ file.txt'];
       mockBuffer.cursor = [0, 16]; // After the escaped space and filename
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [{ label: 'my file.txt', value: 'my file.txt' }],
       });
@@ -970,12 +985,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -988,8 +1004,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@path/my\\ file.txt hello'];
       mockBuffer.cursor = [0, 24]; // After "hello"
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: false,
         suggestions: [],
       });
@@ -997,12 +1013,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -1015,8 +1032,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@docs/my\\ long\\ file\\ name.md'];
       mockBuffer.cursor = [0, 29]; // At the end
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [
           { label: 'my long file name.md', value: 'my long file name.md' },
@@ -1026,12 +1043,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -1044,8 +1062,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['/memory\\ test'];
       mockBuffer.cursor = [0, 13]; // At the end
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [{ label: 'test-command', value: 'test-command' }],
       });
@@ -1053,12 +1071,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -1071,8 +1090,8 @@ describe('InputPrompt', () => {
       mockBuffer.lines = ['@' + path.join('files', 'emoji\\ 👍\\ test.txt')];
       mockBuffer.cursor = [0, 25]; // After the escaped space and emoji
 
-      mockedUseCompletion.mockReturnValue({
-        ...mockCompletion,
+      mockedUseSlashCompletion.mockReturnValue({
+        ...mockSlashCompletion,
         showSuggestions: true,
         suggestions: [
           { label: 'emoji 👍 test.txt', value: 'emoji 👍 test.txt' },
@@ -1082,12 +1101,13 @@ describe('InputPrompt', () => {
       const { unmount } = render(<InputPrompt {...props} />);
       await wait();
 
-      expect(mockedUseCompletion).toHaveBeenCalledWith(
+      expect(mockedUseSlashCompletion).toHaveBeenCalledWith(
         mockBuffer,
         ['/test/project/src'],
         path.join('test', 'project', 'src'),
         mockSlashCommands,
         mockCommandContext,
+        false,
         expect.any(Object),
       );
 
@@ -1169,4 +1189,92 @@ describe('InputPrompt', () => {
       unmount();
     });
   });
+
+  describe('reverse search', () => {
+    beforeEach(async () => {
+      props.shellModeActive = true;
+
+      vi.mocked(useShellHistory).mockReturnValue({
+        history: ['echo hello', 'echo world', 'ls'],
+        getPreviousCommand: vi.fn(),
+        getNextCommand: vi.fn(),
+        addCommandToHistory: vi.fn(),
+        resetHistoryPosition: vi.fn(),
+      });
+    });
+
+    it('invokes reverse search on Ctrl+R', async () => {
+      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      await wait();
+
+      stdin.write('\x12');
+      await wait();
+
+      const frame = stdout.lastFrame();
+      expect(frame).toContain('(r:)');
+      expect(frame).toContain('echo hello');
+      expect(frame).toContain('echo world');
+      expect(frame).toContain('ls');
+
+      unmount();
+    });
+
+    it('resets reverse search state on Escape', async () => {
+      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      await wait();
+
+      stdin.write('\x12');
+      await wait();
+      stdin.write('\x1B');
+      await wait();
+
+      const frame = stdout.lastFrame();
+      expect(frame).not.toContain('(r:)');
+      expect(frame).not.toContain('echo hello');
+
+      unmount();
+    });
+
+    it('completes the highlighted entry on Tab and exits reverse-search', async () => {
+      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      stdin.write('\x12');
+      await wait();
+      stdin.write('\t');
+      await wait();
+
+      expect(stdout.lastFrame()).not.toContain('(r:)');
+      expect(props.buffer.setText).toHaveBeenCalledWith('echo hello');
+      unmount();
+    });
+
+    it('submits the highlighted entry on Enter and exits reverse-search', async () => {
+      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      stdin.write('\x12');
+      await wait();
+      expect(stdout.lastFrame()).toContain('(r:)');
+      stdin.write('\r');
+      await wait();
+
+      expect(stdout.lastFrame()).not.toContain('(r:)');
+      expect(props.onSubmit).toHaveBeenCalledWith('echo hello');
+      unmount();
+    });
+
+    it('text and cursor position should be restored after reverse search', async () => {
+      props.buffer.setText('initial text');
+      props.buffer.cursor = [0, 3];
+      const { stdin, stdout, unmount } = render(<InputPrompt {...props} />);
+      stdin.write('\x12');
+      await wait();
+      expect(stdout.lastFrame()).toContain('(r:)');
+      stdin.write('\x1B');
+      await wait();
+
+      expect(stdout.lastFrame()).not.toContain('(r:)');
+      expect(props.buffer.text).toBe('initial text');
+      expect(props.buffer.cursor).toEqual([0, 3]);
+
+      unmount();
+    });
+  });
 });
-- 
cgit v1.2.3

