mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 12:11:09 +00:00
fix comment
This commit is contained in:
parent
a0b3cc3268
commit
50ade83e4d
8 changed files with 232 additions and 288 deletions
|
|
@ -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(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Hook details');
|
||||
|
|
@ -100,11 +89,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig();
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Event:');
|
||||
|
|
@ -116,11 +101,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig();
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Type:');
|
||||
|
|
@ -132,11 +113,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig(HooksConfigSource.User);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Source:');
|
||||
|
|
@ -148,11 +125,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig(HooksConfigSource.Project);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Local Settings');
|
||||
|
|
@ -167,11 +140,7 @@ describe('HookConfigDetailStep', () => {
|
|||
);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Extensions');
|
||||
|
|
@ -186,11 +155,7 @@ describe('HookConfigDetailStep', () => {
|
|||
);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Extension:');
|
||||
|
|
@ -202,11 +167,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig(HooksConfigSource.User);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
// Should not have Extension label for User Settings
|
||||
|
|
@ -220,11 +181,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig();
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Command:');
|
||||
|
|
@ -245,11 +202,7 @@ describe('HookConfigDetailStep', () => {
|
|||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Name:');
|
||||
|
|
@ -270,11 +223,7 @@ describe('HookConfigDetailStep', () => {
|
|||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Desc:');
|
||||
|
|
@ -286,11 +235,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig();
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('To modify or remove this hook');
|
||||
|
|
@ -301,11 +246,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig();
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Esc to go back');
|
||||
|
|
@ -330,11 +271,7 @@ describe('HookConfigDetailStep', () => {
|
|||
const hookConfig = createMockHookConfig();
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookConfigDetailStep
|
||||
hookEvent={hookEvent}
|
||||
hookConfig={hookConfig}
|
||||
onBack={mockOnBack}
|
||||
/>,
|
||||
<HookConfigDetailStep hookEvent={hookEvent} hookConfig={hookConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain(event);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain(HookEventName.PreToolUse);
|
||||
|
|
@ -88,7 +81,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse, 0, true);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Detailed description for PreToolUse');
|
||||
|
|
@ -98,7 +91,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.Stop, 0, false);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
// Stop event has empty description
|
||||
|
|
@ -110,7 +103,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -125,7 +118,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse, 0);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -137,7 +130,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse, 2);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -151,7 +144,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse, 2);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -163,7 +156,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse, 3);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -174,7 +167,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PreToolUse);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Esc to go back');
|
||||
|
|
@ -184,7 +177,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(HookEventName.PostToolUse, 5);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -205,7 +198,7 @@ describe('HookDetailStep', () => {
|
|||
};
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -226,7 +219,7 @@ describe('HookDetailStep', () => {
|
|||
const hook = createMockHookInfo(event, 1);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HookDetailStep hook={hook} onBack={mockOnBack} />,
|
||||
<HookDetailStep hook={hook} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain(event);
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
</Text>
|
||||
</Box>
|
||||
{/* Spacer between columns */}
|
||||
<Box width={2} />
|
||||
{/* Right column: source */}
|
||||
<Box width={sourceWidth}>
|
||||
<Text color={theme.text.secondary} wrap="wrap">
|
||||
|
|
@ -146,13 +123,9 @@ export function HookDetailStep({
|
|||
);
|
||||
})}
|
||||
<Box marginTop={1}>
|
||||
{onSelectConfig ? (
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Enter to select · Esc to go back')}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={theme.text.secondary}>{t('Esc to go back')}</Text>
|
||||
)}
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Enter to select · Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<HooksListStep
|
||||
hooks={[]}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={[]} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('No hook events found');
|
||||
|
|
@ -101,11 +89,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -121,11 +105,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -140,11 +120,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -157,11 +133,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -174,11 +146,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -192,11 +160,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -211,11 +175,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -228,11 +188,7 @@ describe('HooksListStep', () => {
|
|||
];
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
@ -245,11 +201,7 @@ describe('HooksListStep', () => {
|
|||
.map((_, i) => createMockHookInfo(`${i}` as HookEventName));
|
||||
|
||||
const { lastFrame } = render(
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={mockOnSelect}
|
||||
onCancel={mockOnCancel}
|
||||
/>,
|
||||
<HooksListStep hooks={hooks} selectedIndex={0} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
// 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<string, HookDefinition[]> {
|
||||
if (typeof hooks !== 'object' || hooks === null) {
|
||||
return false;
|
||||
}
|
||||
const record = hooks as Record<string, unknown>;
|
||||
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<number>(-1);
|
||||
const [selectedConfigIndex, setSelectedConfigIndex] = useState<number>(-1);
|
||||
// Track selected index within each step for keyboard navigation
|
||||
const [listSelectedIndex, setListSelectedIndex] = useState<number>(0);
|
||||
const [detailSelectedIndex, setDetailSelectedIndex] = useState<number>(0);
|
||||
const [hooks, setHooks] = useState<HookEventDisplayInfo[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(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<string, unknown>)?.['hooks'] as
|
||||
| Record<string, HookDefinition[]>
|
||||
| undefined;
|
||||
if (userHooks?.[eventName]) {
|
||||
for (const def of userHooks[eventName]) {
|
||||
// Get hooks from user settings (with type validation)
|
||||
const userSettingsRecord = userSettings as Record<string, unknown>;
|
||||
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<string, unknown>)?.[
|
||||
'hooks'
|
||||
] as Record<string, HookDefinition[]> | 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 (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
|
|
@ -235,11 +361,7 @@ export function HooksManagementDialog({
|
|||
switch (currentStep) {
|
||||
case HOOKS_MANAGEMENT_STEPS.HOOKS_LIST:
|
||||
return (
|
||||
<HooksListStep
|
||||
hooks={hooks}
|
||||
onSelect={handleSelectHook}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
<HooksListStep hooks={hooks} selectedIndex={listSelectedIndex} />
|
||||
);
|
||||
|
||||
case HOOKS_MANAGEMENT_STEPS.HOOK_DETAIL:
|
||||
|
|
@ -247,8 +369,7 @@ export function HooksManagementDialog({
|
|||
return (
|
||||
<HookDetailStep
|
||||
hook={selectedHook}
|
||||
onBack={handleNavigateBack}
|
||||
onSelectConfig={handleSelectConfig}
|
||||
selectedIndex={detailSelectedIndex}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -264,7 +385,6 @@ export function HooksManagementDialog({
|
|||
<HookConfigDetailStep
|
||||
hookEvent={selectedHook}
|
||||
hookConfig={selectedConfig}
|
||||
onBack={handleNavigateBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue