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:
@@ -19,6 +19,7 @@ export const aboutCommand: SlashCommand = {
|
||||
name: 'about',
|
||||
description: 'Show version info',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const osVersion = process.platform;
|
||||
let sandboxEnv = 'no sandbox';
|
||||
|
||||
@@ -11,6 +11,7 @@ export const authCommand: SlashCommand = {
|
||||
name: 'auth',
|
||||
description: 'Change the auth method',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (_context, _args): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'auth',
|
||||
|
||||
@@ -21,6 +21,7 @@ export const bugCommand: SlashCommand = {
|
||||
name: 'bug',
|
||||
description: 'Submit a bug report',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext, args?: string): Promise<void> => {
|
||||
const bugDescription = (args || '').trim();
|
||||
const { config } = context.services;
|
||||
|
||||
@@ -68,6 +68,7 @@ const listCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
description: 'List saved conversation checkpoints',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context): Promise<void> => {
|
||||
const chatDetails = await getSavedChatTags(context, false);
|
||||
|
||||
@@ -85,6 +86,7 @@ const saveCommand: SlashCommand = {
|
||||
description:
|
||||
'Save the current conversation as a checkpoint. Usage: /chat save <tag>',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context, args): Promise<SlashCommandActionReturn | void> => {
|
||||
const tag = args.trim();
|
||||
if (!tag) {
|
||||
@@ -153,6 +155,7 @@ const resumeCommand: SlashCommand = {
|
||||
description:
|
||||
'Resume a conversation from a checkpoint. Usage: /chat resume <tag>',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context, args) => {
|
||||
const tag = args.trim();
|
||||
if (!tag) {
|
||||
@@ -236,6 +239,7 @@ const deleteCommand: SlashCommand = {
|
||||
name: 'delete',
|
||||
description: 'Delete a conversation checkpoint. Usage: /chat delete <tag>',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context, args): Promise<MessageActionReturn> => {
|
||||
const tag = args.trim();
|
||||
if (!tag) {
|
||||
@@ -309,6 +313,7 @@ const shareCommand: SlashCommand = {
|
||||
description:
|
||||
'Share the current conversation to a markdown or json file. Usage: /chat share <file>',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context, args): Promise<MessageActionReturn> => {
|
||||
let filePathArg = args.trim();
|
||||
if (!filePathArg) {
|
||||
@@ -376,6 +381,7 @@ export const chatCommand: SlashCommand = {
|
||||
name: 'chat',
|
||||
description: 'Manage conversation history',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [
|
||||
listCommand,
|
||||
saveCommand,
|
||||
|
||||
@@ -13,6 +13,7 @@ export const clearCommand: SlashCommand = {
|
||||
name: 'clear',
|
||||
description: 'Clear the screen and conversation history',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, _args) => {
|
||||
const geminiClient = context.services.config?.getGeminiClient();
|
||||
const config = context.services.config;
|
||||
|
||||
@@ -14,6 +14,7 @@ export const compressCommand: SlashCommand = {
|
||||
altNames: ['summarize'],
|
||||
description: 'Compresses the context by replacing it with a summary',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const { ui } = context;
|
||||
if (ui.pendingItem) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const copyCommand: SlashCommand = {
|
||||
name: 'copy',
|
||||
description: 'Copy the last result or code snippet to clipboard',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
|
||||
const chat = await context.services.config?.getGeminiClient()?.getChat();
|
||||
const history = chat?.getHistory();
|
||||
|
||||
@@ -11,6 +11,7 @@ export const corgiCommand: SlashCommand = {
|
||||
description: 'Toggles corgi mode',
|
||||
hidden: true,
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context, _args) => {
|
||||
context.ui.toggleCorgiMode();
|
||||
},
|
||||
|
||||
@@ -79,6 +79,7 @@ export const directoryCommand: SlashCommand = {
|
||||
description:
|
||||
'Add directories to the workspace. Use comma to separate multiple paths',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext, args: string) => {
|
||||
const {
|
||||
ui: { addItem },
|
||||
|
||||
@@ -17,6 +17,7 @@ export const docsCommand: SlashCommand = {
|
||||
name: 'docs',
|
||||
description: 'Open full Gemini CLI documentation in your browser',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context: CommandContext): Promise<void> => {
|
||||
const docsUrl = 'https://goo.gle/gemini-cli-docs';
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const editorCommand: SlashCommand = {
|
||||
name: 'editor',
|
||||
description: 'Set external editor preference',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'editor',
|
||||
|
||||
@@ -473,6 +473,7 @@ const listExtensionsCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
description: 'List active extensions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: listAction,
|
||||
};
|
||||
|
||||
@@ -480,6 +481,7 @@ const updateExtensionsCommand: SlashCommand = {
|
||||
name: 'update',
|
||||
description: 'Update extensions. Usage: update <extension-names>|--all',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: updateAction,
|
||||
completion: completeExtensions,
|
||||
};
|
||||
@@ -488,6 +490,7 @@ const disableCommand: SlashCommand = {
|
||||
name: 'disable',
|
||||
description: 'Disable an extension',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: disableAction,
|
||||
completion: completeExtensionsAndScopes,
|
||||
};
|
||||
@@ -496,6 +499,7 @@ const enableCommand: SlashCommand = {
|
||||
name: 'enable',
|
||||
description: 'Enable an extension',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: enableAction,
|
||||
completion: completeExtensionsAndScopes,
|
||||
};
|
||||
@@ -504,6 +508,7 @@ const exploreExtensionsCommand: SlashCommand = {
|
||||
name: 'explore',
|
||||
description: 'Open extensions page in your browser',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: exploreAction,
|
||||
};
|
||||
|
||||
@@ -511,6 +516,7 @@ const restartCommand: SlashCommand = {
|
||||
name: 'restart',
|
||||
description: 'Restart all extensions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: restartAction,
|
||||
completion: completeExtensions,
|
||||
};
|
||||
@@ -525,6 +531,7 @@ export function extensionsCommand(
|
||||
name: 'extensions',
|
||||
description: 'Manage extensions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [
|
||||
listExtensionsCommand,
|
||||
updateExtensionsCommand,
|
||||
|
||||
@@ -13,6 +13,7 @@ export const helpCommand: SlashCommand = {
|
||||
altNames: ['?'],
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'For help on gemini-cli',
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const helpItem: Omit<HistoryItemHelp, 'id'> = {
|
||||
type: MessageType.HELP,
|
||||
|
||||
@@ -141,6 +141,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
name: 'ide',
|
||||
description: 'Manage IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: (): SlashCommandActionReturn =>
|
||||
({
|
||||
type: 'message',
|
||||
@@ -154,6 +155,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
name: 'ide',
|
||||
description: 'Manage IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [],
|
||||
};
|
||||
|
||||
@@ -161,6 +163,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
name: 'status',
|
||||
description: 'Check status of IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (): Promise<SlashCommandActionReturn> => {
|
||||
const { messageType, content } =
|
||||
await getIdeStatusMessageWithFiles(ideClient);
|
||||
@@ -176,6 +179,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
name: 'install',
|
||||
description: `Install required IDE companion for ${ideClient.getDetectedIdeDisplayName()}`,
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const installer = getIdeInstaller(currentIDE);
|
||||
if (!installer) {
|
||||
@@ -251,6 +255,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
name: 'enable',
|
||||
description: 'Enable IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context: CommandContext) => {
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
@@ -273,6 +278,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
|
||||
name: 'disable',
|
||||
description: 'Disable IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context: CommandContext) => {
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
|
||||
@@ -17,6 +17,7 @@ export const initCommand: SlashCommand = {
|
||||
name: 'init',
|
||||
description: 'Analyzes the project and creates a tailored GEMINI.md file',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
_args: string,
|
||||
|
||||
@@ -28,6 +28,7 @@ const authCommand: SlashCommand = {
|
||||
name: 'auth',
|
||||
description: 'Authenticate with an OAuth-enabled MCP server',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
@@ -265,6 +266,7 @@ const listCommand: SlashCommand = {
|
||||
altNames: ['ls', 'nodesc', 'nodescription'],
|
||||
description: 'List configured MCP servers and tools',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => listAction(context),
|
||||
};
|
||||
|
||||
@@ -273,6 +275,7 @@ const descCommand: SlashCommand = {
|
||||
altNames: ['description'],
|
||||
description: 'List configured MCP servers and tools with descriptions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => listAction(context, true),
|
||||
};
|
||||
|
||||
@@ -281,6 +284,7 @@ const schemaCommand: SlashCommand = {
|
||||
description:
|
||||
'List configured MCP servers and tools with descriptions and schemas',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => listAction(context, true, true),
|
||||
};
|
||||
|
||||
@@ -288,6 +292,7 @@ const refreshCommand: SlashCommand = {
|
||||
name: 'refresh',
|
||||
description: 'Restarts MCP servers',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
): Promise<void | SlashCommandActionReturn> => {
|
||||
@@ -336,6 +341,7 @@ export const mcpCommand: SlashCommand = {
|
||||
name: 'mcp',
|
||||
description: 'Manage configured Model Context Protocol (MCP) servers',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [
|
||||
listCommand,
|
||||
descCommand,
|
||||
|
||||
@@ -16,11 +16,13 @@ export const memoryCommand: SlashCommand = {
|
||||
name: 'memory',
|
||||
description: 'Commands for interacting with memory',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'show',
|
||||
description: 'Show the current memory contents',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const memoryContent = context.services.config?.getUserMemory() || '';
|
||||
const fileCount = context.services.config?.getGeminiMdFileCount() || 0;
|
||||
@@ -43,6 +45,7 @@ export const memoryCommand: SlashCommand = {
|
||||
name: 'add',
|
||||
description: 'Add content to the memory',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
@@ -71,6 +74,7 @@ export const memoryCommand: SlashCommand = {
|
||||
name: 'refresh',
|
||||
description: 'Refresh the memory from the source',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
context.ui.addItem(
|
||||
{
|
||||
@@ -117,6 +121,7 @@ export const memoryCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
description: 'Lists the paths of the GEMINI.md files in use',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const filePaths = context.services.config?.getGeminiMdFilePaths() || [];
|
||||
const fileCount = filePaths.length;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const modelCommand: SlashCommand = {
|
||||
name: 'model',
|
||||
description: 'Opens a dialog to configure the model',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async () => ({
|
||||
type: 'dialog',
|
||||
dialog: 'model',
|
||||
|
||||
@@ -19,12 +19,14 @@ export const permissionsCommand: SlashCommand = {
|
||||
name: 'permissions',
|
||||
description: 'Manage folder trust settings and other permissions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'trust',
|
||||
description:
|
||||
'Manage folder trust settings. Usage: /permissions trust [<directory-path>]',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: (context, input): SlashCommandActionReturn => {
|
||||
const dirPath = input.trim();
|
||||
let targetDirectory: string;
|
||||
|
||||
@@ -11,6 +11,7 @@ const listPoliciesCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
description: 'List all active policies',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
@@ -69,5 +70,6 @@ export const policiesCommand: SlashCommand = {
|
||||
name: 'policies',
|
||||
description: 'Manage policies',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [listPoliciesCommand],
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export const privacyCommand: SlashCommand = {
|
||||
name: 'privacy',
|
||||
description: 'Display the privacy notice',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'privacy',
|
||||
|
||||
@@ -12,6 +12,7 @@ export const profileCommand: SlashCommand | null = isDevelopment
|
||||
name: 'profile',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
description: 'Toggle the debug profile display',
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
context.ui.toggleDebugProfiler();
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ export const quitCommand: SlashCommand = {
|
||||
altNames: ['exit'],
|
||||
description: 'Exit the cli',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => {
|
||||
const now = Date.now();
|
||||
const { sessionStartTime } = context.session.stats;
|
||||
|
||||
@@ -147,6 +147,7 @@ export const restoreCommand = (config: Config | null): SlashCommand | null => {
|
||||
description:
|
||||
'Restore a tool call. This will reset the conversation and file history to the state it was in when the tool call was suggested',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: restoreAction,
|
||||
completion,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export const resumeCommand: SlashCommand = {
|
||||
name: 'resume',
|
||||
description: 'Browse and resume auto-saved conversations',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
_context: CommandContext,
|
||||
_args: string,
|
||||
|
||||
@@ -11,6 +11,7 @@ export const settingsCommand: SlashCommand = {
|
||||
name: 'settings',
|
||||
description: 'View and edit Gemini CLI settings',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (_context, _args): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'settings',
|
||||
|
||||
@@ -203,6 +203,7 @@ export const setupGithubCommand: SlashCommand = {
|
||||
name: 'setup-github',
|
||||
description: 'Set up GitHub Actions',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
): Promise<SlashCommandActionReturn> => {
|
||||
|
||||
@@ -52,6 +52,7 @@ export const statsCommand: SlashCommand = {
|
||||
altNames: ['usage'],
|
||||
description: 'Check session stats. Usage: /stats [session|model|tools]',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext) => {
|
||||
await defaultSessionView(context);
|
||||
},
|
||||
@@ -60,6 +61,7 @@ export const statsCommand: SlashCommand = {
|
||||
name: 'session',
|
||||
description: 'Show session-specific usage statistics',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context: CommandContext) => {
|
||||
await defaultSessionView(context);
|
||||
},
|
||||
@@ -68,6 +70,7 @@ export const statsCommand: SlashCommand = {
|
||||
name: 'model',
|
||||
description: 'Show model-specific usage statistics',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context: CommandContext) => {
|
||||
context.ui.addItem(
|
||||
{
|
||||
@@ -81,6 +84,7 @@ export const statsCommand: SlashCommand = {
|
||||
name: 'tools',
|
||||
description: 'Show tool-specific usage statistics',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context: CommandContext) => {
|
||||
context.ui.addItem(
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ export const terminalSetupCommand: SlashCommand = {
|
||||
description:
|
||||
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
|
||||
autoExecute: true,
|
||||
action: async (): Promise<MessageActionReturn> => {
|
||||
try {
|
||||
const result = await terminalSetup();
|
||||
|
||||
@@ -11,6 +11,7 @@ export const themeCommand: SlashCommand = {
|
||||
name: 'theme',
|
||||
description: 'Change the theme',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (_context, _args): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'theme',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const toolsCommand: SlashCommand = {
|
||||
name: 'tools',
|
||||
description: 'List available Gemini CLI tools. Usage: /tools [desc]',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext, args?: string): Promise<void> => {
|
||||
const subCommand = args?.trim();
|
||||
|
||||
|
||||
@@ -202,6 +202,14 @@ export interface SlashCommand {
|
||||
|
||||
kind: CommandKind;
|
||||
|
||||
/**
|
||||
* Controls whether the command auto-executes when selected with Enter.
|
||||
*
|
||||
* If true, pressing Enter on the suggestion will execute the command immediately.
|
||||
* If false or undefined, pressing Enter will autocomplete the command into the prompt window.
|
||||
*/
|
||||
autoExecute?: boolean;
|
||||
|
||||
// Optional metadata for extension commands
|
||||
extensionName?: string;
|
||||
extensionId?: string;
|
||||
|
||||
@@ -11,6 +11,7 @@ export const vimCommand: SlashCommand = {
|
||||
name: 'vim',
|
||||
description: 'Toggle vim mode on/off',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, _args) => {
|
||||
const newVimState = await context.ui.toggleVimEnabled();
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -42,6 +42,17 @@ export interface UseCommandCompletionReturn {
|
||||
navigateDown: () => void;
|
||||
handleAutocomplete: (indexToUse: number) => void;
|
||||
promptCompletion: PromptCompletion;
|
||||
getCommandFromSuggestion: (
|
||||
suggestion: Suggestion,
|
||||
) => SlashCommand | undefined;
|
||||
slashCompletionRange: {
|
||||
completionStart: number;
|
||||
completionEnd: number;
|
||||
getCommandFromSuggestion: (
|
||||
suggestion: Suggestion,
|
||||
) => SlashCommand | undefined;
|
||||
};
|
||||
getCompletedText: (suggestion: Suggestion) => string | null;
|
||||
}
|
||||
|
||||
export function useCommandCompletion(
|
||||
@@ -200,12 +211,16 @@ export function useCommandCompletion(
|
||||
setShowSuggestions,
|
||||
]);
|
||||
|
||||
const handleAutocomplete = useCallback(
|
||||
(indexToUse: number) => {
|
||||
if (indexToUse < 0 || indexToUse >= suggestions.length) {
|
||||
return;
|
||||
}
|
||||
const suggestion = suggestions[indexToUse].value;
|
||||
/**
|
||||
* Gets the completed text by replacing the completion range with the suggestion value.
|
||||
* This is the core string replacement logic used by both autocomplete and auto-execute.
|
||||
*
|
||||
* @param suggestion The suggestion to apply
|
||||
* @returns The completed text with the suggestion applied, or null if invalid
|
||||
*/
|
||||
const getCompletedText = useCallback(
|
||||
(suggestion: Suggestion): string | null => {
|
||||
const currentLine = buffer.lines[cursorRow] || '';
|
||||
|
||||
let start = completionStart;
|
||||
let end = completionEnd;
|
||||
@@ -215,10 +230,56 @@ export function useCommandCompletion(
|
||||
}
|
||||
|
||||
if (start === -1 || end === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply space padding for slash commands (needed for subcommands like "/chat list")
|
||||
let suggestionText = suggestion.value;
|
||||
if (completionMode === CompletionMode.SLASH) {
|
||||
// Add leading space if completing a subcommand (cursor is after parent command with no space)
|
||||
if (start === end && start > 1 && currentLine[start - 1] !== ' ') {
|
||||
suggestionText = ' ' + suggestionText;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the completed text with proper spacing
|
||||
return (
|
||||
currentLine.substring(0, start) +
|
||||
suggestionText +
|
||||
currentLine.substring(end)
|
||||
);
|
||||
},
|
||||
[
|
||||
cursorRow,
|
||||
buffer.lines,
|
||||
completionMode,
|
||||
completionStart,
|
||||
completionEnd,
|
||||
slashCompletionRange,
|
||||
],
|
||||
);
|
||||
|
||||
const handleAutocomplete = useCallback(
|
||||
(indexToUse: number) => {
|
||||
if (indexToUse < 0 || indexToUse >= suggestions.length) {
|
||||
return;
|
||||
}
|
||||
const suggestion = suggestions[indexToUse];
|
||||
const completedText = getCompletedText(suggestion);
|
||||
|
||||
if (completedText === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let suggestionText = suggestion;
|
||||
let start = completionStart;
|
||||
let end = completionEnd;
|
||||
if (completionMode === CompletionMode.SLASH) {
|
||||
start = slashCompletionRange.completionStart;
|
||||
end = slashCompletionRange.completionEnd;
|
||||
}
|
||||
|
||||
// Add space padding for Tab completion (auto-execute gets padding from getCompletedText)
|
||||
let suggestionText = suggestion.value;
|
||||
if (completionMode === CompletionMode.SLASH) {
|
||||
if (
|
||||
start === end &&
|
||||
@@ -253,6 +314,7 @@ export function useCommandCompletion(
|
||||
completionStart,
|
||||
completionEnd,
|
||||
slashCompletionRange,
|
||||
getCompletedText,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -270,5 +332,8 @@ export function useCommandCompletion(
|
||||
navigateDown,
|
||||
handleAutocomplete,
|
||||
promptCompletion,
|
||||
getCommandFromSuggestion: slashCompletionRange.getCommandFromSuggestion,
|
||||
slashCompletionRange,
|
||||
getCompletedText,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,6 +376,32 @@ function usePerfectMatch(
|
||||
}, [parserResult]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SlashCommand object for a given suggestion by navigating the command hierarchy
|
||||
* based on the current parser state.
|
||||
* @param suggestion The suggestion object
|
||||
* @param parserResult The current parser result with hierarchy information
|
||||
* @returns The matching SlashCommand or undefined
|
||||
*/
|
||||
function getCommandFromSuggestion(
|
||||
suggestion: Suggestion,
|
||||
parserResult: CommandParserResult,
|
||||
): SlashCommand | undefined {
|
||||
const { currentLevel } = parserResult;
|
||||
|
||||
if (!currentLevel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// suggestion.value is just the command name at the current level (e.g., "list")
|
||||
// Find it in the current level's commands
|
||||
const command = currentLevel.find((cmd) =>
|
||||
matchesCommand(cmd, suggestion.value),
|
||||
);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
export interface UseSlashCompletionProps {
|
||||
enabled: boolean;
|
||||
query: string | null;
|
||||
@@ -389,6 +415,9 @@ export interface UseSlashCompletionProps {
|
||||
export function useSlashCompletion(props: UseSlashCompletionProps): {
|
||||
completionStart: number;
|
||||
completionEnd: number;
|
||||
getCommandFromSuggestion: (
|
||||
suggestion: Suggestion,
|
||||
) => SlashCommand | undefined;
|
||||
} {
|
||||
const {
|
||||
enabled,
|
||||
@@ -536,5 +565,7 @@ export function useSlashCompletion(props: UseSlashCompletionProps): {
|
||||
return {
|
||||
completionStart,
|
||||
completionEnd,
|
||||
getCommandFromSuggestion: (suggestion: Suggestion) =>
|
||||
getCommandFromSuggestion(suggestion, parserResult),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import clipboardy from 'clipboardy';
|
||||
import type { SlashCommand } from '../commands/types.js';
|
||||
|
||||
/**
|
||||
* Checks if a query string potentially represents an '@' command.
|
||||
@@ -72,3 +73,24 @@ export const getUrlOpenCommand = (): string => {
|
||||
}
|
||||
return openCmd;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a slash command should auto-execute when selected.
|
||||
*
|
||||
* All built-in commands have autoExecute explicitly set to true or false.
|
||||
* Custom commands (.toml files) and extension commands without this flag
|
||||
* will default to false (safe default - won't auto-execute).
|
||||
*
|
||||
* @param command The slash command to check
|
||||
* @returns true if the command should auto-execute on Enter
|
||||
*/
|
||||
export function isAutoExecutableCommand(
|
||||
command: SlashCommand | undefined,
|
||||
): boolean {
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simply return the autoExecute flag value, defaulting to false if undefined
|
||||
return command.autoExecute ?? false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user