summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/services/CommandService.test.ts18
-rw-r--r--packages/cli/src/services/CommandService.ts4
-rw-r--r--packages/cli/src/ui/App.tsx1
-rw-r--r--packages/cli/src/ui/commands/toolsCommand.test.ts108
-rw-r--r--packages/cli/src/ui/commands/toolsCommand.ts66
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.test.ts164
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts76
7 files changed, 192 insertions, 245 deletions
diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts
index 5e5e25ae..5c28228e 100644
--- a/packages/cli/src/services/CommandService.test.ts
+++ b/packages/cli/src/services/CommandService.test.ts
@@ -17,8 +17,9 @@ import { themeCommand } from '../ui/commands/themeCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
import { privacyCommand } from '../ui/commands/privacyCommand.js';
import { aboutCommand } from '../ui/commands/aboutCommand.js';
-import { compressCommand } from '../ui/commands/compressCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
+import { toolsCommand } from '../ui/commands/toolsCommand.js';
+import { compressCommand } from '../ui/commands/compressCommand.js';
import { mcpCommand } from '../ui/commands/mcpCommand.js';
// Mock the command modules to isolate the service from the command implementations.
@@ -49,18 +50,21 @@ vi.mock('../ui/commands/statsCommand.js', () => ({
vi.mock('../ui/commands/aboutCommand.js', () => ({
aboutCommand: { name: 'about', description: 'Mock About' },
}));
-vi.mock('../ui/commands/compressCommand.js', () => ({
- compressCommand: { name: 'compress', description: 'Mock Compress' },
-}));
vi.mock('../ui/commands/extensionsCommand.js', () => ({
extensionsCommand: { name: 'extensions', description: 'Mock Extensions' },
}));
+vi.mock('../ui/commands/toolsCommand.js', () => ({
+ toolsCommand: { name: 'tools', description: 'Mock Tools' },
+}));
+vi.mock('../ui/commands/compressCommand.js', () => ({
+ compressCommand: { name: 'compress', description: 'Mock Compress' },
+}));
vi.mock('../ui/commands/mcpCommand.js', () => ({
mcpCommand: { name: 'mcp', description: 'Mock MCP' },
}));
describe('CommandService', () => {
- const subCommandLen = 13;
+ const subCommandLen = 14;
describe('when using default production loader', () => {
let commandService: CommandService;
@@ -98,8 +102,9 @@ describe('CommandService', () => {
expect(commandNames).toContain('stats');
expect(commandNames).toContain('privacy');
expect(commandNames).toContain('about');
- expect(commandNames).toContain('compress');
expect(commandNames).toContain('extensions');
+ expect(commandNames).toContain('tools');
+ expect(commandNames).toContain('compress');
expect(commandNames).toContain('mcp');
});
@@ -140,6 +145,7 @@ describe('CommandService', () => {
privacyCommand,
statsCommand,
themeCommand,
+ toolsCommand,
]);
});
});
diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts
index b9a8df1c..51fe2ad8 100644
--- a/packages/cli/src/services/CommandService.ts
+++ b/packages/cli/src/services/CommandService.ts
@@ -16,8 +16,9 @@ import { chatCommand } from '../ui/commands/chatCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
import { privacyCommand } from '../ui/commands/privacyCommand.js';
import { aboutCommand } from '../ui/commands/aboutCommand.js';
-import { compressCommand } from '../ui/commands/compressCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
+import { toolsCommand } from '../ui/commands/toolsCommand.js';
+import { compressCommand } from '../ui/commands/compressCommand.js';
const loadBuiltInCommands = async (): Promise<SlashCommand[]> => [
aboutCommand,
@@ -33,6 +34,7 @@ const loadBuiltInCommands = async (): Promise<SlashCommand[]> => [
privacyCommand,
statsCommand,
themeCommand,
+ toolsCommand,
];
export class CommandService {
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index f66d8a5b..5e16b449 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -390,7 +390,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
openAuthDialog,
openEditorDialog,
toggleCorgiMode,
- showToolDescriptions,
setQuittingMessages,
openPrivacyNotice,
);
diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts
new file mode 100644
index 00000000..41c5196b
--- /dev/null
+++ b/packages/cli/src/ui/commands/toolsCommand.test.ts
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi } from 'vitest';
+import { toolsCommand } from './toolsCommand.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import { MessageType } from '../types.js';
+import { Tool } from '@google/gemini-cli-core';
+
+// Mock tools for testing
+const mockTools = [
+ {
+ name: 'file-reader',
+ displayName: 'File Reader',
+ description: 'Reads files from the local system.',
+ schema: {},
+ },
+ {
+ name: 'code-editor',
+ displayName: 'Code Editor',
+ description: 'Edits code files.',
+ schema: {},
+ },
+] as Tool[];
+
+describe('toolsCommand', () => {
+ it('should display an error if the tool registry is unavailable', async () => {
+ const mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getToolRegistry: () => Promise.resolve(undefined),
+ },
+ },
+ });
+
+ if (!toolsCommand.action) throw new Error('Action not defined');
+ await toolsCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.ERROR,
+ text: 'Could not retrieve tool registry.',
+ },
+ expect.any(Number),
+ );
+ });
+
+ it('should display "No tools available" when none are found', async () => {
+ const mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getToolRegistry: () =>
+ Promise.resolve({ getAllTools: () => [] as Tool[] }),
+ },
+ },
+ });
+
+ if (!toolsCommand.action) throw new Error('Action not defined');
+ await toolsCommand.action(mockContext, '');
+
+ expect(mockContext.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ text: expect.stringContaining('No tools available'),
+ }),
+ expect.any(Number),
+ );
+ });
+
+ it('should list tools without descriptions by default', async () => {
+ const mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getToolRegistry: () =>
+ Promise.resolve({ getAllTools: () => mockTools }),
+ },
+ },
+ });
+
+ if (!toolsCommand.action) throw new Error('Action not defined');
+ await toolsCommand.action(mockContext, '');
+
+ const message = (mockContext.ui.addItem as vi.Mock).mock.calls[0][0].text;
+ expect(message).not.toContain('Reads files from the local system.');
+ expect(message).toContain('File Reader');
+ expect(message).toContain('Code Editor');
+ });
+
+ it('should list tools with descriptions when "desc" arg is passed', async () => {
+ const mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getToolRegistry: () =>
+ Promise.resolve({ getAllTools: () => mockTools }),
+ },
+ },
+ });
+
+ if (!toolsCommand.action) throw new Error('Action not defined');
+ await toolsCommand.action(mockContext, 'desc');
+
+ const message = (mockContext.ui.addItem as vi.Mock).mock.calls[0][0].text;
+ expect(message).toContain('Reads files from the local system.');
+ expect(message).toContain('Edits code files.');
+ });
+});
diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts
new file mode 100644
index 00000000..f65edd07
--- /dev/null
+++ b/packages/cli/src/ui/commands/toolsCommand.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { type CommandContext, type SlashCommand } from './types.js';
+import { MessageType } from '../types.js';
+
+export const toolsCommand: SlashCommand = {
+ name: 'tools',
+ description: 'list available Gemini CLI tools',
+ 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;
+ }
+
+ const toolRegistry = await context.services.config?.getToolRegistry();
+ if (!toolRegistry) {
+ context.ui.addItem(
+ {
+ type: MessageType.ERROR,
+ text: 'Could not retrieve tool registry.',
+ },
+ Date.now(),
+ );
+ 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));
+
+ let message = 'Available Gemini CLI tools:\n\n';
+
+ if (geminiTools.length > 0) {
+ geminiTools.forEach((tool) => {
+ if (useShowDescriptions && tool.description) {
+ message += ` - \u001b[36m${tool.displayName} (${tool.name})\u001b[0m:\n`;
+
+ const greenColor = '\u001b[32m';
+ const resetColor = '\u001b[0m';
+
+ // Handle multi-line descriptions
+ const descLines = tool.description.trim().split('\n');
+ for (const descLine of descLines) {
+ message += ` ${greenColor}${descLine}${resetColor}\n`;
+ }
+ } else {
+ message += ` - \u001b[36m${tool.displayName}\u001b[0m\n`;
+ }
+ });
+ } else {
+ message += ' No tools available\n';
+ }
+ message += '\n';
+
+ message += '\u001b[0m';
+
+ context.ui.addItem({ type: MessageType.INFO, text: message }, Date.now());
+ },
+};
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
index 3a0428d9..2d7a8ffd 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
@@ -66,7 +66,7 @@ import {
} from 'vitest';
import open from 'open';
import { useSlashCommandProcessor } from './slashCommandProcessor.js';
-import { MessageType, SlashCommandProcessorResult } from '../types.js';
+import { SlashCommandProcessorResult } from '../types.js';
import { Config, GeminiClient } from '@google/gemini-cli-core';
import { useSessionStats } from '../contexts/SessionContext.js';
import { LoadedSettings } from '../../config/settings.js';
@@ -176,7 +176,7 @@ describe('useSlashCommandProcessor', () => {
process.env = { ...globalThis.process.env };
});
- const getProcessorHook = (showToolDescriptions: boolean = false) => {
+ const getProcessorHook = () => {
const settings = {
merged: {
contextFileName: 'GEMINI.md',
@@ -197,15 +197,13 @@ describe('useSlashCommandProcessor', () => {
mockOpenAuthDialog,
mockOpenEditorDialog,
mockCorgiMode,
- showToolDescriptions,
mockSetQuittingMessages,
vi.fn(), // mockOpenPrivacyNotice
),
);
};
- const getProcessor = (showToolDescriptions: boolean = false) =>
- getProcessorHook(showToolDescriptions).result.current;
+ const getProcessor = () => getProcessorHook().result.current;
describe('Other commands', () => {
it('/editor should open editor dialog and return handled', async () => {
@@ -595,160 +593,4 @@ describe('useSlashCommandProcessor', () => {
},
);
});
-
- describe('Unknown command', () => {
- it('should show an error and return handled for a general unknown command', async () => {
- const { handleSlashCommand } = getProcessor();
- let commandResult: SlashCommandProcessorResult | false = false;
- await act(async () => {
- commandResult = await handleSlashCommand('/unknowncommand');
- });
- expect(mockAddItem).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- type: MessageType.ERROR,
- text: 'Unknown command: /unknowncommand',
- }),
- expect.any(Number),
- );
- expect(commandResult).toEqual({ type: 'handled' });
- });
- });
-
- describe('/tools command', () => {
- it('should show an error if tool registry is not available', async () => {
- mockConfig = {
- ...mockConfig,
- getToolRegistry: vi.fn().mockResolvedValue(undefined),
- } as unknown as Config;
- const { handleSlashCommand } = getProcessor();
- let commandResult: SlashCommandProcessorResult | false = false;
- await act(async () => {
- commandResult = await handleSlashCommand('/tools');
- });
-
- expect(mockAddItem).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- type: MessageType.ERROR,
- text: 'Could not retrieve tools.',
- }),
- expect.any(Number),
- );
- expect(commandResult).toEqual({ type: 'handled' });
- });
-
- it('should show an error if getAllTools returns undefined', async () => {
- mockConfig = {
- ...mockConfig,
- getToolRegistry: vi.fn().mockResolvedValue({
- getAllTools: vi.fn().mockReturnValue(undefined),
- }),
- } as unknown as Config;
- const { handleSlashCommand } = getProcessor();
- let commandResult: SlashCommandProcessorResult | false = false;
- await act(async () => {
- commandResult = await handleSlashCommand('/tools');
- });
-
- expect(mockAddItem).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- type: MessageType.ERROR,
- text: 'Could not retrieve tools.',
- }),
- expect.any(Number),
- );
- expect(commandResult).toEqual({ type: 'handled' });
- });
-
- it('should display only Gemini CLI tools (filtering out MCP tools)', async () => {
- // Create mock tools - some with serverName property (MCP tools) and some without (Gemini CLI tools)
- const mockTools = [
- { name: 'tool1', displayName: 'Tool1' },
- { name: 'tool2', displayName: 'Tool2' },
- { name: 'mcp_tool1', serverName: 'mcp-server1' },
- { name: 'mcp_tool2', serverName: 'mcp-server1' },
- ];
-
- mockConfig = {
- ...mockConfig,
- getToolRegistry: vi.fn().mockResolvedValue({
- getAllTools: vi.fn().mockReturnValue(mockTools),
- }),
- } as unknown as Config;
-
- const { handleSlashCommand } = getProcessor();
- let commandResult: SlashCommandProcessorResult | false = false;
- await act(async () => {
- commandResult = await handleSlashCommand('/tools');
- });
-
- // Should only show tool1 and tool2, not the MCP tools
- const message = mockAddItem.mock.calls[1][0].text;
- expect(message).toContain('Tool1');
- expect(message).toContain('Tool2');
- expect(commandResult).toEqual({ type: 'handled' });
- });
-
- it('should display a message when no Gemini CLI tools are available', async () => {
- // Only MCP tools available
- const mockTools = [
- { name: 'mcp_tool1', serverName: 'mcp-server1' },
- { name: 'mcp_tool2', serverName: 'mcp-server1' },
- ];
-
- mockConfig = {
- ...mockConfig,
- getToolRegistry: vi.fn().mockResolvedValue({
- getAllTools: vi.fn().mockReturnValue(mockTools),
- }),
- } as unknown as Config;
-
- const { handleSlashCommand } = getProcessor();
- let commandResult: SlashCommandProcessorResult | false = false;
- await act(async () => {
- commandResult = await handleSlashCommand('/tools');
- });
-
- const message = mockAddItem.mock.calls[1][0].text;
- expect(message).toContain('No tools available');
- expect(commandResult).toEqual({ type: 'handled' });
- });
-
- it('should display tool descriptions when /tools desc is used', async () => {
- const mockTools = [
- {
- name: 'tool1',
- displayName: 'Tool1',
- description: 'Description for Tool1',
- },
- {
- name: 'tool2',
- displayName: 'Tool2',
- description: 'Description for Tool2',
- },
- ];
-
- mockConfig = {
- ...mockConfig,
- getToolRegistry: vi.fn().mockResolvedValue({
- getAllTools: vi.fn().mockReturnValue(mockTools),
- }),
- } as unknown as Config;
-
- const { handleSlashCommand } = getProcessor();
- let commandResult: SlashCommandProcessorResult | false = false;
- await act(async () => {
- commandResult = await handleSlashCommand('/tools desc');
- });
-
- const message = mockAddItem.mock.calls[1][0].text;
- expect(message).toContain('Tool1');
- expect(message).toContain('Description for Tool1');
- expect(message).toContain('Tool2');
- expect(message).toContain('Description for Tool2');
- expect(commandResult).toEqual({ type: 'handled' });
- });
- });
});
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index 8fa3f880..24758842 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -66,7 +66,6 @@ export const useSlashCommandProcessor = (
openAuthDialog: () => void,
openEditorDialog: () => void,
toggleCorgiMode: () => void,
- showToolDescriptions: boolean = false,
setQuittingMessages: (message: HistoryItem[]) => void,
openPrivacyNotice: () => void,
) => {
@@ -206,80 +205,6 @@ export const useSlashCommandProcessor = (
action: (_mainCommand, _subCommand, _args) => openEditorDialog(),
},
{
- name: 'tools',
- description: 'list available Gemini CLI tools',
- action: async (_mainCommand, _subCommand, _args) => {
- // Check if the _subCommand includes a specific flag to control description visibility
- let useShowDescriptions = showToolDescriptions;
- if (_subCommand === 'desc' || _subCommand === 'descriptions') {
- useShowDescriptions = true;
- } else if (
- _subCommand === 'nodesc' ||
- _subCommand === 'nodescriptions'
- ) {
- useShowDescriptions = false;
- } else if (_args === 'desc' || _args === 'descriptions') {
- useShowDescriptions = true;
- } else if (_args === 'nodesc' || _args === 'nodescriptions') {
- useShowDescriptions = false;
- }
-
- const toolRegistry = await config?.getToolRegistry();
- const tools = toolRegistry?.getAllTools();
- if (!tools) {
- addMessage({
- type: MessageType.ERROR,
- content: 'Could not retrieve tools.',
- timestamp: new Date(),
- });
- return;
- }
-
- // Filter out MCP tools by checking if they have a serverName property
- const geminiTools = tools.filter((tool) => !('serverName' in tool));
-
- let message = 'Available Gemini CLI tools:\n\n';
-
- if (geminiTools.length > 0) {
- geminiTools.forEach((tool) => {
- if (useShowDescriptions && tool.description) {
- // Format tool name in cyan using simple ANSI cyan color
- message += ` - \u001b[36m${tool.displayName} (${tool.name})\u001b[0m:\n`;
-
- // Apply green color to the description text
- const greenColor = '\u001b[32m';
- const resetColor = '\u001b[0m';
-
- // Handle multi-line descriptions by properly indenting and preserving formatting
- const descLines = tool.description.trim().split('\n');
-
- // If there are multiple lines, add proper indentation for each line
- if (descLines) {
- for (const descLine of descLines) {
- message += ` ${greenColor}${descLine}${resetColor}\n`;
- }
- }
- } else {
- // Use cyan color for the tool name even when not showing descriptions
- message += ` - \u001b[36m${tool.displayName}\u001b[0m\n`;
- }
- });
- } else {
- message += ' No tools available\n';
- }
- message += '\n';
-
- // Make sure to reset any ANSI formatting at the end to prevent it from affecting the terminal
- message += '\u001b[0m';
-
- addMessage({
- type: MessageType.INFO,
- content: message,
- timestamp: new Date(),
- });
- },
- },
- {
name: 'corgi',
action: (_mainCommand, _subCommand, _args) => {
toggleCorgiMode();
@@ -503,7 +428,6 @@ export const useSlashCommandProcessor = (
openEditorDialog,
toggleCorgiMode,
config,
- showToolDescriptions,
session,
gitService,
loadHistory,