Files
qwen-code/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx

174 lines
4.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useRef } from 'react';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import { WizardStepProps, WizardAction } from '../types.js';
import { sanitizeInput } from '../utils.js';
import { Config, subagentGenerator } from '@qwen-code/qwen-code-core';
import { useKeypress, Key } from '../../../hooks/useKeypress.js';
import { keyMatchers, Command } from '../../../keyMatchers.js';
import { theme } from '../../../semantic-colors.js';
import { Colors } from '../../../colors.js';
import { TextInput } from '../../shared/TextInput.js';
/**
* Step 3: Description input with LLM generation.
*/
export function DescriptionInput({
state,
dispatch,
onNext,
config,
}: WizardStepProps) {
const abortControllerRef = useRef<AbortController | null>(null);
const handleTextChange = useCallback(
(text: string) => {
const sanitized = sanitizeInput(text);
dispatch({
type: 'SET_USER_DESCRIPTION',
description: sanitized,
});
},
[dispatch],
);
// TextInput will manage its own buffer; we just pass value and handlers
const handleGenerate = useCallback(
async (
userDescription: string,
dispatch: (action: WizardAction) => void,
config: Config,
): Promise<void> => {
const abortController = new AbortController();
abortControllerRef.current = abortController;
try {
const generated = await subagentGenerator(
userDescription,
config.getGeminiClient(),
abortController.signal,
);
// Only dispatch if not aborted
if (!abortController.signal.aborted) {
dispatch({
type: 'SET_GENERATED_CONTENT',
name: generated.name,
description: generated.description,
systemPrompt: generated.systemPrompt,
});
onNext();
}
} finally {
abortControllerRef.current = null;
}
},
[onNext],
);
const handleSubmit = useCallback(async () => {
if (!state.canProceed || state.isGenerating) {
return;
}
const inputValue = state.userDescription.trim();
if (!inputValue) {
return;
}
// Start LLM generation
dispatch({ type: 'SET_GENERATING', isGenerating: true });
try {
if (!config) {
throw new Error('Configuration not available');
}
// Use real LLM integration
await handleGenerate(inputValue, dispatch, config);
} catch (error) {
dispatch({ type: 'SET_GENERATING', isGenerating: false });
// Don't show error if it was cancelled by user
if (error instanceof Error && error.name === 'AbortError') {
return;
}
dispatch({
type: 'SET_VALIDATION_ERRORS',
errors: [
`Failed to generate subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
],
});
}
}, [
state.canProceed,
state.isGenerating,
state.userDescription,
dispatch,
config,
handleGenerate,
]);
// Handle keyboard input during generation
const handleGenerationKeypress = useCallback(
(key: Key) => {
if (keyMatchers[Command.ESCAPE](key)) {
if (abortControllerRef.current) {
// Cancel the ongoing generation
abortControllerRef.current.abort();
dispatch({ type: 'SET_GENERATING', isGenerating: false });
}
}
},
[dispatch],
);
// Use separate keypress handlers for different states
useKeypress(handleGenerationKeypress, {
isActive: state.isGenerating,
});
const placeholder =
'e.g., Expert code reviewer that reviews code based on best practices...';
return (
<Box flexDirection="column" gap={1}>
<Box>
<Text color={Colors.Gray}>
Describe what this subagent should do and when it should be used. (Be
comprehensive for best results)
</Text>
</Box>
{state.isGenerating ? (
<Box>
<Box marginRight={1}>
<Spinner />
</Box>
<Text color={theme.text.accent}>
Generating subagent configuration...
</Text>
</Box>
) : (
<TextInput
value={state.userDescription || ''}
onChange={handleTextChange}
onSubmit={handleSubmit}
placeholder={placeholder}
height={10}
isActive={!state.isGenerating}
validationErrors={state.validationErrors}
/>
)}
</Box>
);
}