diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js
index 47312f9f2..da6f26899 100644
--- a/packages/cli/src/i18n/locales/de.js
+++ b/packages/cli/src/i18n/locales/de.js
@@ -614,6 +614,15 @@ export default {
'Für dieses Ereignis sind keine Hooks konfiguriert.',
'To add hooks, edit settings.json directly or ask Qwen.':
'Um Hooks hinzuzufügen, bearbeiten Sie settings.json direkt oder fragen Sie Qwen.',
+ 'Enter to select · Esc to go back': 'Enter zum Auswählen · Esc zum Zurück',
+ // Hooks - Config Detail Step
+ 'Hook details': 'Hook-Details',
+ 'Event:': 'Ereignis:',
+ 'Extension:': 'Erweiterung:',
+ 'Desc:': 'Beschreibung:',
+ 'No hook config selected': 'Keine Hook-Konfiguration ausgewählt',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ 'Um diesen Hook zu ändern oder zu entfernen, bearbeiten Sie settings.json direkt oder fragen Sie Qwen.',
// Hooks - Source
Project: 'Projekt',
User: 'Benutzer',
diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js
index 0a5f21536..5a2299b2d 100644
--- a/packages/cli/src/i18n/locales/en.js
+++ b/packages/cli/src/i18n/locales/en.js
@@ -687,6 +687,15 @@ export default {
'No hooks configured for this event.': 'No hooks configured for this event.',
'To add hooks, edit settings.json directly or ask Qwen.':
'To add hooks, edit settings.json directly or ask Qwen.',
+ 'Enter to select · Esc to go back': 'Enter to select · Esc to go back',
+ // Hooks - Config Detail Step
+ 'Hook details': 'Hook details',
+ 'Event:': 'Event:',
+ 'Extension:': 'Extension:',
+ 'Desc:': 'Desc:',
+ 'No hook config selected': 'No hook config selected',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.',
// Hooks - Source
Project: 'Project',
User: 'User',
diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js
index 906867911..0a5ed8403 100644
--- a/packages/cli/src/i18n/locales/ja.js
+++ b/packages/cli/src/i18n/locales/ja.js
@@ -400,6 +400,15 @@ export default {
'このイベントにはフックが設定されていません。',
'To add hooks, edit settings.json directly or ask Qwen.':
'フックを追加するには、settings.json を直接編集するか、Qwen に尋ねてください。',
+ 'Enter to select · Esc to go back': 'Enter で選択 · Esc で戻る',
+ // Hooks - Config Detail Step
+ 'Hook details': 'フック詳細',
+ 'Event:': 'イベント:',
+ 'Extension:': '拡張機能:',
+ 'Desc:': '説明:',
+ 'No hook config selected': 'フック設定が選択されていません',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ 'このフックを変更または削除するには、settings.json を直接編集するか、Qwen に尋ねてください。',
// Hooks - Source
Project: 'プロジェクト',
User: 'ユーザー',
diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js
index c5110a2ce..e0a9afed4 100644
--- a/packages/cli/src/i18n/locales/pt.js
+++ b/packages/cli/src/i18n/locales/pt.js
@@ -620,6 +620,15 @@ export default {
'Nenhum hook configurado para este evento.',
'To add hooks, edit settings.json directly or ask Qwen.':
'Para adicionar hooks, edite settings.json diretamente ou pergunte ao Qwen.',
+ 'Enter to select · Esc to go back': 'Enter para selecionar · Esc para voltar',
+ // Hooks - Config Detail Step
+ 'Hook details': 'Detalhes do Hook',
+ 'Event:': 'Evento:',
+ 'Extension:': 'Extensão:',
+ 'Desc:': 'Descrição:',
+ 'No hook config selected': 'Nenhuma configuração de hook selecionada',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ 'Para modificar ou remover este hook, edite settings.json diretamente ou pergunte ao Qwen.',
// Hooks - Source
Project: 'Projeto',
User: 'Usuário',
diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js
index f7a137f5d..28d42b450 100644
--- a/packages/cli/src/i18n/locales/ru.js
+++ b/packages/cli/src/i18n/locales/ru.js
@@ -625,6 +625,15 @@ export default {
'Для этого события нет настроенных хуков.',
'To add hooks, edit settings.json directly or ask Qwen.':
'Чтобы добавить хуки, отредактируйте settings.json напрямую или спросите Qwen.',
+ 'Enter to select · Esc to go back': 'Enter для выбора · Esc для возврата',
+ // Hooks - Config Detail Step
+ 'Hook details': 'Детали хука',
+ 'Event:': 'Событие:',
+ 'Extension:': 'Расширение:',
+ 'Desc:': 'Описание:',
+ 'No hook config selected': 'Конфигурация хука не выбрана',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ 'Чтобы изменить или удалить этот хук, отредактируйте settings.json напрямую или спросите Qwen.',
// Hooks - Source
Project: 'Проект',
User: 'Пользователь',
diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js
index 371e98b40..859ae6fc3 100644
--- a/packages/cli/src/i18n/locales/zh.js
+++ b/packages/cli/src/i18n/locales/zh.js
@@ -651,6 +651,15 @@ export default {
'No hooks configured for this event.': '此事件未配置 Hook。',
'To add hooks, edit settings.json directly or ask Qwen.':
'要添加 Hook,请直接编辑 settings.json 或询问 Qwen。',
+ 'Enter to select · Esc to go back': 'Enter 选择 · Esc 返回',
+ // Hooks - Config Detail Step
+ 'Hook details': 'Hook 详情',
+ 'Event:': '事件:',
+ 'Extension:': '扩展:',
+ 'Desc:': '描述:',
+ 'No hook config selected': '未选择 Hook 配置',
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.':
+ '要修改或删除此 Hook,请直接编辑 settings.json 或询问 Qwen。',
// Hooks - Source
Project: '项目',
User: '用户',
diff --git a/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx
new file mode 100644
index 000000000..2c7385215
--- /dev/null
+++ b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx
@@ -0,0 +1,343 @@
+/**
+ * @license
+ * Copyright 2026 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render } from 'ink-testing-library';
+import {
+ HookEventName,
+ HooksConfigSource,
+ HookType,
+} from '@qwen-code/qwen-code-core';
+import { HookConfigDetailStep } from './HookConfigDetailStep.js';
+import type { HookEventDisplayInfo, HookConfigDisplayInfo } from './types.js';
+
+// Mock i18n module
+vi.mock('../../../i18n/index.js', () => ({
+ t: vi.fn((key: string) => key),
+}));
+
+// Mock useKeypress
+vi.mock('../../hooks/useKeypress.js', () => ({
+ useKeypress: vi.fn(),
+}));
+
+// Mock useTerminalSize
+vi.mock('../../hooks/useTerminalSize.js', () => ({
+ useTerminalSize: vi.fn(() => ({ columns: 100, rows: 24 })),
+}));
+
+// Mock semantic-colors
+vi.mock('../../semantic-colors.js', () => ({
+ theme: {
+ text: {
+ primary: 'white',
+ secondary: 'gray',
+ accent: 'cyan',
+ },
+ border: {
+ default: 'gray',
+ },
+ },
+}));
+
+describe('HookConfigDetailStep', () => {
+ const mockOnBack = vi.fn();
+
+ const createMockHookEvent = (): HookEventDisplayInfo => ({
+ event: HookEventName.Stop,
+ shortDescription: 'Right before Qwen Code concludes its response',
+ description: '',
+ exitCodes: [
+ { code: 0, description: 'stdout/stderr not shown' },
+ {
+ code: 2,
+ description: 'show stderr to model and continue conversation',
+ },
+ { code: 'Other', description: 'show stderr to user only' },
+ ],
+ configs: [],
+ });
+
+ const createMockHookConfig = (
+ source: HooksConfigSource = HooksConfigSource.User,
+ sourceDisplay = 'User Settings',
+ sourcePath?: string,
+ ): HookConfigDisplayInfo => ({
+ config: {
+ type: HookType.Command,
+ command: '/path/to/hook.sh',
+ },
+ source,
+ sourceDisplay,
+ sourcePath,
+ enabled: true,
+ });
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render hook details title', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Hook details');
+ });
+
+ it('should render event name', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Event:');
+ expect(lastFrame()).toContain(HookEventName.Stop);
+ });
+
+ it('should render hook type', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Type:');
+ expect(lastFrame()).toContain('command');
+ });
+
+ it('should render source for User Settings', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig(HooksConfigSource.User);
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Source:');
+ expect(lastFrame()).toContain('User Settings');
+ });
+
+ it('should render source for Local Settings', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig(HooksConfigSource.Project);
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Local Settings');
+ });
+
+ it('should render source for Extensions with path', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig(
+ HooksConfigSource.Extensions,
+ 'ralph-wiggum',
+ '/Users/test/.qwen/extensions/ralph-wiggum',
+ );
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Extensions');
+ expect(lastFrame()).toContain('/Users/test/.qwen/extensions/ralph-wiggum');
+ });
+
+ it('should render Extension field for extensions', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig(
+ HooksConfigSource.Extensions,
+ 'ralph-wiggum',
+ );
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Extension:');
+ expect(lastFrame()).toContain('ralph-wiggum');
+ });
+
+ it('should not render Extension field for non-extensions', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig(HooksConfigSource.User);
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ // Should not have Extension label for User Settings
+ const output = lastFrame();
+ const extensionMatch = output?.match(/Extension:/g);
+ expect(extensionMatch).toBeNull();
+ });
+
+ it('should render command', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Command:');
+ expect(lastFrame()).toContain('/path/to/hook.sh');
+ });
+
+ it('should render hook name if present', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig: HookConfigDisplayInfo = {
+ config: {
+ type: HookType.Command,
+ command: '/path/to/hook.sh',
+ name: 'My Hook',
+ },
+ source: HooksConfigSource.User,
+ sourceDisplay: 'User Settings',
+ enabled: true,
+ };
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Name:');
+ expect(lastFrame()).toContain('My Hook');
+ });
+
+ it('should render hook description if present', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig: HookConfigDisplayInfo = {
+ config: {
+ type: HookType.Command,
+ command: '/path/to/hook.sh',
+ description: 'A test hook',
+ },
+ source: HooksConfigSource.User,
+ sourceDisplay: 'User Settings',
+ enabled: true,
+ };
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Desc:');
+ expect(lastFrame()).toContain('A test hook');
+ });
+
+ it('should render help text', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('To modify or remove this hook');
+ });
+
+ it('should render Esc hint', () => {
+ const hookEvent = createMockHookEvent();
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain('Esc to go back');
+ });
+
+ it('should handle different event types', () => {
+ const events = [
+ HookEventName.PreToolUse,
+ HookEventName.PostToolUse,
+ HookEventName.UserPromptSubmit,
+ HookEventName.SessionStart,
+ ];
+
+ for (const event of events) {
+ const hookEvent: HookEventDisplayInfo = {
+ event,
+ shortDescription: 'Test',
+ description: '',
+ exitCodes: [],
+ configs: [],
+ };
+ const hookConfig = createMockHookConfig();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain(event);
+ }
+ });
+});
diff --git a/packages/cli/src/ui/components/hooks/HookConfigDetailStep.tsx b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.tsx
new file mode 100644
index 000000000..e83345b43
--- /dev/null
+++ b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.tsx
@@ -0,0 +1,179 @@
+/**
+ * @license
+ * Copyright 2026 Qwen Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Box, Text } from 'ink';
+import { theme } from '../../semantic-colors.js';
+import { useKeypress } from '../../hooks/useKeypress.js';
+import { useTerminalSize } from '../../hooks/useTerminalSize.js';
+import type { HookConfigDisplayInfo, HookEventDisplayInfo } from './types.js';
+import { HooksConfigSource } from '@qwen-code/qwen-code-core';
+import { t } from '../../../i18n/index.js';
+
+interface HookConfigDetailStepProps {
+ hookEvent: HookEventDisplayInfo;
+ hookConfig: HookConfigDisplayInfo;
+ onBack: () => void;
+}
+
+export function HookConfigDetailStep({
+ hookEvent,
+ hookConfig,
+ onBack,
+}: HookConfigDetailStepProps): React.JSX.Element {
+ const { columns: terminalWidth } = useTerminalSize();
+
+ useKeypress(
+ (key) => {
+ if (key.name === 'escape') {
+ onBack();
+ }
+ },
+ { isActive: true },
+ );
+
+ // Get source display
+ const getSourceDisplay = (): string => {
+ switch (hookConfig.source) {
+ case HooksConfigSource.Project:
+ return t('Local Settings');
+ case HooksConfigSource.User:
+ return t('User Settings');
+ case HooksConfigSource.System:
+ return t('System Settings');
+ case HooksConfigSource.Extensions:
+ return t('Extensions');
+ default:
+ return hookConfig.source;
+ }
+ };
+
+ // Check if this is from an extension
+ const isFromExtension = hookConfig.source === HooksConfigSource.Extensions;
+
+ // Get hook type display
+ const getHookTypeDisplay = (): string => {
+ switch (hookConfig.config.type) {
+ case 'command':
+ return 'command';
+ default:
+ return hookConfig.config.type;
+ }
+ };
+
+ // Get command to display
+ const getCommand = (): string => {
+ if (hookConfig.config.type === 'command') {
+ return hookConfig.config.command;
+ }
+ return '';
+ };
+
+ // Calculate box width for command display
+ const commandBoxWidth = Math.min(terminalWidth - 6, 80);
+
+ // Label width for alignment (Extension: is the longest label)
+ const labelWidth = 12;
+
+ return (
+
+ {/* Title */}
+
+
+ {t('Hook details')}
+
+
+
+ {/* Event */}
+
+
+ {t('Event:')}
+
+ {hookEvent.event}
+
+
+ {/* Type */}
+
+
+ {t('Type:')}
+
+ {getHookTypeDisplay()}
+
+
+ {/* Source */}
+
+
+ {t('Source:')}
+
+ {getSourceDisplay()}
+ {hookConfig.sourcePath && (
+ ({hookConfig.sourcePath})
+ )}
+
+
+ {/* Extension name (only for extensions) */}
+ {isFromExtension && hookConfig.sourceDisplay && (
+
+
+ {t('Extension:')}
+
+ {hookConfig.sourceDisplay}
+
+ )}
+
+ {/* Name (if exists) */}
+ {hookConfig.config.name && (
+
+
+ {t('Name:')}
+
+ {hookConfig.config.name}
+
+ )}
+
+ {/* Description (if exists) */}
+ {hookConfig.config.description && (
+
+
+ {t('Desc:')}
+
+
+ {hookConfig.config.description}
+
+
+ )}
+
+ {/* Command */}
+
+ {t('Command:')}
+
+
+ {/* Command box */}
+
+ {getCommand()}
+
+
+ {/* Help text */}
+
+
+ {t(
+ 'To modify or remove this hook, edit settings.json directly or ask Qwen to help.',
+ )}
+
+
+
+ {/* Footer hint */}
+
+ {t('Esc to go back')}
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx b/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx
index 294a16952..4e53d0988 100644
--- a/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx
+++ b/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx
@@ -24,6 +24,11 @@ vi.mock('../../hooks/useKeypress.js', () => ({
useKeypress: vi.fn(),
}));
+// Mock useTerminalSize
+vi.mock('../../hooks/useTerminalSize.js', () => ({
+ useTerminalSize: vi.fn(() => ({ columns: 100, rows: 24 })),
+}));
+
// Mock semantic-colors
vi.mock('../../semantic-colors.js', () => ({
theme: {
@@ -137,6 +142,7 @@ describe('HookDetailStep', () => {
const output = lastFrame();
expect(output).toContain('Configured hooks');
+ expect(output).toContain('[command]');
expect(output).toContain('hook-command-0');
expect(output).toContain('hook-command-1');
});
diff --git a/packages/cli/src/ui/components/hooks/HookDetailStep.tsx b/packages/cli/src/ui/components/hooks/HookDetailStep.tsx
index d5078eb31..0a99a5cb7 100644
--- a/packages/cli/src/ui/components/hooks/HookDetailStep.tsx
+++ b/packages/cli/src/ui/components/hooks/HookDetailStep.tsx
@@ -8,25 +8,34 @@ import { useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import { useKeypress } from '../../hooks/useKeypress.js';
+import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import type { HookEventDisplayInfo } from './types.js';
+import { HooksConfigSource } from '@qwen-code/qwen-code-core';
import { getTranslatedSourceDisplayMap } from './constants.js';
import { t } from '../../../i18n/index.js';
interface HookDetailStepProps {
hook: HookEventDisplayInfo;
onBack: () => void;
+ onSelectConfig?: (index: number) => void;
}
export function HookDetailStep({
hook,
onBack,
+ onSelectConfig,
}: HookDetailStepProps): React.JSX.Element {
const hasConfigs = hook.configs.length > 0;
const [selectedIndex, setSelectedIndex] = useState(0);
+ const { columns: terminalWidth } = useTerminalSize();
// Get translated source display map
const sourceDisplayMap = getTranslatedSourceDisplayMap();
+ // Calculate column widths (command: 70%, source: 30%)
+ const commandWidth = Math.floor(terminalWidth * 0.65);
+ const sourceWidth = Math.floor(terminalWidth * 0.3);
+
// Handle keyboard navigation
useKeypress(
(key) => {
@@ -39,12 +48,26 @@ export function HookDetailStep({
setSelectedIndex((prev) =>
Math.min(hook.configs.length - 1, prev + 1),
);
+ } else if (key.name === 'return' && onSelectConfig) {
+ onSelectConfig(selectedIndex);
}
}
},
{ isActive: true },
);
+ // Get source display for config list
+ const getConfigSourceDisplay = (config: {
+ source: HooksConfigSource;
+ sourceDisplay: string;
+ }): string => {
+ if (config.source === HooksConfigSource.Extensions) {
+ // For extensions, sourceDisplay is the extension name
+ return `${sourceDisplayMap[HooksConfigSource.Extensions]} (${config.sourceDisplay})`;
+ }
+ return sourceDisplayMap[config.source] || config.source;
+ };
+
return (
{/* Title */}
@@ -87,31 +110,49 @@ export function HookDetailStep({
{hook.configs.map((config, index) => {
const isSelected = index === selectedIndex;
- const sourceDisplay =
- sourceDisplayMap[config.source] || config.source;
+ const sourceDisplay = getConfigSourceDisplay(config);
+ const command =
+ config.config.type === 'command' ? config.config.command : '';
+ const hookType = config.config.type;
return (
-
+ {/* Left column: selector + command */}
+
+
+
+ {isSelected ? '❯' : ' '}
+
+
- {isSelected ? '❯' : ' '}
+ {`${index + 1}. [${hookType}] ${command}`}
+
+
+ {/* Right column: source */}
+
+
+ {sourceDisplay}
-
- {`${index + 1}. ${config.config.command}`}
-
- ·
- {sourceDisplay}
);
})}
- {t('Esc to go back')}
+ {onSelectConfig ? (
+
+ {t('Enter to select · Esc to go back')}
+
+ ) : (
+ {t('Esc to go back')}
+ )}
>
) : (
diff --git a/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx b/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx
index 562cdfeb9..7d49e8e6a 100644
--- a/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx
+++ b/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx
@@ -22,6 +22,7 @@ import type {
import { HOOKS_MANAGEMENT_STEPS } from './types.js';
import { HooksListStep } from './HooksListStep.js';
import { HookDetailStep } from './HookDetailStep.js';
+import { HookConfigDetailStep } from './HookConfigDetailStep.js';
import {
DISPLAY_HOOK_EVENTS,
getTranslatedSourceDisplayMap,
@@ -42,6 +43,7 @@ export function HooksManagementDialog({
HOOKS_MANAGEMENT_STEPS.HOOKS_LIST,
]);
const [selectedHookIndex, setSelectedHookIndex] = useState(-1);
+ const [selectedConfigIndex, setSelectedConfigIndex] = useState(-1);
const [hooks, setHooks] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState(null);
@@ -107,7 +109,8 @@ export function HooksManagementDialog({
hookInfo.configs.push({
config: hookConfig,
source: HooksConfigSource.Extensions,
- sourceDisplay: sourceDisplayMap[HooksConfigSource.Extensions],
+ sourceDisplay: extension.name,
+ sourcePath: extension.path,
enabled: true,
});
}
@@ -167,13 +170,23 @@ export function HooksManagementDialog({
});
}, [onClose]);
- // Select hook
+ // Select hook event
const handleSelectHook = useCallback((index: number) => {
setSelectedHookIndex(index);
+ setSelectedConfigIndex(-1);
setNavigationStack((prev) => [...prev, HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL]);
}, []);
- // Selected hook
+ // Select hook config
+ const handleSelectConfig = useCallback((index: number) => {
+ setSelectedConfigIndex(index);
+ setNavigationStack((prev) => [
+ ...prev,
+ HOOKS_MANAGEMENT_STEPS.HOOK_CONFIG_DETAIL,
+ ]);
+ }, []);
+
+ // Selected hook event
const selectedHook = useMemo(() => {
if (selectedHookIndex >= 0 && selectedHookIndex < hooks.length) {
return hooks[selectedHookIndex];
@@ -181,6 +194,18 @@ export function HooksManagementDialog({
return null;
}, [hooks, selectedHookIndex]);
+ // Selected hook config
+ const selectedConfig = useMemo(() => {
+ if (
+ selectedHook &&
+ selectedConfigIndex >= 0 &&
+ selectedConfigIndex < selectedHook.configs.length
+ ) {
+ return selectedHook.configs[selectedConfigIndex];
+ }
+ return null;
+ }, [selectedHook, selectedConfigIndex]);
+
// Render based on current step
const renderContent = () => {
const currentStep = getCurrentStep();
@@ -220,7 +245,11 @@ export function HooksManagementDialog({
case HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL:
if (selectedHook) {
return (
-
+
);
}
return (
@@ -229,6 +258,24 @@ export function HooksManagementDialog({
);
+ case HOOKS_MANAGEMENT_STEPS.HOOK_CONFIG_DETAIL:
+ if (selectedHook && selectedConfig) {
+ return (
+
+ );
+ }
+ return (
+
+
+ {t('No hook config selected')}
+
+
+ );
+
default:
return null;
}
diff --git a/packages/cli/src/ui/components/hooks/types.ts b/packages/cli/src/ui/components/hooks/types.ts
index 821aa8af8..c4d3d92ee 100644
--- a/packages/cli/src/ui/components/hooks/types.ts
+++ b/packages/cli/src/ui/components/hooks/types.ts
@@ -36,6 +36,7 @@ export interface HookConfigDisplayInfo {
config: HookConfig;
source: HooksConfigSource;
sourceDisplay: string;
+ sourcePath?: string;
enabled: boolean;
}
@@ -45,6 +46,7 @@ export interface HookConfigDisplayInfo {
export const HOOKS_MANAGEMENT_STEPS = {
HOOKS_LIST: 'hooks_list',
HOOK_DETAIL: 'hook_detail',
+ HOOK_CONFIG_DETAIL: 'hook_config_detail',
} as const;
export type HooksManagementStep =