From e45275e1c7c99434dcd6dc5fcb776b950154dacd Mon Sep 17 00:00:00 2001 From: JayadityaGit Date: Thu, 5 Mar 2026 09:52:05 +0530 Subject: [PATCH 1/5] feat(cli): add completion and search capability to /tools command --- .../cli/src/ui/commands/toolsCommand.test.ts | 81 +++++++++++++++++++ packages/cli/src/ui/commands/toolsCommand.ts | 29 ++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index 257e6ba1679..7bcd8fc46e1 100644 --- a/packages/cli/src/ui/commands/toolsCommand.test.ts +++ b/packages/cli/src/ui/commands/toolsCommand.test.ts @@ -110,4 +110,85 @@ describe('toolsCommand', () => { ); expect(message.tools[1].description).toBe('Edits code files.'); }); + + it('should search tools by name and show descriptions', async () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ getAllTools: () => mockTools }), + }, + }, + }); + + if (!toolsCommand.action) throw new Error('Action not defined'); + await toolsCommand.action(mockContext, 'file-reader'); + + const [message] = (mockContext.ui.addItem as ReturnType).mock + .calls[0]; + expect(message.type).toBe(MessageType.TOOLS_LIST); + expect(message.showDescriptions).toBe(true); + expect(message.tools).toHaveLength(1); + expect(message.tools[0].name).toBe('file-reader'); + }); + + it('should search tools by description and show descriptions', async () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ getAllTools: () => mockTools }), + }, + }, + }); + + if (!toolsCommand.action) throw new Error('Action not defined'); + await toolsCommand.action(mockContext, 'edits code'); + + const [message] = (mockContext.ui.addItem as ReturnType).mock + .calls[0]; + expect(message.type).toBe(MessageType.TOOLS_LIST); + expect(message.showDescriptions).toBe(true); + expect(message.tools).toHaveLength(1); + expect(message.tools[0].name).toBe('code-editor'); + }); + + it('should provide completions for "desc", "descriptions", and tool names', async () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ + getAllTools: () => + mockTools as unknown as Array>, + }), + }, + }, + }); + + if (!toolsCommand.completion) throw new Error('Completion not defined'); + const completions = await toolsCommand.completion(mockContext, ''); + + expect(completions).toContain('desc'); + expect(completions).toContain('descriptions'); + expect(completions).toContain('file-reader'); + expect(completions).toContain('code-editor'); + }); + + it('should filter completions based on partial input', async () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ + getAllTools: () => + mockTools as unknown as Array>, + }), + }, + }, + }); + + if (!toolsCommand.completion) throw new Error('Completion not defined'); + const completions = await toolsCommand.completion(mockContext, 'de'); + + expect(completions).toContain('desc'); + expect(completions).toContain('descriptions'); + expect(completions).not.toContain('file-reader'); + }); }); diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index ff772c5cc84..9d08b917062 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -13,7 +13,7 @@ import { MessageType, type HistoryItemToolsList } from '../types.js'; export const toolsCommand: SlashCommand = { name: 'tools', - description: 'List available Gemini CLI tools. Usage: /tools [desc]', + description: 'List available Gemini CLI tools. Usage: /tools [desc|search]', kind: CommandKind.BUILT_IN, autoExecute: false, action: async (context: CommandContext, args?: string): Promise => { @@ -21,8 +21,12 @@ export const toolsCommand: SlashCommand = { // Default to NOT showing descriptions. The user must opt in with an argument. let useShowDescriptions = false; + let searchTerm = ''; + if (subCommand === 'desc' || subCommand === 'descriptions') { useShowDescriptions = true; + } else if (subCommand) { + searchTerm = subCommand.toLowerCase(); } const toolRegistry = context.services.config?.getToolRegistry(); @@ -36,7 +40,18 @@ export const toolsCommand: SlashCommand = { const tools = toolRegistry.getAllTools(); // Filter out MCP tools by checking for the absence of a serverName property - const geminiTools = tools.filter((tool) => !('serverName' in tool)); + let geminiTools = tools.filter((tool) => !('serverName' in tool)); + + if (searchTerm) { + geminiTools = geminiTools.filter( + (tool) => + tool.name.toLowerCase().includes(searchTerm) || + tool.displayName.toLowerCase().includes(searchTerm) || + tool.description.toLowerCase().includes(searchTerm), + ); + // When searching, it's often more useful to see descriptions too. + useShowDescriptions = true; + } const toolsListItem: HistoryItemToolsList = { type: MessageType.TOOLS_LIST, @@ -50,4 +65,14 @@ export const toolsCommand: SlashCommand = { context.ui.addItem(toolsListItem); }, + completion: (context: CommandContext, partialArg: string) => { + const toolRegistry = context.services.config?.getToolRegistry(); + const suggestions = ['desc', 'descriptions']; + if (toolRegistry) { + const tools = toolRegistry.getAllTools(); + const geminiTools = tools.filter((tool) => !('serverName' in tool)); + suggestions.push(...geminiTools.map((t) => t.name)); + } + return suggestions.filter((s) => s.startsWith(partialArg)); + }, }; From b00e34144646f9a875f8659c3d16b7018e987691 Mon Sep 17 00:00:00 2001 From: JayadityaGit Date: Thu, 5 Mar 2026 10:21:43 +0530 Subject: [PATCH 2/5] refactor(cli): simplify /tools with list and desc subcommands --- .../cli/src/ui/commands/toolsCommand.test.ts | 81 ++++-------- packages/cli/src/ui/commands/toolsCommand.ts | 125 ++++++++++-------- 2 files changed, 99 insertions(+), 107 deletions(-) diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index 7bcd8fc46e1..8606e4253df 100644 --- a/packages/cli/src/ui/commands/toolsCommand.test.ts +++ b/packages/cli/src/ui/commands/toolsCommand.test.ts @@ -67,7 +67,7 @@ describe('toolsCommand', () => { }); }); - it('should list tools without descriptions by default', async () => { + it('should list tools without descriptions by default (no args)', async () => { const mockContext = createMockCommandContext({ services: { config: { @@ -84,11 +84,9 @@ describe('toolsCommand', () => { expect(message.type).toBe(MessageType.TOOLS_LIST); expect(message.showDescriptions).toBe(false); expect(message.tools).toHaveLength(2); - expect(message.tools[0].displayName).toBe('File Reader'); - expect(message.tools[1].displayName).toBe('Code Editor'); }); - it('should list tools with descriptions when "desc" arg is passed', async () => { + it('should list tools without descriptions when "list" arg is passed', async () => { const mockContext = createMockCommandContext({ services: { config: { @@ -98,20 +96,15 @@ describe('toolsCommand', () => { }); if (!toolsCommand.action) throw new Error('Action not defined'); - await toolsCommand.action(mockContext, 'desc'); + await toolsCommand.action(mockContext, 'list'); const [message] = (mockContext.ui.addItem as ReturnType).mock .calls[0]; expect(message.type).toBe(MessageType.TOOLS_LIST); - expect(message.showDescriptions).toBe(true); - expect(message.tools).toHaveLength(2); - expect(message.tools[0].description).toBe( - 'Reads files from the local system.', - ); - expect(message.tools[1].description).toBe('Edits code files.'); + expect(message.showDescriptions).toBe(false); }); - it('should search tools by name and show descriptions', async () => { + it('should list tools with descriptions when "desc" arg is passed', async () => { const mockContext = createMockCommandContext({ services: { config: { @@ -121,17 +114,24 @@ describe('toolsCommand', () => { }); if (!toolsCommand.action) throw new Error('Action not defined'); - await toolsCommand.action(mockContext, 'file-reader'); + await toolsCommand.action(mockContext, 'desc'); const [message] = (mockContext.ui.addItem as ReturnType).mock .calls[0]; expect(message.type).toBe(MessageType.TOOLS_LIST); expect(message.showDescriptions).toBe(true); - expect(message.tools).toHaveLength(1); - expect(message.tools[0].name).toBe('file-reader'); + expect(message.tools).toHaveLength(2); }); - it('should search tools by description and show descriptions', async () => { + it('should have "list" and "desc" subcommands', () => { + expect(toolsCommand.subCommands).toBeDefined(); + const names = toolsCommand.subCommands?.map((s) => s.name); + expect(names).toContain('list'); + expect(names).toContain('desc'); + expect(names).not.toContain('descriptions'); + }); + + it('subcommand "list" should display tools without descriptions', async () => { const mockContext = createMockCommandContext({ services: { config: { @@ -140,55 +140,30 @@ describe('toolsCommand', () => { }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); - await toolsCommand.action(mockContext, 'edits code'); + const listCmd = toolsCommand.subCommands?.find((s) => s.name === 'list'); + if (!listCmd?.action) throw new Error('Action not defined'); + await listCmd.action(mockContext, ''); const [message] = (mockContext.ui.addItem as ReturnType).mock .calls[0]; - expect(message.type).toBe(MessageType.TOOLS_LIST); - expect(message.showDescriptions).toBe(true); - expect(message.tools).toHaveLength(1); - expect(message.tools[0].name).toBe('code-editor'); - }); - - it('should provide completions for "desc", "descriptions", and tool names', async () => { - const mockContext = createMockCommandContext({ - services: { - config: { - getToolRegistry: () => ({ - getAllTools: () => - mockTools as unknown as Array>, - }), - }, - }, - }); - - if (!toolsCommand.completion) throw new Error('Completion not defined'); - const completions = await toolsCommand.completion(mockContext, ''); - - expect(completions).toContain('desc'); - expect(completions).toContain('descriptions'); - expect(completions).toContain('file-reader'); - expect(completions).toContain('code-editor'); + expect(message.showDescriptions).toBe(false); }); - it('should filter completions based on partial input', async () => { + it('subcommand "desc" should display tools with descriptions', async () => { const mockContext = createMockCommandContext({ services: { config: { - getToolRegistry: () => ({ - getAllTools: () => - mockTools as unknown as Array>, - }), + getToolRegistry: () => ({ getAllTools: () => mockTools }), }, }, }); - if (!toolsCommand.completion) throw new Error('Completion not defined'); - const completions = await toolsCommand.completion(mockContext, 'de'); + const descCmd = toolsCommand.subCommands?.find((s) => s.name === 'desc'); + if (!descCmd?.action) throw new Error('Action not defined'); + await descCmd.action(mockContext, ''); - expect(completions).toContain('desc'); - expect(completions).toContain('descriptions'); - expect(completions).not.toContain('file-reader'); + const [message] = (mockContext.ui.addItem as ReturnType).mock + .calls[0]; + expect(message.showDescriptions).toBe(true); }); }); diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index 9d08b917062..87afa677920 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -11,68 +11,85 @@ import { } from './types.js'; import { MessageType, type HistoryItemToolsList } from '../types.js'; -export const toolsCommand: SlashCommand = { - name: 'tools', - description: 'List available Gemini CLI tools. Usage: /tools [desc|search]', - kind: CommandKind.BUILT_IN, - autoExecute: false, - action: async (context: CommandContext, args?: string): Promise => { - const subCommand = args?.trim(); +const getToolsList = (context: CommandContext, showDescriptions: boolean) => { + const toolRegistry = context.services.config?.getToolRegistry(); + if (!toolRegistry) { + context.ui.addItem({ + type: MessageType.ERROR, + text: 'Could not retrieve tool registry.', + }); + return null; + } - // Default to NOT showing descriptions. The user must opt in with an argument. - let useShowDescriptions = false; - let searchTerm = ''; + const tools = toolRegistry.getAllTools(); + // Filter out MCP tools by checking for the absence of a serverName property + const geminiTools = tools.filter((tool) => !('serverName' in tool)); - if (subCommand === 'desc' || subCommand === 'descriptions') { - useShowDescriptions = true; - } else if (subCommand) { - searchTerm = subCommand.toLowerCase(); - } + const toolsListItem: HistoryItemToolsList = { + type: MessageType.TOOLS_LIST, + tools: geminiTools.map((tool) => ({ + name: tool.name, + displayName: tool.displayName, + description: tool.description, + })), + showDescriptions, + }; - const toolRegistry = context.services.config?.getToolRegistry(); - if (!toolRegistry) { - context.ui.addItem({ - type: MessageType.ERROR, - text: 'Could not retrieve tool registry.', - }); - return; - } + return toolsListItem; +}; - const tools = toolRegistry.getAllTools(); - // Filter out MCP tools by checking for the absence of a serverName property - let geminiTools = tools.filter((tool) => !('serverName' in tool)); +const listSubCommand: SlashCommand = { + name: 'list', + description: 'List available Gemini CLI tools', + kind: CommandKind.BUILT_IN, + autoExecute: true, + action: async (context: CommandContext): Promise => { + const item = getToolsList(context, false); + if (item) { + context.ui.addItem(item); + } + }, +}; - if (searchTerm) { - geminiTools = geminiTools.filter( - (tool) => - tool.name.toLowerCase().includes(searchTerm) || - tool.displayName.toLowerCase().includes(searchTerm) || - tool.description.toLowerCase().includes(searchTerm), - ); - // When searching, it's often more useful to see descriptions too. - useShowDescriptions = true; +const descSubCommand: SlashCommand = { + name: 'desc', + description: 'List available Gemini CLI tools with descriptions', + kind: CommandKind.BUILT_IN, + autoExecute: true, + action: async (context: CommandContext): Promise => { + const item = getToolsList(context, true); + if (item) { + context.ui.addItem(item); } + }, +}; - const toolsListItem: HistoryItemToolsList = { - type: MessageType.TOOLS_LIST, - tools: geminiTools.map((tool) => ({ - name: tool.name, - displayName: tool.displayName, - description: tool.description, - })), - showDescriptions: useShowDescriptions, - }; +export const toolsCommand: SlashCommand = { + name: 'tools', + description: 'List available Gemini CLI tools', + kind: CommandKind.BUILT_IN, + autoExecute: false, + subCommands: [listSubCommand, descSubCommand], + action: async (context: CommandContext, args?: string): Promise => { + const subCommand = args?.trim(); - context.ui.addItem(toolsListItem); - }, - completion: (context: CommandContext, partialArg: string) => { - const toolRegistry = context.services.config?.getToolRegistry(); - const suggestions = ['desc', 'descriptions']; - if (toolRegistry) { - const tools = toolRegistry.getAllTools(); - const geminiTools = tools.filter((tool) => !('serverName' in tool)); - suggestions.push(...geminiTools.map((t) => t.name)); + if (subCommand === 'desc') { + const item = getToolsList(context, true); + if (item) { + context.ui.addItem(item); + } + } else if (!subCommand || subCommand === 'list') { + const item = getToolsList(context, false); + if (item) { + context.ui.addItem(item); + } + } else { + // For any other argument, default to list or let the UI handle it if it was a subcommand. + // But since we want to be strict and simple: + const item = getToolsList(context, false); + if (item) { + context.ui.addItem(item); + } } - return suggestions.filter((s) => s.startsWith(partialArg)); }, }; From 2aae74a125e21dd370657bbb842a6eab670293b4 Mon Sep 17 00:00:00 2001 From: JayadityaGit Date: Mon, 9 Mar 2026 10:58:38 +0530 Subject: [PATCH 3/5] refactor(cli): delegate /tools action to subcommands and strengthen tests --- .../cli/src/ui/commands/toolsCommand.test.ts | 13 ++++++++++++ packages/cli/src/ui/commands/toolsCommand.ts | 20 +++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index 8606e4253df..af75f025248 100644 --- a/packages/cli/src/ui/commands/toolsCommand.test.ts +++ b/packages/cli/src/ui/commands/toolsCommand.test.ts @@ -84,6 +84,8 @@ describe('toolsCommand', () => { expect(message.type).toBe(MessageType.TOOLS_LIST); expect(message.showDescriptions).toBe(false); expect(message.tools).toHaveLength(2); + expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[1].displayName).toBe('Code Editor'); }); it('should list tools without descriptions when "list" arg is passed', async () => { @@ -102,6 +104,9 @@ describe('toolsCommand', () => { .calls[0]; expect(message.type).toBe(MessageType.TOOLS_LIST); expect(message.showDescriptions).toBe(false); + expect(message.tools).toHaveLength(2); + expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[1].displayName).toBe('Code Editor'); }); it('should list tools with descriptions when "desc" arg is passed', async () => { @@ -121,6 +126,8 @@ describe('toolsCommand', () => { expect(message.type).toBe(MessageType.TOOLS_LIST); expect(message.showDescriptions).toBe(true); expect(message.tools).toHaveLength(2); + expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[1].displayName).toBe('Code Editor'); }); it('should have "list" and "desc" subcommands', () => { @@ -147,6 +154,9 @@ describe('toolsCommand', () => { const [message] = (mockContext.ui.addItem as ReturnType).mock .calls[0]; expect(message.showDescriptions).toBe(false); + expect(message.tools).toHaveLength(2); + expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[1].displayName).toBe('Code Editor'); }); it('subcommand "desc" should display tools with descriptions', async () => { @@ -165,5 +175,8 @@ describe('toolsCommand', () => { const [message] = (mockContext.ui.addItem as ReturnType).mock .calls[0]; expect(message.showDescriptions).toBe(true); + expect(message.tools).toHaveLength(2); + expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[1].displayName).toBe('Code Editor'); }); }); diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index 87afa677920..8d6f17c7063 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -74,22 +74,12 @@ export const toolsCommand: SlashCommand = { const subCommand = args?.trim(); if (subCommand === 'desc') { - const item = getToolsList(context, true); - if (item) { - context.ui.addItem(item); - } - } else if (!subCommand || subCommand === 'list') { - const item = getToolsList(context, false); - if (item) { - context.ui.addItem(item); - } + // Delegate to the subcommand's action for consistency. + // The action is guaranteed to exist on our own subcommand definition. + await descSubCommand.action!(context, ''); } else { - // For any other argument, default to list or let the UI handle it if it was a subcommand. - // But since we want to be strict and simple: - const item = getToolsList(context, false); - if (item) { - context.ui.addItem(item); - } + // Default to 'list' for no subcommand, 'list', or any other invalid subcommand. + await listSubCommand.action!(context, ''); } }, }; From 2ad105559b53a4a331ca63b572d3310135ce11d2 Mon Sep 17 00:00:00 2001 From: JayadityaGit Date: Mon, 9 Mar 2026 11:06:21 +0530 Subject: [PATCH 4/5] test(cli): add description assertions to /tools desc tests --- packages/cli/src/ui/commands/toolsCommand.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index af75f025248..e2507250575 100644 --- a/packages/cli/src/ui/commands/toolsCommand.test.ts +++ b/packages/cli/src/ui/commands/toolsCommand.test.ts @@ -127,7 +127,11 @@ describe('toolsCommand', () => { expect(message.showDescriptions).toBe(true); expect(message.tools).toHaveLength(2); expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[0].description).toBe( + 'Reads files from the local system.', + ); expect(message.tools[1].displayName).toBe('Code Editor'); + expect(message.tools[1].description).toBe('Edits code files.'); }); it('should have "list" and "desc" subcommands', () => { @@ -177,6 +181,10 @@ describe('toolsCommand', () => { expect(message.showDescriptions).toBe(true); expect(message.tools).toHaveLength(2); expect(message.tools[0].displayName).toBe('File Reader'); + expect(message.tools[0].description).toBe( + 'Reads files from the local system.', + ); expect(message.tools[1].displayName).toBe('Code Editor'); + expect(message.tools[1].description).toBe('Edits code files.'); }); }); From 8a52309e46dc3f2e51373c81238376f5fb24bf91 Mon Sep 17 00:00:00 2001 From: JayadityaGit Date: Mon, 9 Mar 2026 11:13:35 +0530 Subject: [PATCH 5/5] refactor(cli): implement dynamic subcommand routing for /tools --- packages/cli/src/ui/commands/toolsCommand.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index 8d6f17c7063..3d1d9d4d1f0 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -71,15 +71,14 @@ export const toolsCommand: SlashCommand = { autoExecute: false, subCommands: [listSubCommand, descSubCommand], action: async (context: CommandContext, args?: string): Promise => { - const subCommand = args?.trim(); + const subCommandName = args?.trim().split(' ')[0]; - if (subCommand === 'desc') { - // Delegate to the subcommand's action for consistency. - // The action is guaranteed to exist on our own subcommand definition. - await descSubCommand.action!(context, ''); - } else { - // Default to 'list' for no subcommand, 'list', or any other invalid subcommand. - await listSubCommand.action!(context, ''); - } + // Find the subcommand, defaulting to 'list' if not found or not provided. + const subCommandToRun = + toolsCommand.subCommands?.find((cmd) => cmd.name === subCommandName) ?? + listSubCommand; + + // The action is guaranteed to exist on our own subcommand definitions. + await subCommandToRun.action!(context, ''); }, };