feat: add click-to-focus support for interactive shell (#13341)

This commit is contained in:
Gal Zahavi
2025-11-19 15:49:39 -08:00
committed by GitHub
parent d0a845b6e6
commit 2231497b1f
17 changed files with 1072 additions and 416 deletions
@@ -104,6 +104,10 @@ describe('InputPrompt', () => {
useReverseSearchCompletion,
);
const mockedUseKittyKeyboardProtocol = vi.mocked(useKittyKeyboardProtocol);
const mockSetEmbeddedShellFocused = vi.fn();
const uiActions = {
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
};
beforeEach(() => {
vi.resetAllMocks();
@@ -240,7 +244,9 @@ describe('InputPrompt', () => {
it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
props.shellModeActive = true;
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\u001B[A');
@@ -253,7 +259,9 @@ describe('InputPrompt', () => {
it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
props.shellModeActive = true;
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\u001B[B');
@@ -269,7 +277,9 @@ describe('InputPrompt', () => {
vi.mocked(mockShellHistory.getPreviousCommand).mockReturnValue(
'previous command',
);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\u001B[A');
@@ -284,7 +294,9 @@ describe('InputPrompt', () => {
it('should call shellHistory.addCommandToHistory on submit in shell mode', async () => {
props.shellModeActive = true;
props.buffer.setText('ls -l');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r');
@@ -300,7 +312,9 @@ describe('InputPrompt', () => {
it('should NOT call shell history methods when not in shell mode', async () => {
props.buffer.setText('some text');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\u001B[A'); // Up arrow
@@ -339,7 +353,9 @@ describe('InputPrompt', () => {
props.buffer.setText('/mem');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
// Test up arrow
await act(async () => {
@@ -371,7 +387,9 @@ describe('InputPrompt', () => {
});
props.buffer.setText('/mem');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
// Test down arrow
await act(async () => {
@@ -398,7 +416,9 @@ describe('InputPrompt', () => {
showSuggestions: false,
});
props.buffer.setText('some text');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\u001B[A'); // Up arrow
@@ -628,7 +648,9 @@ describe('InputPrompt', () => {
activeSuggestionIndex: activeIndex,
});
props.buffer.setText(bufferText);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => stdin.write('\t'));
await waitFor(() =>
@@ -648,7 +670,9 @@ describe('InputPrompt', () => {
});
props.buffer.setText('/mem');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r');
@@ -680,7 +704,9 @@ describe('InputPrompt', () => {
});
props.buffer.setText('/?');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\t'); // Press Tab for autocomplete
@@ -694,7 +720,9 @@ describe('InputPrompt', () => {
it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
props.buffer.setText(' '); // Set buffer to whitespace
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r'); // Press Enter
@@ -714,7 +742,9 @@ describe('InputPrompt', () => {
});
props.buffer.setText('/clear');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r');
@@ -731,7 +761,9 @@ describe('InputPrompt', () => {
});
props.buffer.setText('/clear');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r');
@@ -749,7 +781,9 @@ describe('InputPrompt', () => {
});
props.buffer.setText('@src/components/');
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r');
@@ -767,7 +801,9 @@ describe('InputPrompt', () => {
mockBuffer.cursor = [0, 11];
mockBuffer.lines = ['first line\\'];
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\r');
@@ -785,7 +821,9 @@ describe('InputPrompt', () => {
await act(async () => {
props.buffer.setText('some text to clear');
});
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\x03'); // Ctrl+C character
@@ -800,7 +838,9 @@ describe('InputPrompt', () => {
it('should NOT clear the buffer on Ctrl+C if it is empty', async () => {
props.buffer.text = '';
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\x03'); // Ctrl+C character
@@ -813,7 +853,9 @@ describe('InputPrompt', () => {
});
it('should call setBannerVisible(false) when clear screen key is pressed', async () => {
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await act(async () => {
stdin.write('\x0C'); // Ctrl+L
@@ -918,7 +960,9 @@ describe('InputPrompt', () => {
: [],
});
const { unmount } = renderWithProviders(<InputPrompt {...props} />);
const { unmount } = renderWithProviders(<InputPrompt {...props} />, {
uiActions,
});
await waitFor(() => {
expect(mockedUseCommandCompletion).toHaveBeenCalledWith(
@@ -1988,7 +2032,7 @@ describe('InputPrompt', () => {
const { stdin, stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{ mouseEventsEnabled: true },
{ mouseEventsEnabled: true, uiActions },
);
// Wait for initial render
@@ -2012,6 +2056,33 @@ describe('InputPrompt', () => {
unmount();
},
);
it('should unfocus embedded shell on click', async () => {
props.buffer.text = 'hello';
props.buffer.lines = ['hello'];
props.buffer.viewportVisualLines = ['hello'];
props.buffer.visualToLogicalMap = [[0, 0]];
props.isEmbeddedShellFocused = true;
const { stdin, stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{ mouseEventsEnabled: true, uiActions },
);
await waitFor(() => {
expect(stdout.lastFrame()).toContain('hello');
});
await act(async () => {
// Click somewhere in the prompt
stdin.write(`\x1b[<0;5;2M`);
});
await waitFor(() => {
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
});
unmount();
});
});
describe('queued message editing', () => {