From 556825f81c6d49063985b421d9fdd4b14c936979 Mon Sep 17 00:00:00 2001 From: JAYADITYA <96861162+JayadityaGit@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:06:12 +0530 Subject: [PATCH] feat(cli): give visibility to /tools list command in the TUI and follow the subcommand pattern of other commands (#21213) --- .../cli/src/ui/commands/toolsCommand.test.ts | 79 ++++++++++++++++++- packages/cli/src/ui/commands/toolsCommand.ts | 15 +++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index cfb6d4368e..1e5b0feb90 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: { @@ -88,6 +88,27 @@ describe('toolsCommand', () => { expect(message.tools[1].displayName).toBe('Code Editor'); }); + it('should list tools without descriptions when "list" arg is passed', async () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ getAllTools: () => mockTools }), + }, + }, + }); + + if (!toolsCommand.action) throw new Error('Action not defined'); + 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(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 () => { const mockContext = createMockCommandContext({ services: { @@ -105,9 +126,65 @@ 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[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', () => { + 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: { + getToolRegistry: () => ({ getAllTools: () => mockTools }), + }, + }, + }); + + 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.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 () => { + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ getAllTools: () => mockTools }), + }, + }, + }); + + const descCmd = toolsCommand.subCommands?.find((s) => s.name === 'desc'); + if (!descCmd?.action) throw new Error('Action not defined'); + await descCmd.action(mockContext, ''); + + 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[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.'); }); diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index 6a26d4f3d6..082da26fab 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -41,7 +41,16 @@ async function listTools( context.ui.addItem(toolsListItem); } -const toolsDescSubCommand: SlashCommand = { +const listSubCommand: SlashCommand = { + name: 'list', + description: 'List available Gemini CLI tools.', + kind: CommandKind.BUILT_IN, + autoExecute: true, + action: async (context: CommandContext): Promise => + listTools(context, false), +}; + +const descSubCommand: SlashCommand = { name: 'desc', altNames: ['descriptions'], description: 'List available Gemini CLI tools with descriptions.', @@ -57,11 +66,11 @@ export const toolsCommand: SlashCommand = { 'List available Gemini CLI tools. Use /tools desc to include descriptions.', kind: CommandKind.BUILT_IN, autoExecute: false, - subCommands: [toolsDescSubCommand], + subCommands: [listSubCommand, descSubCommand], action: async (context: CommandContext, args?: string): Promise => { const subCommand = args?.trim(); - // Keep backward compatibility for typed arguments while exposing desc in TUI via subcommands. + // Keep backward compatibility for typed arguments while exposing subcommands in TUI. const useShowDescriptions = subCommand === 'desc' || subCommand === 'descriptions';