feat(cli): give visibility to /tools list command in the TUI and follow the subcommand pattern of other commands (#21213)

This commit is contained in:
JAYADITYA
2026-03-10 22:06:12 +05:30
committed by GitHub
parent b158c96465
commit 556825f81c
2 changed files with 90 additions and 4 deletions

View File

@@ -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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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.');
});

View File

@@ -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<void> =>
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<void> => {
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';