mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(cli): Hooks enable-all/disable-all feature with dynamic status (#15552)
This commit is contained in:
@@ -533,14 +533,29 @@ Use the `/hooks panel` command to view all registered hooks:
|
|||||||
|
|
||||||
This command displays:
|
This command displays:
|
||||||
|
|
||||||
- All active hooks organized by event
|
- All configured hooks organized by event
|
||||||
- Hook source (user, project, system)
|
- Hook source (user, project, system)
|
||||||
- Hook type (command or plugin)
|
- Hook type (command or plugin)
|
||||||
- Execution status and recent output
|
- Individual hook status (enabled/disabled)
|
||||||
|
|
||||||
### Enable and disable hooks
|
### Enable and disable all hooks at once
|
||||||
|
|
||||||
You can temporarily enable or disable individual hooks using commands:
|
You can enable or disable all hooks at once using commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/hooks enable-all
|
||||||
|
/hooks disable-all
|
||||||
|
```
|
||||||
|
|
||||||
|
These commands provide a shortcut to enable or disable all configured hooks
|
||||||
|
without managing them individually. The `enable-all` command removes all hooks
|
||||||
|
from the `hooks.disabled` array, while `disable-all` adds all configured hooks
|
||||||
|
to the disabled list. Changes take effect immediately without requiring a
|
||||||
|
restart.
|
||||||
|
|
||||||
|
### Enable and disable individual hooks
|
||||||
|
|
||||||
|
You can enable or disable individual hooks using commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/hooks enable hook-name
|
/hooks enable hook-name
|
||||||
@@ -549,7 +564,8 @@ You can temporarily enable or disable individual hooks using commands:
|
|||||||
|
|
||||||
These commands allow you to control hook execution without editing configuration
|
These commands allow you to control hook execution without editing configuration
|
||||||
files. The hook name should match the `name` field in your hook configuration.
|
files. The hook name should match the `name` field in your hook configuration.
|
||||||
Changes made via these commands are persisted to your global User settings
|
Changes made via these commands are persisted to your settings. The settings are
|
||||||
|
saved to workspace scope if available, otherwise to your global user settings
|
||||||
(`~/.gemini/settings.json`).
|
(`~/.gemini/settings.json`).
|
||||||
|
|
||||||
### Disabled hooks configuration
|
### Disabled hooks configuration
|
||||||
|
|||||||
@@ -21,12 +21,16 @@ describe('hooksCommand', () => {
|
|||||||
};
|
};
|
||||||
let mockConfig: {
|
let mockConfig: {
|
||||||
getHookSystem: ReturnType<typeof vi.fn>;
|
getHookSystem: ReturnType<typeof vi.fn>;
|
||||||
|
getEnableHooks: ReturnType<typeof vi.fn>;
|
||||||
};
|
};
|
||||||
let mockSettings: {
|
let mockSettings: {
|
||||||
merged: {
|
merged: {
|
||||||
hooks?: {
|
hooks?: {
|
||||||
disabled?: string[];
|
disabled?: string[];
|
||||||
};
|
};
|
||||||
|
tools?: {
|
||||||
|
enableHooks?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
setValue: ReturnType<typeof vi.fn>;
|
setValue: ReturnType<typeof vi.fn>;
|
||||||
};
|
};
|
||||||
@@ -46,6 +50,7 @@ describe('hooksCommand', () => {
|
|||||||
// Create mock config
|
// Create mock config
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
getHookSystem: vi.fn().mockReturnValue(mockHookSystem),
|
getHookSystem: vi.fn().mockReturnValue(mockHookSystem),
|
||||||
|
getEnableHooks: vi.fn().mockReturnValue(true),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create mock settings
|
// Create mock settings
|
||||||
@@ -79,12 +84,14 @@ describe('hooksCommand', () => {
|
|||||||
|
|
||||||
it('should have all expected subcommands', () => {
|
it('should have all expected subcommands', () => {
|
||||||
expect(hooksCommand.subCommands).toBeDefined();
|
expect(hooksCommand.subCommands).toBeDefined();
|
||||||
expect(hooksCommand.subCommands).toHaveLength(3);
|
expect(hooksCommand.subCommands).toHaveLength(5);
|
||||||
|
|
||||||
const subCommandNames = hooksCommand.subCommands!.map((cmd) => cmd.name);
|
const subCommandNames = hooksCommand.subCommands!.map((cmd) => cmd.name);
|
||||||
expect(subCommandNames).toContain('panel');
|
expect(subCommandNames).toContain('panel');
|
||||||
expect(subCommandNames).toContain('enable');
|
expect(subCommandNames).toContain('enable');
|
||||||
expect(subCommandNames).toContain('disable');
|
expect(subCommandNames).toContain('disable');
|
||||||
|
expect(subCommandNames).toContain('enable-all');
|
||||||
|
expect(subCommandNames).toContain('disable-all');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delegate to panel action when invoked without subcommand', async () => {
|
it('should delegate to panel action when invoked without subcommand', async () => {
|
||||||
@@ -131,7 +138,7 @@ describe('hooksCommand', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return info message when hook system is not enabled', async () => {
|
it('should display panel even when hook system is not enabled', async () => {
|
||||||
mockConfig.getHookSystem.mockReturnValue(null);
|
mockConfig.getHookSystem.mockReturnValue(null);
|
||||||
|
|
||||||
const panelCmd = hooksCommand.subCommands!.find(
|
const panelCmd = hooksCommand.subCommands!.find(
|
||||||
@@ -141,18 +148,22 @@ describe('hooksCommand', () => {
|
|||||||
throw new Error('panel command must have an action');
|
throw new Error('panel command must have an action');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await panelCmd.action(mockContext, '');
|
await panelCmd.action(mockContext, '');
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
type: 'message',
|
expect.objectContaining({
|
||||||
messageType: 'info',
|
type: MessageType.HOOKS_LIST,
|
||||||
content:
|
hooks: [],
|
||||||
'Hook system is not enabled. Enable it in settings with hooks.enabled.',
|
}),
|
||||||
});
|
expect.any(Number),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return info message when no hooks are configured', async () => {
|
it('should display panel when no hooks are configured', async () => {
|
||||||
mockHookSystem.getAllHooks.mockReturnValue([]);
|
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||||
|
(mockContext.services.settings.merged as Record<string, unknown>)[
|
||||||
|
'tools'
|
||||||
|
] = { enableHooks: true };
|
||||||
|
|
||||||
const panelCmd = hooksCommand.subCommands!.find(
|
const panelCmd = hooksCommand.subCommands!.find(
|
||||||
(cmd) => cmd.name === 'panel',
|
(cmd) => cmd.name === 'panel',
|
||||||
@@ -161,14 +172,15 @@ describe('hooksCommand', () => {
|
|||||||
throw new Error('panel command must have an action');
|
throw new Error('panel command must have an action');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await panelCmd.action(mockContext, '');
|
await panelCmd.action(mockContext, '');
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
type: 'message',
|
expect.objectContaining({
|
||||||
messageType: 'info',
|
type: MessageType.HOOKS_LIST,
|
||||||
content:
|
hooks: [],
|
||||||
'No hooks configured. Add hooks to your settings to get started.',
|
}),
|
||||||
});
|
expect.any(Number),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display hooks list when hooks are configured', async () => {
|
it('should display hooks list when hooks are configured', async () => {
|
||||||
@@ -178,6 +190,9 @@ describe('hooksCommand', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||||
|
(mockContext.services.settings.merged as Record<string, unknown>)[
|
||||||
|
'tools'
|
||||||
|
] = { enableHooks: true };
|
||||||
|
|
||||||
const panelCmd = hooksCommand.subCommands!.find(
|
const panelCmd = hooksCommand.subCommands!.find(
|
||||||
(cmd) => cmd.name === 'panel',
|
(cmd) => cmd.name === 'panel',
|
||||||
@@ -562,6 +577,254 @@ describe('hooksCommand', () => {
|
|||||||
expect(result).toEqual(['test-hook']);
|
expect(result).toEqual(['test-hook']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('enable-all subcommand', () => {
|
||||||
|
it('should return error when config is not loaded', async () => {
|
||||||
|
const contextWithoutConfig = createMockCommandContext({
|
||||||
|
services: {
|
||||||
|
config: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'enable-all',
|
||||||
|
);
|
||||||
|
if (!enableAllCmd?.action) {
|
||||||
|
throw new Error('enable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await enableAllCmd.action(contextWithoutConfig, '');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Config not loaded.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when hook system is not enabled', async () => {
|
||||||
|
mockConfig.getHookSystem.mockReturnValue(null);
|
||||||
|
|
||||||
|
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'enable-all',
|
||||||
|
);
|
||||||
|
if (!enableAllCmd?.action) {
|
||||||
|
throw new Error('enable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await enableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Hook system is not enabled.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable all disabled hooks', async () => {
|
||||||
|
const mockHooks = [
|
||||||
|
createMockHook('hook-1', HookEventName.BeforeTool, false),
|
||||||
|
createMockHook('hook-2', HookEventName.AfterTool, false),
|
||||||
|
createMockHook('hook-3', HookEventName.BeforeAgent, true), // already enabled
|
||||||
|
];
|
||||||
|
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||||
|
|
||||||
|
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'enable-all',
|
||||||
|
);
|
||||||
|
if (!enableAllCmd?.action) {
|
||||||
|
throw new Error('enable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await enableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'hooks.disabled',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||||
|
'hook-1',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||||
|
'hook-2',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'Enabled 2 hook(s) successfully.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return info when no hooks are configured', async () => {
|
||||||
|
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||||
|
|
||||||
|
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'enable-all',
|
||||||
|
);
|
||||||
|
if (!enableAllCmd?.action) {
|
||||||
|
throw new Error('enable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await enableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'No hooks configured.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return info when all hooks are already enabled', async () => {
|
||||||
|
const mockHooks = [
|
||||||
|
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||||
|
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||||
|
];
|
||||||
|
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||||
|
|
||||||
|
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'enable-all',
|
||||||
|
);
|
||||||
|
if (!enableAllCmd?.action) {
|
||||||
|
throw new Error('enable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await enableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'All hooks are already enabled.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('disable-all subcommand', () => {
|
||||||
|
it('should return error when config is not loaded', async () => {
|
||||||
|
const contextWithoutConfig = createMockCommandContext({
|
||||||
|
services: {
|
||||||
|
config: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'disable-all',
|
||||||
|
);
|
||||||
|
if (!disableAllCmd?.action) {
|
||||||
|
throw new Error('disable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await disableAllCmd.action(contextWithoutConfig, '');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Config not loaded.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when hook system is not enabled', async () => {
|
||||||
|
mockConfig.getHookSystem.mockReturnValue(null);
|
||||||
|
|
||||||
|
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'disable-all',
|
||||||
|
);
|
||||||
|
if (!disableAllCmd?.action) {
|
||||||
|
throw new Error('disable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await disableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Hook system is not enabled.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable all enabled hooks', async () => {
|
||||||
|
const mockHooks = [
|
||||||
|
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||||
|
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||||
|
createMockHook('hook-3', HookEventName.BeforeAgent, false), // already disabled
|
||||||
|
];
|
||||||
|
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||||
|
|
||||||
|
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'disable-all',
|
||||||
|
);
|
||||||
|
if (!disableAllCmd?.action) {
|
||||||
|
throw new Error('disable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await disableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'hooks.disabled',
|
||||||
|
['hook-1', 'hook-2', 'hook-3'],
|
||||||
|
);
|
||||||
|
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||||
|
'hook-1',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||||
|
'hook-2',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'Disabled 2 hook(s) successfully.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return info when no hooks are configured', async () => {
|
||||||
|
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||||
|
|
||||||
|
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'disable-all',
|
||||||
|
);
|
||||||
|
if (!disableAllCmd?.action) {
|
||||||
|
throw new Error('disable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await disableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'No hooks configured.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return info when all hooks are already disabled', async () => {
|
||||||
|
const mockHooks = [
|
||||||
|
createMockHook('hook-1', HookEventName.BeforeTool, false),
|
||||||
|
createMockHook('hook-2', HookEventName.AfterTool, false),
|
||||||
|
];
|
||||||
|
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||||
|
|
||||||
|
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||||
|
(cmd) => cmd.name === 'disable-all',
|
||||||
|
);
|
||||||
|
if (!disableAllCmd?.action) {
|
||||||
|
throw new Error('disable-all command must have an action');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await disableAllCmd.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'All hooks are already disabled.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,24 +30,7 @@ async function panelAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hookSystem = config.getHookSystem();
|
const hookSystem = config.getHookSystem();
|
||||||
if (!hookSystem) {
|
const allHooks = hookSystem?.getAllHooks() || [];
|
||||||
return {
|
|
||||||
type: 'message',
|
|
||||||
messageType: 'info',
|
|
||||||
content:
|
|
||||||
'Hook system is not enabled. Enable it in settings with hooks.enabled.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const allHooks = hookSystem.getAllHooks();
|
|
||||||
if (allHooks.length === 0) {
|
|
||||||
return {
|
|
||||||
type: 'message',
|
|
||||||
messageType: 'info',
|
|
||||||
content:
|
|
||||||
'No hooks configured. Add hooks to your settings to get started.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const hooksListItem: HistoryItemHooksList = {
|
const hooksListItem: HistoryItemHooksList = {
|
||||||
type: MessageType.HOOKS_LIST,
|
type: MessageType.HOOKS_LIST,
|
||||||
@@ -102,7 +85,10 @@ async function enableAction(
|
|||||||
|
|
||||||
// Update settings (setValue automatically saves)
|
// Update settings (setValue automatically saves)
|
||||||
try {
|
try {
|
||||||
settings.setValue(SettingScope.User, 'hooks.disabled', newDisabledHooks);
|
const scope = settings.workspace
|
||||||
|
? SettingScope.Workspace
|
||||||
|
: SettingScope.User;
|
||||||
|
settings.setValue(scope, 'hooks.disabled', newDisabledHooks);
|
||||||
|
|
||||||
// Enable in hook system
|
// Enable in hook system
|
||||||
hookSystem.setHookEnabled(hookName, true);
|
hookSystem.setHookEnabled(hookName, true);
|
||||||
@@ -165,7 +151,10 @@ async function disableAction(
|
|||||||
|
|
||||||
// Update settings (setValue automatically saves)
|
// Update settings (setValue automatically saves)
|
||||||
try {
|
try {
|
||||||
settings.setValue(SettingScope.User, 'hooks.disabled', newDisabledHooks);
|
const scope = settings.workspace
|
||||||
|
? SettingScope.Workspace
|
||||||
|
: SettingScope.User;
|
||||||
|
settings.setValue(scope, 'hooks.disabled', newDisabledHooks);
|
||||||
|
|
||||||
// Disable in hook system
|
// Disable in hook system
|
||||||
hookSystem.setHookEnabled(hookName, false);
|
hookSystem.setHookEnabled(hookName, false);
|
||||||
@@ -216,6 +205,145 @@ function getHookDisplayName(hook: HookRegistryEntry): string {
|
|||||||
return hook.config.name || hook.config.command || 'unknown-hook';
|
return hook.config.name || hook.config.command || 'unknown-hook';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable all hooks by clearing the disabled list
|
||||||
|
*/
|
||||||
|
async function enableAllAction(
|
||||||
|
context: CommandContext,
|
||||||
|
): Promise<void | MessageActionReturn> {
|
||||||
|
const { config } = context.services;
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Config not loaded.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookSystem = config.getHookSystem();
|
||||||
|
if (!hookSystem) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Hook system is not enabled.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = context.services.settings;
|
||||||
|
const allHooks = hookSystem.getAllHooks();
|
||||||
|
|
||||||
|
if (allHooks.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'No hooks configured.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledHooks = allHooks.filter((hook) => !hook.enabled);
|
||||||
|
if (disabledHooks.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'All hooks are already enabled.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scope = settings.workspace
|
||||||
|
? SettingScope.Workspace
|
||||||
|
: SettingScope.User;
|
||||||
|
settings.setValue(scope, 'hooks.disabled', []);
|
||||||
|
|
||||||
|
for (const hook of disabledHooks) {
|
||||||
|
const hookName = getHookDisplayName(hook);
|
||||||
|
hookSystem.setHookEnabled(hookName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: `Enabled ${disabledHooks.length} hook(s) successfully.`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Failed to enable hooks: ${getErrorMessage(error)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable all hooks by adding all hooks to the disabled list
|
||||||
|
*/
|
||||||
|
async function disableAllAction(
|
||||||
|
context: CommandContext,
|
||||||
|
): Promise<void | MessageActionReturn> {
|
||||||
|
const { config } = context.services;
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Config not loaded.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookSystem = config.getHookSystem();
|
||||||
|
if (!hookSystem) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: 'Hook system is not enabled.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = context.services.settings;
|
||||||
|
const allHooks = hookSystem.getAllHooks();
|
||||||
|
|
||||||
|
if (allHooks.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'No hooks configured.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledHooks = allHooks.filter((hook) => hook.enabled);
|
||||||
|
if (enabledHooks.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: 'All hooks are already disabled.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allHookNames = allHooks.map((hook) => getHookDisplayName(hook));
|
||||||
|
const scope = settings.workspace
|
||||||
|
? SettingScope.Workspace
|
||||||
|
: SettingScope.User;
|
||||||
|
settings.setValue(scope, 'hooks.disabled', allHookNames);
|
||||||
|
|
||||||
|
for (const hook of enabledHooks) {
|
||||||
|
const hookName = getHookDisplayName(hook);
|
||||||
|
hookSystem.setHookEnabled(hookName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'info',
|
||||||
|
content: `Disabled ${enabledHooks.length} hook(s) successfully.`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
messageType: 'error',
|
||||||
|
content: `Failed to disable hooks: ${getErrorMessage(error)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const panelCommand: SlashCommand = {
|
const panelCommand: SlashCommand = {
|
||||||
name: 'panel',
|
name: 'panel',
|
||||||
altNames: ['list', 'show'],
|
altNames: ['list', 'show'],
|
||||||
@@ -242,10 +370,34 @@ const disableCommand: SlashCommand = {
|
|||||||
completion: completeHookNames,
|
completion: completeHookNames,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enableAllCommand: SlashCommand = {
|
||||||
|
name: 'enable-all',
|
||||||
|
altNames: ['enableall'],
|
||||||
|
description: 'Enable all disabled hooks',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
autoExecute: true,
|
||||||
|
action: enableAllAction,
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableAllCommand: SlashCommand = {
|
||||||
|
name: 'disable-all',
|
||||||
|
altNames: ['disableall'],
|
||||||
|
description: 'Disable all enabled hooks',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
autoExecute: true,
|
||||||
|
action: disableAllAction,
|
||||||
|
};
|
||||||
|
|
||||||
export const hooksCommand: SlashCommand = {
|
export const hooksCommand: SlashCommand = {
|
||||||
name: 'hooks',
|
name: 'hooks',
|
||||||
description: 'Manage hooks',
|
description: 'Manage hooks',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
subCommands: [panelCommand, enableCommand, disableCommand],
|
subCommands: [
|
||||||
|
panelCommand,
|
||||||
|
enableCommand,
|
||||||
|
disableCommand,
|
||||||
|
enableAllCommand,
|
||||||
|
disableAllCommand,
|
||||||
|
],
|
||||||
action: async (context: CommandContext) => panelCommand.action!(context, ''),
|
action: async (context: CommandContext) => panelCommand.action!(context, ''),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,105 +25,104 @@ interface HooksListProps {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HooksList: React.FC<HooksListProps> = ({ hooks }) => (
|
export const HooksList: React.FC<HooksListProps> = ({ hooks }) => {
|
||||||
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
if (hooks.length === 0) {
|
||||||
<Text>
|
return (
|
||||||
Hooks are scripts or programs that Gemini CLI executes at specific points
|
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
||||||
in the agentic loop, allowing you to intercept and customize behavior.
|
<Box marginTop={1}>
|
||||||
</Text>
|
<Text>No hooks configured.</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
// Group hooks by event name for better organization
|
||||||
<Text color={theme.status.warning} bold underline>
|
const hooksByEvent = hooks.reduce(
|
||||||
⚠️ Security Warning:
|
(acc, hook) => {
|
||||||
</Text>
|
if (!acc[hook.eventName]) {
|
||||||
<Text color={theme.status.warning}>
|
acc[hook.eventName] = [];
|
||||||
Hooks can execute arbitrary commands on your system. Only use hooks from
|
}
|
||||||
sources you trust. Review hook scripts carefully.
|
acc[hook.eventName].push(hook);
|
||||||
</Text>
|
return acc;
|
||||||
</Box>
|
},
|
||||||
|
{} as Record<string, Array<(typeof hooks)[number]>>,
|
||||||
|
);
|
||||||
|
|
||||||
<Box marginTop={1}>
|
return (
|
||||||
<Text>
|
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
||||||
Learn more:{' '}
|
<Box marginTop={1} flexDirection="column">
|
||||||
<Text color={theme.text.link}>https://geminicli.com/docs/hooks</Text>
|
<Text color={theme.status.warning} bold underline>
|
||||||
</Text>
|
⚠️ Security Warning:
|
||||||
</Box>
|
</Text>
|
||||||
|
<Text color={theme.status.warning}>
|
||||||
|
Hooks can execute arbitrary commands on your system. Only use hooks
|
||||||
|
from sources you trust. Review hook scripts carefully.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
<Box marginTop={1}>
|
||||||
{hooks.length === 0 ? (
|
<Text>
|
||||||
<Text>No hooks configured.</Text>
|
Learn more:{' '}
|
||||||
) : (
|
<Text color={theme.text.link}>https://geminicli.com/docs/hooks</Text>
|
||||||
<>
|
</Text>
|
||||||
<Text bold underline>
|
</Box>
|
||||||
Registered Hooks:
|
|
||||||
</Text>
|
|
||||||
<Box flexDirection="column" paddingLeft={2} marginTop={1}>
|
|
||||||
{Object.entries(
|
|
||||||
hooks.reduce(
|
|
||||||
(acc, hook) => {
|
|
||||||
if (!acc[hook.eventName]) {
|
|
||||||
acc[hook.eventName] = [];
|
|
||||||
}
|
|
||||||
acc[hook.eventName].push(hook);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, Array<(typeof hooks)[number]>>,
|
|
||||||
),
|
|
||||||
).map(([eventName, eventHooks]) => (
|
|
||||||
<Box key={eventName} flexDirection="column" marginBottom={1}>
|
|
||||||
<Text color="cyan" bold>
|
|
||||||
{eventName}:
|
|
||||||
</Text>
|
|
||||||
<Box flexDirection="column" paddingLeft={2}>
|
|
||||||
{eventHooks.map((hook, index) => {
|
|
||||||
const hookName =
|
|
||||||
hook.config.name || hook.config.command || 'unknown';
|
|
||||||
const statusColor = hook.enabled ? 'green' : 'gray';
|
|
||||||
const statusText = hook.enabled ? 'enabled' : 'disabled';
|
|
||||||
|
|
||||||
return (
|
<Box marginTop={1}>
|
||||||
<Box key={`${eventName}-${index}`} flexDirection="column">
|
<Text bold>Configured Hooks:</Text>
|
||||||
<Box>
|
</Box>
|
||||||
<Text>
|
<Box flexDirection="column" paddingLeft={2} marginTop={1}>
|
||||||
<Text color="yellow">{hookName}</Text>
|
{Object.entries(hooksByEvent).map(([eventName, eventHooks]) => (
|
||||||
<Text
|
<Box key={eventName} flexDirection="column" marginBottom={1}>
|
||||||
color={statusColor}
|
<Text color={theme.text.accent} bold>
|
||||||
>{` [${statusText}]`}</Text>
|
{eventName}:
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
<Box paddingLeft={2} flexDirection="column">
|
{eventHooks.map((hook, index) => {
|
||||||
{hook.config.description && (
|
const hookName =
|
||||||
<Text italic color={theme.text.primary}>
|
hook.config.name || hook.config.command || 'unknown';
|
||||||
{hook.config.description}
|
const statusColor = hook.enabled
|
||||||
</Text>
|
? theme.status.success
|
||||||
)}
|
: theme.text.secondary;
|
||||||
<Text dimColor>
|
const statusText = hook.enabled ? 'enabled' : 'disabled';
|
||||||
Source: {hook.source}
|
|
||||||
{hook.config.name &&
|
return (
|
||||||
hook.config.command &&
|
<Box key={`${eventName}-${index}`} flexDirection="column">
|
||||||
` | Command: ${hook.config.command}`}
|
<Box>
|
||||||
{hook.matcher && ` | Matcher: ${hook.matcher}`}
|
<Text>
|
||||||
{hook.sequential && ` | Sequential`}
|
<Text color={theme.text.accent}>{hookName}</Text>
|
||||||
{hook.config.timeout &&
|
<Text color={statusColor}>{` [${statusText}]`}</Text>
|
||||||
` | Timeout: ${hook.config.timeout}s`}
|
</Text>
|
||||||
</Text>
|
</Box>
|
||||||
</Box>
|
<Box paddingLeft={2} flexDirection="column">
|
||||||
</Box>
|
{hook.config.description && (
|
||||||
);
|
<Text italic>{hook.config.description}</Text>
|
||||||
})}
|
)}
|
||||||
</Box>
|
<Text dimColor>
|
||||||
</Box>
|
Source: {hook.source}
|
||||||
))}
|
{hook.config.name &&
|
||||||
|
hook.config.command &&
|
||||||
|
` | Command: ${hook.config.command}`}
|
||||||
|
{hook.matcher && ` | Matcher: ${hook.matcher}`}
|
||||||
|
{hook.sequential && ` | Sequential`}
|
||||||
|
{hook.config.timeout &&
|
||||||
|
` | Timeout: ${hook.config.timeout}s`}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
))}
|
||||||
)}
|
</Box>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text dimColor>
|
||||||
|
Tip: Use <Text bold>/hooks enable {'<hook-name>'}</Text> or{' '}
|
||||||
|
<Text bold>/hooks disable {'<hook-name>'}</Text> to toggle individual
|
||||||
|
hooks. Use <Text bold>/hooks enable-all</Text> or{' '}
|
||||||
|
<Text bold>/hooks disable-all</Text> to toggle all hooks at once.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
<Box marginTop={1}>
|
};
|
||||||
<Text dimColor>
|
|
||||||
Tip: Use `/hooks enable {'<hook-name>'}` or `/hooks disable{' '}
|
|
||||||
{'<hook-name>'}` to toggle hooks
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user