Skip to content

Commit bcd547b

Browse files
feat: add /commands reload to refresh custom TOML commands (#19078)
1 parent 5559d40 commit bcd547b

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed

docs/cli/commands.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ Slash commands provide meta-level control over the CLI itself.
7171
the visual display is cleared.
7272
- **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action.
7373

74+
### `/commands`
75+
76+
- **Description:** Manage custom slash commands loaded from `.toml` files.
77+
- **Sub-commands:**
78+
- **`reload`**:
79+
- **Description:** Reload custom command definitions from all sources
80+
(user-level `~/.gemini/commands/`, project-level
81+
`<project>/.gemini/commands/`, MCP prompts, and extensions). Use this to
82+
pick up new or modified `.toml` files without restarting the CLI.
83+
- **Usage:** `/commands reload`
84+
7485
### `/compress`
7586

7687
- **Description:** Replace the entire chat context with a summary. This saves on

docs/cli/custom-commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ separator (`/` or `\`) being converted to a colon (`:`).
3030
- A file at `<project>/.gemini/commands/git/commit.toml` becomes the namespaced
3131
command `/git:commit`.
3232

33+
> [!TIP] After creating or modifying `.toml` command files, run
34+
> `/commands reload` to pick up your changes without restarting the CLI.
35+
3336
## TOML file format (v1)
3437

3538
Your command definition files must be written in the TOML format and use the

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { authCommand } from '../ui/commands/authCommand.js';
2323
import { bugCommand } from '../ui/commands/bugCommand.js';
2424
import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js';
2525
import { clearCommand } from '../ui/commands/clearCommand.js';
26+
import { commandsCommand } from '../ui/commands/commandsCommand.js';
2627
import { compressCommand } from '../ui/commands/compressCommand.js';
2728
import { copyCommand } from '../ui/commands/copyCommand.js';
2829
import { corgiCommand } from '../ui/commands/corgiCommand.js';
@@ -89,6 +90,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
8990
: chatCommand.subCommands,
9091
},
9192
clearCommand,
93+
commandsCommand,
9294
compressCommand,
9395
copyCommand,
9496
corgiCommand,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach } from 'vitest';
8+
import { commandsCommand } from './commandsCommand.js';
9+
import { MessageType } from '../types.js';
10+
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
11+
import type { CommandContext } from './types.js';
12+
13+
describe('commandsCommand', () => {
14+
let context: CommandContext;
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
context = createMockCommandContext({
19+
ui: {
20+
reloadCommands: vi.fn(),
21+
},
22+
});
23+
});
24+
25+
describe('default action', () => {
26+
it('should return an info message prompting subcommand usage', async () => {
27+
const result = await commandsCommand.action!(context, '');
28+
29+
expect(result).toEqual({
30+
type: 'message',
31+
messageType: 'info',
32+
content:
33+
'Use "/commands reload" to reload custom command definitions from .toml files.',
34+
});
35+
});
36+
});
37+
38+
describe('reload', () => {
39+
it('should call reloadCommands and show a success message', async () => {
40+
const reloadCmd = commandsCommand.subCommands!.find(
41+
(s) => s.name === 'reload',
42+
)!;
43+
44+
await reloadCmd.action!(context, '');
45+
46+
expect(context.ui.reloadCommands).toHaveBeenCalledTimes(1);
47+
expect(context.ui.addItem).toHaveBeenCalledWith(
48+
expect.objectContaining({
49+
type: MessageType.INFO,
50+
text: 'Custom commands reloaded successfully.',
51+
}),
52+
expect.any(Number),
53+
);
54+
});
55+
});
56+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
type CommandContext,
9+
type SlashCommand,
10+
type SlashCommandActionReturn,
11+
CommandKind,
12+
} from './types.js';
13+
import {
14+
MessageType,
15+
type HistoryItemError,
16+
type HistoryItemInfo,
17+
} from '../types.js';
18+
19+
/**
20+
* Action for the default `/commands` invocation.
21+
* Displays a message prompting the user to use a subcommand.
22+
*/
23+
async function listAction(
24+
_context: CommandContext,
25+
_args: string,
26+
): Promise<void | SlashCommandActionReturn> {
27+
return {
28+
type: 'message',
29+
messageType: 'info',
30+
content:
31+
'Use "/commands reload" to reload custom command definitions from .toml files.',
32+
};
33+
}
34+
35+
/**
36+
* Action for `/commands reload`.
37+
* Triggers a full re-discovery and reload of all slash commands, including
38+
* user/project-level .toml files, MCP prompts, and extension commands.
39+
*/
40+
async function reloadAction(
41+
context: CommandContext,
42+
): Promise<void | SlashCommandActionReturn> {
43+
try {
44+
context.ui.reloadCommands();
45+
46+
context.ui.addItem(
47+
{
48+
type: MessageType.INFO,
49+
text: 'Custom commands reloaded successfully.',
50+
} as HistoryItemInfo,
51+
Date.now(),
52+
);
53+
} catch (error) {
54+
context.ui.addItem(
55+
{
56+
type: MessageType.ERROR,
57+
text: `Failed to reload commands: ${error instanceof Error ? error.message : String(error)}`,
58+
} as HistoryItemError,
59+
Date.now(),
60+
);
61+
}
62+
}
63+
64+
export const commandsCommand: SlashCommand = {
65+
name: 'commands',
66+
description: 'Manage custom slash commands. Usage: /commands [reload]',
67+
kind: CommandKind.BUILT_IN,
68+
autoExecute: false,
69+
subCommands: [
70+
{
71+
name: 'reload',
72+
description:
73+
'Reload custom command definitions from .toml files. Usage: /commands reload',
74+
kind: CommandKind.BUILT_IN,
75+
autoExecute: true,
76+
action: reloadAction,
77+
},
78+
],
79+
action: listAction,
80+
};

0 commit comments

Comments
 (0)