mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
feat: auto-execute simple slash commands on Enter (#13985)
This commit is contained in:
@@ -191,6 +191,13 @@ describe('InputPrompt', () => {
|
||||
isActive: false,
|
||||
markSelected: vi.fn(),
|
||||
},
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(undefined),
|
||||
slashCompletionRange: {
|
||||
completionStart: -1,
|
||||
completionEnd: -1,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(undefined),
|
||||
},
|
||||
getCompletedText: vi.fn().mockReturnValue(null),
|
||||
};
|
||||
mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion);
|
||||
|
||||
@@ -778,6 +785,173 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should auto-execute commands with autoExecute: true on Enter', async () => {
|
||||
const aboutCommand: SlashCommand = {
|
||||
name: 'about',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'About command',
|
||||
action: vi.fn(),
|
||||
autoExecute: true,
|
||||
};
|
||||
|
||||
const suggestion = { label: 'about', value: 'about' };
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(aboutCommand),
|
||||
getCompletedText: vi.fn().mockReturnValue('/about'),
|
||||
slashCompletionRange: {
|
||||
completionStart: 1,
|
||||
completionEnd: 3, // "/ab" -> start at 1, end at 3
|
||||
getCommandFromSuggestion: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
// User typed partial command
|
||||
props.buffer.setText('/ab');
|
||||
props.buffer.lines = ['/ab'];
|
||||
props.buffer.cursor = [0, 3];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Enter
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should submit the full command constructed from buffer + suggestion
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/about');
|
||||
// Should NOT handle autocomplete (which just fills text)
|
||||
expect(mockCommandCompletion.handleAutocomplete).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete commands with autoExecute: false on Enter', async () => {
|
||||
const shareCommand: SlashCommand = {
|
||||
name: 'share',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Share conversation to file',
|
||||
action: vi.fn(),
|
||||
autoExecute: false, // Explicitly set to false
|
||||
};
|
||||
|
||||
const suggestion = { label: 'share', value: 'share' };
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(shareCommand),
|
||||
getCompletedText: vi.fn().mockReturnValue('/share'),
|
||||
});
|
||||
|
||||
props.buffer.setText('/sh');
|
||||
props.buffer.lines = ['/sh'];
|
||||
props.buffer.cursor = [0, 3];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Enter
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should autocomplete to allow adding file argument
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete on Tab, even for executable commands', async () => {
|
||||
const executableCommand: SlashCommand = {
|
||||
name: 'about',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'About info',
|
||||
action: vi.fn(),
|
||||
autoExecute: true,
|
||||
};
|
||||
|
||||
const suggestion = { label: 'about', value: 'about' };
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(executableCommand),
|
||||
getCompletedText: vi.fn().mockReturnValue('/about'),
|
||||
});
|
||||
|
||||
props.buffer.setText('/ab');
|
||||
props.buffer.lines = ['/ab'];
|
||||
props.buffer.cursor = [0, 3];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t'); // Tab
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Tab always autocompletes, never executes
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete custom commands from .toml files on Enter', async () => {
|
||||
const customCommand: SlashCommand = {
|
||||
name: 'find-capital',
|
||||
kind: CommandKind.FILE,
|
||||
description: 'Find capital of a country',
|
||||
action: vi.fn(),
|
||||
// No autoExecute flag - custom commands default to undefined
|
||||
};
|
||||
|
||||
const suggestion = { label: 'find-capital', value: 'find-capital' };
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCommandFromSuggestion: vi.fn().mockReturnValue(customCommand),
|
||||
getCompletedText: vi.fn().mockReturnValue('/find-capital'),
|
||||
});
|
||||
|
||||
props.buffer.setText('/find');
|
||||
props.buffer.lines = ['/find'];
|
||||
props.buffer.cursor = [0, 5];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Enter
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should autocomplete (not execute) since autoExecute is undefined
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete an @-path on Enter without submitting', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
|
||||
@@ -35,12 +35,15 @@ import {
|
||||
saveClipboardImage,
|
||||
cleanupOldClipboardImages,
|
||||
} from '../utils/clipboardUtils.js';
|
||||
import {
|
||||
isAutoExecutableCommand,
|
||||
isSlashCommand,
|
||||
} from '../utils/commandUtils.js';
|
||||
import * as path from 'node:path';
|
||||
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
|
||||
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { useMouseClick } from '../hooks/useMouseClick.js';
|
||||
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
@@ -621,7 +624,27 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
completion.activeSuggestionIndex === -1
|
||||
? 0 // Default to the first if none is active
|
||||
: completion.activeSuggestionIndex;
|
||||
|
||||
if (targetIndex < completion.suggestions.length) {
|
||||
const suggestion = completion.suggestions[targetIndex];
|
||||
|
||||
const isEnterKey = key.name === 'return' && !key.ctrl;
|
||||
|
||||
if (isEnterKey && buffer.text.startsWith('/')) {
|
||||
const command = completion.getCommandFromSuggestion(suggestion);
|
||||
|
||||
if (command && isAutoExecutableCommand(command)) {
|
||||
const completedText = completion.getCompletedText(suggestion);
|
||||
|
||||
if (completedText) {
|
||||
setExpandedSuggestionIndex(-1);
|
||||
handleSubmit(completedText.trim());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default behavior: auto-complete to prompt box
|
||||
completion.handleAutocomplete(targetIndex);
|
||||
setExpandedSuggestionIndex(-1); // Reset expansion after selection
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user