From 06fcdc231c32c2d336129c79be8b4a95f52c958e Mon Sep 17 00:00:00 2001 From: Sri Pasumarthi <111310667+sripasg@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:01:44 -0700 Subject: [PATCH] feat(acp): add /help command (#24839) --- packages/cli/src/acp/commandHandler.test.ts | 3 ++ packages/cli/src/acp/commandHandler.ts | 2 + packages/cli/src/acp/commands/help.test.ts | 53 +++++++++++++++++++++ packages/cli/src/acp/commands/help.ts | 50 +++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 packages/cli/src/acp/commands/help.test.ts create mode 100644 packages/cli/src/acp/commands/help.ts diff --git a/packages/cli/src/acp/commandHandler.test.ts b/packages/cli/src/acp/commandHandler.test.ts index 23bf907ec3..4a1ce6d2e5 100644 --- a/packages/cli/src/acp/commandHandler.test.ts +++ b/packages/cli/src/acp/commandHandler.test.ts @@ -29,5 +29,8 @@ describe('CommandHandler', () => { const about = parse('/about'); expect(about.commandToExecute?.name).toBe('about'); + + const help = parse('/help'); + expect(help.commandToExecute?.name).toBe('help'); }); }); diff --git a/packages/cli/src/acp/commandHandler.ts b/packages/cli/src/acp/commandHandler.ts index 4ed846188e..b35512adb2 100644 --- a/packages/cli/src/acp/commandHandler.ts +++ b/packages/cli/src/acp/commandHandler.ts @@ -11,6 +11,7 @@ import { ExtensionsCommand } from './commands/extensions.js'; import { InitCommand } from './commands/init.js'; import { RestoreCommand } from './commands/restore.js'; import { AboutCommand } from './commands/about.js'; +import { HelpCommand } from './commands/help.js'; export class CommandHandler { private registry: CommandRegistry; @@ -26,6 +27,7 @@ export class CommandHandler { registry.register(new InitCommand()); registry.register(new RestoreCommand()); registry.register(new AboutCommand()); + registry.register(new HelpCommand(registry)); return registry; } diff --git a/packages/cli/src/acp/commands/help.test.ts b/packages/cli/src/acp/commands/help.test.ts new file mode 100644 index 0000000000..c703328a42 --- /dev/null +++ b/packages/cli/src/acp/commands/help.test.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { HelpCommand } from './help.js'; +import { CommandRegistry } from './commandRegistry.js'; +import type { Command, CommandContext } from './types.js'; + +describe('HelpCommand', () => { + it('returns formatted help text with sorted commands', async () => { + const registry = new CommandRegistry(); + + const cmdB: Command = { + name: 'bravo', + description: 'Bravo command', + execute: async () => ({ name: 'bravo', data: '' }), + }; + + const cmdA: Command = { + name: 'alpha', + description: 'Alpha command', + execute: async () => ({ name: 'alpha', data: '' }), + }; + + registry.register(cmdB); + registry.register(cmdA); + + const helpCommand = new HelpCommand(registry); + + const context = {} as CommandContext; + + const response = await helpCommand.execute(context, []); + + expect(response.name).toBe('help'); + + const data = response.data as string; + + expect(data).toContain('Gemini CLI Help:'); + expect(data).toContain('### Basics'); + expect(data).toContain('### Commands'); + + const lines = data.split('\n'); + const alphaIndex = lines.findIndex((l) => l.includes('/alpha')); + const bravoIndex = lines.findIndex((l) => l.includes('/bravo')); + + expect(alphaIndex).toBeLessThan(bravoIndex); + expect(alphaIndex).not.toBe(-1); + expect(bravoIndex).not.toBe(-1); + }); +}); diff --git a/packages/cli/src/acp/commands/help.ts b/packages/cli/src/acp/commands/help.ts new file mode 100644 index 0000000000..0c0729ecab --- /dev/null +++ b/packages/cli/src/acp/commands/help.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + Command, + CommandContext, + CommandExecutionResponse, +} from './types.js'; +import type { CommandRegistry } from './commandRegistry.js'; + +export class HelpCommand implements Command { + readonly name = 'help'; + readonly description = 'Show available commands'; + + constructor(private registry: CommandRegistry) {} + + async execute( + _context: CommandContext, + _args: string[] = [], + ): Promise { + const commands = this.registry + .getAllCommands() + .sort((a, b) => a.name.localeCompare(b.name)); + + const lines: string[] = []; + + lines.push('Gemini CLI Help:'); + lines.push(''); + lines.push('### Basics'); + lines.push( + '- **Add context**: Use `@` to specify files for context (e.g., `@src/myFile.ts`) to target specific files or folders.', + ); + lines.push(''); + + lines.push('### Commands'); + for (const cmd of commands) { + if (cmd.description) { + lines.push(`- **/${cmd.name}** - ${cmd.description}`); + } + } + + return { + name: this.name, + data: lines.join('\n'), + }; + } +}