/** * @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(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 => { 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 ( Describe what this subagent should do and when it should be used. (Be comprehensive for best results) {state.isGenerating ? ( Generating subagent configuration... ) : ( )} ); }