2025-12-03 10:01:57 -08:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-12-04 10:56:16 -05:00
|
|
|
import type { SlashCommand, CommandContext } from './types.js';
|
2025-12-03 10:01:57 -08:00
|
|
|
import { CommandKind } from './types.js';
|
|
|
|
|
import { MessageType, type HistoryItemHooksList } from '../types.js';
|
2025-12-04 10:56:16 -05:00
|
|
|
import type {
|
|
|
|
|
HookRegistryEntry,
|
|
|
|
|
MessageActionReturn,
|
|
|
|
|
} from '@google/gemini-cli-core';
|
2025-12-03 10:01:57 -08:00
|
|
|
import { getErrorMessage } from '@google/gemini-cli-core';
|
|
|
|
|
import { SettingScope } from '../../config/settings.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display a formatted list of hooks with their status
|
|
|
|
|
*/
|
|
|
|
|
async function panelAction(
|
|
|
|
|
context: CommandContext,
|
|
|
|
|
): Promise<void | MessageActionReturn> {
|
|
|
|
|
const { config } = context.services;
|
|
|
|
|
if (!config) {
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'error',
|
|
|
|
|
content: 'Config not loaded.',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hookSystem = config.getHookSystem();
|
2026-01-12 12:42:04 +05:00
|
|
|
const allHooks = hookSystem?.getAllHooks() || [];
|
2025-12-03 10:01:57 -08:00
|
|
|
|
|
|
|
|
const hooksListItem: HistoryItemHooksList = {
|
|
|
|
|
type: MessageType.HOOKS_LIST,
|
|
|
|
|
hooks: allHooks,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 14:15:04 -05:00
|
|
|
context.ui.addItem(hooksListItem);
|
2025-12-03 10:01:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Enable a hook by name
|
|
|
|
|
*/
|
|
|
|
|
async function enableAction(
|
|
|
|
|
context: CommandContext,
|
|
|
|
|
args: string,
|
|
|
|
|
): 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 hookName = args.trim();
|
|
|
|
|
if (!hookName) {
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'error',
|
|
|
|
|
content: 'Usage: /hooks enable <hook-name>',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current disabled hooks from settings
|
|
|
|
|
const settings = context.services.settings;
|
2026-01-20 14:47:31 -08:00
|
|
|
const disabledHooks = settings.merged.hooksConfig.disabled;
|
2025-12-03 10:01:57 -08:00
|
|
|
// Remove from disabled list if present
|
|
|
|
|
const newDisabledHooks = disabledHooks.filter(
|
|
|
|
|
(name: string) => name !== hookName,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update settings (setValue automatically saves)
|
|
|
|
|
try {
|
2026-01-12 12:42:04 +05:00
|
|
|
const scope = settings.workspace
|
|
|
|
|
? SettingScope.Workspace
|
|
|
|
|
: SettingScope.User;
|
2026-01-20 14:47:31 -08:00
|
|
|
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
2025-12-03 10:01:57 -08:00
|
|
|
|
2026-01-16 19:28:36 -05:00
|
|
|
// Update core config so re-initialization (e.g. extension reload) respects the change
|
2026-01-20 14:47:31 -08:00
|
|
|
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
2026-01-16 19:28:36 -05:00
|
|
|
|
2025-12-03 10:01:57 -08:00
|
|
|
// Enable in hook system
|
|
|
|
|
hookSystem.setHookEnabled(hookName, true);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'info',
|
|
|
|
|
content: `Hook "${hookName}" enabled successfully.`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'error',
|
|
|
|
|
content: `Failed to enable hook: ${getErrorMessage(error)}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disable a hook by name
|
|
|
|
|
*/
|
|
|
|
|
async function disableAction(
|
|
|
|
|
context: CommandContext,
|
|
|
|
|
args: string,
|
|
|
|
|
): 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 hookName = args.trim();
|
|
|
|
|
if (!hookName) {
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'error',
|
|
|
|
|
content: 'Usage: /hooks disable <hook-name>',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current disabled hooks from settings
|
|
|
|
|
const settings = context.services.settings;
|
2026-01-20 14:47:31 -08:00
|
|
|
const disabledHooks = settings.merged.hooksConfig.disabled;
|
2025-12-03 10:01:57 -08:00
|
|
|
// Add to disabled list if not already present
|
2026-01-16 19:28:36 -05:00
|
|
|
try {
|
|
|
|
|
if (!disabledHooks.includes(hookName)) {
|
|
|
|
|
const newDisabledHooks = [...disabledHooks, hookName];
|
2025-12-03 10:01:57 -08:00
|
|
|
|
2026-01-12 12:42:04 +05:00
|
|
|
const scope = settings.workspace
|
|
|
|
|
? SettingScope.Workspace
|
|
|
|
|
: SettingScope.User;
|
2026-01-20 14:47:31 -08:00
|
|
|
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
2026-01-16 19:28:36 -05:00
|
|
|
}
|
2025-12-03 10:01:57 -08:00
|
|
|
|
2026-01-16 19:28:36 -05:00
|
|
|
// Update core config so re-initialization (e.g. extension reload) respects the change
|
2026-01-20 14:47:31 -08:00
|
|
|
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
2026-01-16 19:28:36 -05:00
|
|
|
|
|
|
|
|
// Always disable in hook system to ensure in-memory state matches settings
|
|
|
|
|
hookSystem.setHookEnabled(hookName, false);
|
2025-12-03 10:01:57 -08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'info',
|
2026-01-16 19:28:36 -05:00
|
|
|
content: `Hook "${hookName}" disabled successfully.`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
type: 'message',
|
|
|
|
|
messageType: 'error',
|
|
|
|
|
content: `Failed to disable hook: ${getErrorMessage(error)}`,
|
2025-12-03 10:01:57 -08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Completion function for hook names
|
|
|
|
|
*/
|
|
|
|
|
function completeHookNames(
|
|
|
|
|
context: CommandContext,
|
|
|
|
|
partialArg: string,
|
|
|
|
|
): string[] {
|
|
|
|
|
const { config } = context.services;
|
|
|
|
|
if (!config) return [];
|
|
|
|
|
|
|
|
|
|
const hookSystem = config.getHookSystem();
|
|
|
|
|
if (!hookSystem) return [];
|
|
|
|
|
|
|
|
|
|
const allHooks = hookSystem.getAllHooks();
|
|
|
|
|
const hookNames = allHooks.map((hook) => getHookDisplayName(hook));
|
|
|
|
|
return hookNames.filter((name) => name.startsWith(partialArg));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a display name for a hook
|
|
|
|
|
*/
|
|
|
|
|
function getHookDisplayName(hook: HookRegistryEntry): string {
|
2025-12-18 11:09:24 -05:00
|
|
|
return hook.config.name || hook.config.command || 'unknown-hook';
|
2025-12-03 10:01:57 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-12 12:42:04 +05:00
|
|
|
/**
|
|
|
|
|
* 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;
|
2026-01-20 14:47:31 -08:00
|
|
|
settings.setValue(scope, 'hooksConfig.disabled', []);
|
2026-01-12 12:42:04 +05:00
|
|
|
|
2026-01-16 19:28:36 -05:00
|
|
|
// Update core config so re-initialization (e.g. extension reload) respects the change
|
2026-01-20 14:47:31 -08:00
|
|
|
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
2026-01-16 19:28:36 -05:00
|
|
|
|
2026-01-12 12:42:04 +05:00
|
|
|
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;
|
2026-01-20 14:47:31 -08:00
|
|
|
settings.setValue(scope, 'hooksConfig.disabled', allHookNames);
|
2026-01-12 12:42:04 +05:00
|
|
|
|
2026-01-16 19:28:36 -05:00
|
|
|
// Update core config so re-initialization (e.g. extension reload) respects the change
|
2026-01-20 14:47:31 -08:00
|
|
|
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
2026-01-16 19:28:36 -05:00
|
|
|
|
2026-01-12 12:42:04 +05:00
|
|
|
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)}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 10:01:57 -08:00
|
|
|
const panelCommand: SlashCommand = {
|
|
|
|
|
name: 'panel',
|
|
|
|
|
altNames: ['list', 'show'],
|
|
|
|
|
description: 'Display all registered hooks with their status',
|
|
|
|
|
kind: CommandKind.BUILT_IN,
|
|
|
|
|
action: panelAction,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const enableCommand: SlashCommand = {
|
|
|
|
|
name: 'enable',
|
|
|
|
|
description: 'Enable a hook by name',
|
|
|
|
|
kind: CommandKind.BUILT_IN,
|
2025-12-08 16:32:39 -05:00
|
|
|
autoExecute: true,
|
2025-12-03 10:01:57 -08:00
|
|
|
action: enableAction,
|
|
|
|
|
completion: completeHookNames,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const disableCommand: SlashCommand = {
|
|
|
|
|
name: 'disable',
|
|
|
|
|
description: 'Disable a hook by name',
|
|
|
|
|
kind: CommandKind.BUILT_IN,
|
2025-12-08 16:32:39 -05:00
|
|
|
autoExecute: true,
|
2025-12-03 10:01:57 -08:00
|
|
|
action: disableAction,
|
|
|
|
|
completion: completeHookNames,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-12 12:42:04 +05:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 10:01:57 -08:00
|
|
|
export const hooksCommand: SlashCommand = {
|
|
|
|
|
name: 'hooks',
|
|
|
|
|
description: 'Manage hooks',
|
|
|
|
|
kind: CommandKind.BUILT_IN,
|
2026-01-12 12:42:04 +05:00
|
|
|
subCommands: [
|
|
|
|
|
panelCommand,
|
|
|
|
|
enableCommand,
|
|
|
|
|
disableCommand,
|
|
|
|
|
enableAllCommand,
|
|
|
|
|
disableAllCommand,
|
|
|
|
|
],
|
2025-12-03 10:01:57 -08:00
|
|
|
action: async (context: CommandContext) => panelCommand.action!(context, ''),
|
|
|
|
|
};
|