feat: add /commands reload to refresh custom TOML commands (#19078)

This commit is contained in:
Krushna Korade
2026-02-15 01:55:30 +05:30
committed by GitHub
parent 5559d40f31
commit bcd547baf6
5 changed files with 152 additions and 0 deletions

View File

@@ -71,6 +71,17 @@ Slash commands provide meta-level control over the CLI itself.
the visual display is cleared. the visual display is cleared.
- **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action. - **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action.
### `/commands`
- **Description:** Manage custom slash commands loaded from `.toml` files.
- **Sub-commands:**
- **`reload`**:
- **Description:** Reload custom command definitions from all sources
(user-level `~/.gemini/commands/`, project-level
`<project>/.gemini/commands/`, MCP prompts, and extensions). Use this to
pick up new or modified `.toml` files without restarting the CLI.
- **Usage:** `/commands reload`
### `/compress` ### `/compress`
- **Description:** Replace the entire chat context with a summary. This saves on - **Description:** Replace the entire chat context with a summary. This saves on

View File

@@ -30,6 +30,9 @@ separator (`/` or `\`) being converted to a colon (`:`).
- A file at `<project>/.gemini/commands/git/commit.toml` becomes the namespaced - A file at `<project>/.gemini/commands/git/commit.toml` becomes the namespaced
command `/git:commit`. command `/git:commit`.
> [!TIP] After creating or modifying `.toml` command files, run
> `/commands reload` to pick up your changes without restarting the CLI.
## TOML file format (v1) ## TOML file format (v1)
Your command definition files must be written in the TOML format and use the Your command definition files must be written in the TOML format and use the

View File

@@ -23,6 +23,7 @@ import { authCommand } from '../ui/commands/authCommand.js';
import { bugCommand } from '../ui/commands/bugCommand.js'; import { bugCommand } from '../ui/commands/bugCommand.js';
import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js'; import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js'; import { clearCommand } from '../ui/commands/clearCommand.js';
import { commandsCommand } from '../ui/commands/commandsCommand.js';
import { compressCommand } from '../ui/commands/compressCommand.js'; import { compressCommand } from '../ui/commands/compressCommand.js';
import { copyCommand } from '../ui/commands/copyCommand.js'; import { copyCommand } from '../ui/commands/copyCommand.js';
import { corgiCommand } from '../ui/commands/corgiCommand.js'; import { corgiCommand } from '../ui/commands/corgiCommand.js';
@@ -89,6 +90,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
: chatCommand.subCommands, : chatCommand.subCommands,
}, },
clearCommand, clearCommand,
commandsCommand,
compressCommand, compressCommand,
copyCommand, copyCommand,
corgiCommand, corgiCommand,

View File

@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { commandsCommand } from './commandsCommand.js';
import { MessageType } from '../types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import type { CommandContext } from './types.js';
describe('commandsCommand', () => {
let context: CommandContext;
beforeEach(() => {
vi.clearAllMocks();
context = createMockCommandContext({
ui: {
reloadCommands: vi.fn(),
},
});
});
describe('default action', () => {
it('should return an info message prompting subcommand usage', async () => {
const result = await commandsCommand.action!(context, '');
expect(result).toEqual({
type: 'message',
messageType: 'info',
content:
'Use "/commands reload" to reload custom command definitions from .toml files.',
});
});
});
describe('reload', () => {
it('should call reloadCommands and show a success message', async () => {
const reloadCmd = commandsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
await reloadCmd.action!(context, '');
expect(context.ui.reloadCommands).toHaveBeenCalledTimes(1);
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'Custom commands reloaded successfully.',
}),
expect.any(Number),
);
});
});
});

View File

@@ -0,0 +1,80 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type CommandContext,
type SlashCommand,
type SlashCommandActionReturn,
CommandKind,
} from './types.js';
import {
MessageType,
type HistoryItemError,
type HistoryItemInfo,
} from '../types.js';
/**
* Action for the default `/commands` invocation.
* Displays a message prompting the user to use a subcommand.
*/
async function listAction(
_context: CommandContext,
_args: string,
): Promise<void | SlashCommandActionReturn> {
return {
type: 'message',
messageType: 'info',
content:
'Use "/commands reload" to reload custom command definitions from .toml files.',
};
}
/**
* Action for `/commands reload`.
* Triggers a full re-discovery and reload of all slash commands, including
* user/project-level .toml files, MCP prompts, and extension commands.
*/
async function reloadAction(
context: CommandContext,
): Promise<void | SlashCommandActionReturn> {
try {
context.ui.reloadCommands();
context.ui.addItem(
{
type: MessageType.INFO,
text: 'Custom commands reloaded successfully.',
} as HistoryItemInfo,
Date.now(),
);
} catch (error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to reload commands: ${error instanceof Error ? error.message : String(error)}`,
} as HistoryItemError,
Date.now(),
);
}
}
export const commandsCommand: SlashCommand = {
name: 'commands',
description: 'Manage custom slash commands. Usage: /commands [reload]',
kind: CommandKind.BUILT_IN,
autoExecute: false,
subCommands: [
{
name: 'reload',
description:
'Reload custom command definitions from .toml files. Usage: /commands reload',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: reloadAction,
},
],
action: listAction,
};