feat(cli): Hooks enable-all/disable-all feature with dynamic status (#15552)

This commit is contained in:
Abdul Tawab
2026-01-12 12:42:04 +05:00
committed by GitHub
parent 93b57b82c1
commit 9703fe73cf
4 changed files with 569 additions and 139 deletions

View File

@@ -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

View File

@@ -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.',
});
});
});
}); });
/** /**

View File

@@ -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, ''),
}; };

View File

@@ -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>
);