diff --git a/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx index 2c7385215..1f2728965 100644 --- a/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx +++ b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.test.tsx @@ -19,11 +19,6 @@ 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 })), @@ -44,8 +39,6 @@ vi.mock('../../semantic-colors.js', () => ({ })); describe('HookConfigDetailStep', () => { - const mockOnBack = vi.fn(); - const createMockHookEvent = (): HookEventDisplayInfo => ({ event: HookEventName.Stop, shortDescription: 'Right before Qwen Code concludes its response', @@ -85,11 +78,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Hook details'); @@ -100,11 +89,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Event:'); @@ -116,11 +101,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Type:'); @@ -132,11 +113,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(HooksConfigSource.User); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Source:'); @@ -148,11 +125,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(HooksConfigSource.Project); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Local Settings'); @@ -167,11 +140,7 @@ describe('HookConfigDetailStep', () => { ); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Extensions'); @@ -186,11 +155,7 @@ describe('HookConfigDetailStep', () => { ); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Extension:'); @@ -202,11 +167,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(HooksConfigSource.User); const { lastFrame } = render( - , + , ); // Should not have Extension label for User Settings @@ -220,11 +181,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Command:'); @@ -245,11 +202,7 @@ describe('HookConfigDetailStep', () => { }; const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Name:'); @@ -270,11 +223,7 @@ describe('HookConfigDetailStep', () => { }; const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Desc:'); @@ -286,11 +235,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('To modify or remove this hook'); @@ -301,11 +246,7 @@ describe('HookConfigDetailStep', () => { const hookConfig = createMockHookConfig(); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Esc to go back'); @@ -330,11 +271,7 @@ describe('HookConfigDetailStep', () => { 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 index e83345b43..27f3016a1 100644 --- a/packages/cli/src/ui/components/hooks/HookConfigDetailStep.tsx +++ b/packages/cli/src/ui/components/hooks/HookConfigDetailStep.tsx @@ -6,7 +6,6 @@ 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'; @@ -15,25 +14,14 @@ 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) { diff --git a/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx b/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx index 4e53d0988..0b5f1c6b7 100644 --- a/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx +++ b/packages/cli/src/ui/components/hooks/HookDetailStep.test.tsx @@ -19,11 +19,6 @@ 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 })), @@ -45,8 +40,6 @@ vi.mock('../../semantic-colors.js', () => ({ })); describe('HookDetailStep', () => { - const mockOnBack = vi.fn(); - const createMockHookInfo = ( event: HookEventName, configCount = 0, @@ -78,7 +71,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain(HookEventName.PreToolUse); @@ -88,7 +81,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse, 0, true); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Detailed description for PreToolUse'); @@ -98,7 +91,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.Stop, 0, false); const { lastFrame } = render( - , + , ); // Stop event has empty description @@ -110,7 +103,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse); const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -125,7 +118,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse, 0); const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -137,7 +130,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse, 2); const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -151,7 +144,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse, 2); const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -163,7 +156,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse, 3); const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -174,7 +167,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PreToolUse); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('Esc to go back'); @@ -184,7 +177,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(HookEventName.PostToolUse, 5); const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -205,7 +198,7 @@ describe('HookDetailStep', () => { }; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -226,7 +219,7 @@ describe('HookDetailStep', () => { const hook = createMockHookInfo(event, 1); const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain(event); diff --git a/packages/cli/src/ui/components/hooks/HookDetailStep.tsx b/packages/cli/src/ui/components/hooks/HookDetailStep.tsx index 0a99a5cb7..69c5d24e3 100644 --- a/packages/cli/src/ui/components/hooks/HookDetailStep.tsx +++ b/packages/cli/src/ui/components/hooks/HookDetailStep.tsx @@ -4,10 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -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'; @@ -16,17 +14,14 @@ import { t } from '../../../i18n/index.js'; interface HookDetailStepProps { hook: HookEventDisplayInfo; - onBack: () => void; - onSelectConfig?: (index: number) => void; + selectedIndex: number; } export function HookDetailStep({ hook, - onBack, - onSelectConfig, + selectedIndex, }: HookDetailStepProps): React.JSX.Element { const hasConfigs = hook.configs.length > 0; - const [selectedIndex, setSelectedIndex] = useState(0); const { columns: terminalWidth } = useTerminalSize(); // Get translated source display map @@ -36,26 +31,6 @@ export function HookDetailStep({ const commandWidth = Math.floor(terminalWidth * 0.65); const sourceWidth = Math.floor(terminalWidth * 0.3); - // Handle keyboard navigation - useKeypress( - (key) => { - if (key.name === 'escape') { - onBack(); - } else if (hasConfigs) { - if (key.name === 'up') { - setSelectedIndex((prev) => Math.max(0, prev - 1)); - } else if (key.name === 'down') { - 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; @@ -136,6 +111,8 @@ export function HookDetailStep({ {`${index + 1}. [${hookType}] ${command}`} + {/* Spacer between columns */} + {/* Right column: source */} @@ -146,13 +123,9 @@ export function HookDetailStep({ ); })} - {onSelectConfig ? ( - - {t('Enter to select · Esc to go back')} - - ) : ( - {t('Esc to go back')} - )} + + {t('Enter to select · Esc to go back')} + ) : ( diff --git a/packages/cli/src/ui/components/hooks/HooksListStep.test.tsx b/packages/cli/src/ui/components/hooks/HooksListStep.test.tsx index 8d4b5f79f..5f60763bd 100644 --- a/packages/cli/src/ui/components/hooks/HooksListStep.test.tsx +++ b/packages/cli/src/ui/components/hooks/HooksListStep.test.tsx @@ -33,11 +33,6 @@ vi.mock('../../hooks/useTerminalSize.js', () => ({ useTerminalSize: vi.fn(() => ({ columns: 120, rows: 24 })), })); -// Mock useKeypress -vi.mock('../../hooks/useKeypress.js', () => ({ - useKeypress: vi.fn(), -})); - // Mock semantic-colors vi.mock('../../semantic-colors.js', () => ({ theme: { @@ -54,9 +49,6 @@ vi.mock('../../semantic-colors.js', () => ({ })); describe('HooksListStep', () => { - const mockOnSelect = vi.fn(); - const mockOnCancel = vi.fn(); - const createMockHookInfo = ( event: HookEventName, configCount = 0, @@ -84,11 +76,7 @@ describe('HooksListStep', () => { it('should render empty state when no hooks', () => { const { lastFrame } = render( - , + , ); expect(lastFrame()).toContain('No hook events found'); @@ -101,11 +89,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -121,11 +105,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -140,11 +120,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -157,11 +133,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -174,11 +146,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -192,11 +160,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -211,11 +175,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -228,11 +188,7 @@ describe('HooksListStep', () => { ]; const { lastFrame } = render( - , + , ); const output = lastFrame(); @@ -245,11 +201,7 @@ describe('HooksListStep', () => { .map((_, i) => createMockHookInfo(`${i}` as HookEventName)); const { lastFrame } = render( - , + , ); const output = lastFrame(); diff --git a/packages/cli/src/ui/components/hooks/HooksListStep.tsx b/packages/cli/src/ui/components/hooks/HooksListStep.tsx index 17b4e1b09..5b3da41f5 100644 --- a/packages/cli/src/ui/components/hooks/HooksListStep.tsx +++ b/packages/cli/src/ui/components/hooks/HooksListStep.tsx @@ -4,26 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -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 { t } from '../../../i18n/index.js'; interface HooksListStepProps { hooks: HookEventDisplayInfo[]; - onSelect: (index: number) => void; - onCancel: () => void; + selectedIndex: number; } export function HooksListStep({ hooks, - onSelect, - onCancel, + selectedIndex, }: HooksListStepProps): React.JSX.Element { - const [selectedIndex, setSelectedIndex] = useState(0); const { columns: terminalWidth } = useTerminalSize(); // Calculate responsive width for hook name column (min 20, max 35) @@ -32,21 +27,6 @@ export function HooksListStep({ Math.max(20, Math.floor(terminalWidth * 0.25)), ); - useKeypress( - (key) => { - if (key.name === 'up') { - setSelectedIndex((prev) => Math.max(0, prev - 1)); - } else if (key.name === 'down') { - setSelectedIndex((prev) => Math.min(hooks.length - 1, prev + 1)); - } else if (key.name === 'return') { - onSelect(selectedIndex); - } else if (key.name === 'escape') { - onCancel(); - } - }, - { isActive: true }, - ); - if (hooks.length === 0) { return ( diff --git a/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx b/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx index 7d49e8e6a..8db0633d5 100644 --- a/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx +++ b/packages/cli/src/ui/components/hooks/HooksManagementDialog.tsx @@ -8,11 +8,13 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../../semantic-colors.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; import { useConfig } from '../../contexts/ConfigContext.js'; import { loadSettings, SettingScope } from '../../../config/settings.js'; import { HooksConfigSource, type HookDefinition, + type HookConfig, createDebugLogger, } from '@qwen-code/qwen-code-core'; import type { @@ -32,6 +34,71 @@ import { t } from '../../../i18n/index.js'; const debugLogger = createDebugLogger('HOOKS_DIALOG'); +/** + * Type guard to check if a value is a valid HookConfig + */ +function isValidHookConfig(config: unknown): config is HookConfig { + return ( + typeof config === 'object' && + config !== null && + 'type' in config && + 'command' in config && + typeof (config as HookConfig).command === 'string' + ); +} + +/** + * Type guard to check if a value is a valid HookDefinition + */ +function isValidHookDefinition(def: unknown): def is HookDefinition { + if (typeof def !== 'object' || def === null) { + return false; + } + const obj = def as Record; + // hooks array is required + if (!('hooks' in obj) || !Array.isArray(obj['hooks'])) { + return false; + } + // Validate each hook config in the array + for (const hook of obj['hooks']) { + if (!isValidHookConfig(hook)) { + return false; + } + } + // matcher is optional but must be a string if present + if ('matcher' in obj && typeof obj['matcher'] !== 'string') { + return false; + } + // sequential is optional but must be a boolean if present + if ('sequential' in obj && typeof obj['sequential'] !== 'boolean') { + return false; + } + return true; +} + +/** + * Type guard to check if a value is a valid hooks record + */ +function isValidHooksRecord( + hooks: unknown, +): hooks is Record { + if (typeof hooks !== 'object' || hooks === null) { + return false; + } + const record = hooks as Record; + for (const value of Object.values(record)) { + if (!Array.isArray(value)) { + return false; + } + for (const def of value) { + if (!isValidHookDefinition(def)) { + return false; + } + } + } + return true; +} + export function HooksManagementDialog({ onClose, }: HooksManagementDialogProps): React.JSX.Element { @@ -44,10 +111,94 @@ export function HooksManagementDialog({ ]); const [selectedHookIndex, setSelectedHookIndex] = useState(-1); const [selectedConfigIndex, setSelectedConfigIndex] = useState(-1); + // Track selected index within each step for keyboard navigation + const [listSelectedIndex, setListSelectedIndex] = useState(0); + const [detailSelectedIndex, setDetailSelectedIndex] = useState(0); const [hooks, setHooks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); + // Current step + const currentStep = + navigationStack[navigationStack.length - 1] || + HOOKS_MANAGEMENT_STEPS.HOOKS_LIST; + + // Selected hook event + const selectedHook = useMemo(() => { + if (selectedHookIndex >= 0 && selectedHookIndex < hooks.length) { + return hooks[selectedHookIndex]; + } + return null; + }, [hooks, selectedHookIndex]); + + // Centralized keyboard handler + useKeypress( + (key) => { + if (isLoading || loadError) { + // Allow Escape to close even during loading/error states + if (key.name === 'escape') { + onClose(); + } + return; + } + + switch (currentStep) { + case HOOKS_MANAGEMENT_STEPS.HOOKS_LIST: + if (key.name === 'up') { + setListSelectedIndex((prev) => Math.max(0, prev - 1)); + } else if (key.name === 'down') { + setListSelectedIndex((prev) => + Math.min(hooks.length - 1, prev + 1), + ); + } else if (key.name === 'return') { + if (hooks.length > 0 && listSelectedIndex >= 0) { + setSelectedHookIndex(listSelectedIndex); + setSelectedConfigIndex(-1); + setDetailSelectedIndex(0); + setNavigationStack((prev) => [ + ...prev, + HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL, + ]); + } + } else if (key.name === 'escape') { + onClose(); + } + break; + + case HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL: + if (key.name === 'escape') { + handleNavigateBack(); + } else if (selectedHook && selectedHook.configs.length > 0) { + if (key.name === 'up') { + setDetailSelectedIndex((prev) => Math.max(0, prev - 1)); + } else if (key.name === 'down') { + setDetailSelectedIndex((prev) => + Math.min(selectedHook.configs.length - 1, prev + 1), + ); + } else if (key.name === 'return') { + setSelectedConfigIndex(detailSelectedIndex); + setNavigationStack((prev) => [ + ...prev, + HOOKS_MANAGEMENT_STEPS.HOOK_CONFIG_DETAIL, + ]); + } + } + break; + + case HOOKS_MANAGEMENT_STEPS.HOOK_CONFIG_DETAIL: + if (key.name === 'escape') { + handleNavigateBack(); + } + break; + + default: + // No action for unknown steps + break; + } + }, + { isActive: true }, + ); + // Load hooks data const fetchHooksData = useCallback((): HookEventDisplayInfo[] => { if (!config) return []; @@ -66,12 +217,11 @@ export function HooksManagementDialog({ for (const eventName of DISPLAY_HOOK_EVENTS) { const hookInfo = createEmptyHookEventInfo(eventName); - // Get hooks from user settings - const userHooks = (userSettings as Record)?.['hooks'] as - | Record - | undefined; - if (userHooks?.[eventName]) { - for (const def of userHooks[eventName]) { + // Get hooks from user settings (with type validation) + const userSettingsRecord = userSettings as Record; + const userHooksRaw = userSettingsRecord?.['hooks']; + if (isValidHooksRecord(userHooksRaw) && userHooksRaw[eventName]) { + for (const def of userHooksRaw[eventName]) { for (const hookConfig of def.hooks) { hookInfo.configs.push({ config: hookConfig, @@ -83,12 +233,17 @@ export function HooksManagementDialog({ } } - // Get hooks from workspace settings - const workspaceHooks = (workspaceSettings as Record)?.[ - 'hooks' - ] as Record | undefined; - if (workspaceHooks?.[eventName]) { - for (const def of workspaceHooks[eventName]) { + // Get hooks from workspace settings (with type validation) + const workspaceSettingsRecord = workspaceSettings as Record< + string, + unknown + >; + const workspaceHooksRaw = workspaceSettingsRecord?.['hooks']; + if ( + isValidHooksRecord(workspaceHooksRaw) && + workspaceHooksRaw[eventName] + ) { + for (const def of workspaceHooksRaw[eventName]) { for (const hookConfig of def.hooks) { hookInfo.configs.push({ config: hookConfig, @@ -100,19 +255,24 @@ export function HooksManagementDialog({ } } - // Get hooks from extensions + // Get hooks from extensions (with type validation) const extensions = config.getExtensions() || []; for (const extension of extensions) { if (extension.isActive && extension.hooks?.[eventName]) { - for (const def of extension.hooks[eventName]!) { - for (const hookConfig of def.hooks) { - hookInfo.configs.push({ - config: hookConfig, - source: HooksConfigSource.Extensions, - sourceDisplay: extension.name, - sourcePath: extension.path, - enabled: true, - }); + const extensionHooks = extension.hooks[eventName]; + if (Array.isArray(extensionHooks)) { + for (const def of extensionHooks) { + if (isValidHookDefinition(def)) { + for (const hookConfig of def.hooks) { + hookInfo.configs.push({ + config: hookConfig, + source: HooksConfigSource.Extensions, + sourceDisplay: extension.name, + sourcePath: extension.path, + enabled: true, + }); + } + } } } } @@ -151,15 +311,7 @@ export function HooksManagementDialog({ }; }, [fetchHooksData]); - // Current step - const getCurrentStep = useCallback( - () => - navigationStack[navigationStack.length - 1] || - HOOKS_MANAGEMENT_STEPS.HOOKS_LIST, - [navigationStack], - ); - - // Navigation handlers + // Navigation handler for going back const handleNavigateBack = useCallback(() => { setNavigationStack((prev) => { if (prev.length <= 1) { @@ -170,30 +322,6 @@ export function HooksManagementDialog({ }); }, [onClose]); - // Select hook event - const handleSelectHook = useCallback((index: number) => { - setSelectedHookIndex(index); - setSelectedConfigIndex(-1); - setNavigationStack((prev) => [...prev, HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL]); - }, []); - - // 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]; - } - return null; - }, [hooks, selectedHookIndex]); - // Selected hook config const selectedConfig = useMemo(() => { if ( @@ -208,8 +336,6 @@ export function HooksManagementDialog({ // Render based on current step const renderContent = () => { - const currentStep = getCurrentStep(); - if (isLoading) { return ( @@ -235,11 +361,7 @@ export function HooksManagementDialog({ switch (currentStep) { case HOOKS_MANAGEMENT_STEPS.HOOKS_LIST: return ( - + ); case HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL: @@ -247,8 +369,7 @@ export function HooksManagementDialog({ return ( ); } @@ -264,7 +385,6 @@ export function HooksManagementDialog({ ); } diff --git a/packages/core/src/extension/variables.ts b/packages/core/src/extension/variables.ts index 31c1a28e3..d9c623e78 100644 --- a/packages/core/src/extension/variables.ts +++ b/packages/core/src/extension/variables.ts @@ -7,7 +7,8 @@ import { type VariableSchema, VARIABLE_SCHEMA } from './variableSchema.js'; import path from 'node:path'; import { QWEN_DIR } from '../config/storage.js'; -import type { HookDefinition, HookEventName } from '../hooks/types.js'; +import type { HookDefinition } from '../hooks/types.js'; +import type { HookEventName } from '../hooks/types.js'; import * as fs from 'node:fs'; import { glob } from 'glob'; import { createDebugLogger } from '../utils/debugLogger.js';