feat(cli): expose /tools desc as explicit subcommand for discoverability (#21241)

Co-authored-by: Coco Sheng <cocosheng@google.com>
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
This commit is contained in:
aworki
2026-03-09 23:31:05 +08:00
committed by GitHub
parent 0f019122b0
commit 37ffd608fd
2 changed files with 71 additions and 30 deletions

View File

@@ -110,4 +110,28 @@ describe('toolsCommand', () => {
);
expect(message.tools[1].description).toBe('Edits code files.');
});
it('should expose a desc subcommand for TUI discoverability', async () => {
const descSubCommand = toolsCommand.subCommands?.find(
(cmd) => cmd.name === 'desc',
);
expect(descSubCommand).toBeDefined();
expect(descSubCommand?.description).toContain('descriptions');
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
if (!descSubCommand?.action) throw new Error('Action not defined');
await descSubCommand.action(mockContext, '');
const [message] = (mockContext.ui.addItem as ReturnType<typeof vi.fn>).mock
.calls[0];
expect(message.type).toBe(MessageType.TOOLS_LIST);
expect(message.showDescriptions).toBe(true);
});
});

View File

@@ -11,43 +11,60 @@ import {
} from './types.js';
import { MessageType, type HistoryItemToolsList } from '../types.js';
async function listTools(
context: CommandContext,
showDescriptions: boolean,
): Promise<void> {
const toolRegistry = context.services.config?.getToolRegistry();
if (!toolRegistry) {
context.ui.addItem({
type: MessageType.ERROR,
text: 'Could not retrieve tool registry.',
});
return;
}
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));
const toolsListItem: HistoryItemToolsList = {
type: MessageType.TOOLS_LIST,
tools: geminiTools.map((tool) => ({
name: tool.name,
displayName: tool.displayName,
description: tool.description,
})),
showDescriptions,
};
context.ui.addItem(toolsListItem);
}
const toolsDescSubCommand: SlashCommand = {
name: 'desc',
altNames: ['descriptions'],
description: 'List available Gemini CLI tools with descriptions.',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext): Promise<void> =>
listTools(context, true),
};
export const toolsCommand: SlashCommand = {
name: 'tools',
description: 'List available Gemini CLI tools. Usage: /tools [desc]',
description:
'List available Gemini CLI tools. Use /tools desc to include descriptions.',
kind: CommandKind.BUILT_IN,
autoExecute: false,
subCommands: [toolsDescSubCommand],
action: async (context: CommandContext, args?: string): Promise<void> => {
const subCommand = args?.trim();
// Default to NOT showing descriptions. The user must opt in with an argument.
let useShowDescriptions = false;
if (subCommand === 'desc' || subCommand === 'descriptions') {
useShowDescriptions = true;
}
// Keep backward compatibility for typed arguments while exposing desc in TUI via subcommands.
const useShowDescriptions =
subCommand === 'desc' || subCommand === 'descriptions';
const toolRegistry = context.services.config?.getToolRegistry();
if (!toolRegistry) {
context.ui.addItem({
type: MessageType.ERROR,
text: 'Could not retrieve tool registry.',
});
return;
}
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));
const toolsListItem: HistoryItemToolsList = {
type: MessageType.TOOLS_LIST,
tools: geminiTools.map((tool) => ({
name: tool.name,
displayName: tool.displayName,
description: tool.description,
})),
showDescriptions: useShowDescriptions,
};
context.ui.addItem(toolsListItem);
await listTools(context, useShowDescriptions);
},
};