/** * @license * Copyright 2025 Qwen * SPDX-License-Identifier: Apache-2.0 */ import { useReducer, useCallback, useMemo } from 'react'; import { Box, Text } from 'ink'; import { wizardReducer, initialWizardState } from '../reducers.js'; import { LocationSelector } from './LocationSelector.js'; import { GenerationMethodSelector } from './GenerationMethodSelector.js'; import { DescriptionInput } from './DescriptionInput.js'; import { ToolSelector } from './ToolSelector.js'; import { ColorSelector } from './ColorSelector.js'; import { CreationSummary } from './CreationSummary.js'; import { WizardStepProps } from '../types.js'; import { WIZARD_STEPS } from '../constants.js'; import { getStepKind } from '../utils.js'; import { Config } from '@qwen-code/qwen-code-core'; import { Colors } from '../../../colors.js'; import { theme } from '../../../semantic-colors.js'; import { TextEntryStep } from './TextEntryStep.js'; import { useKeypress } from '../../../hooks/useKeypress.js'; interface AgentCreationWizardProps { onClose: () => void; config: Config | null; } /** * Main orchestrator component for the subagent creation wizard. */ export function AgentCreationWizard({ onClose, config, }: AgentCreationWizardProps) { const [state, dispatch] = useReducer(wizardReducer, initialWizardState); const handleNext = useCallback(() => { dispatch({ type: 'GO_TO_NEXT_STEP' }); }, []); const handlePrevious = useCallback(() => { dispatch({ type: 'GO_TO_PREVIOUS_STEP' }); }, []); const handleCancel = useCallback(() => { dispatch({ type: 'RESET_WIZARD' }); onClose(); }, [onClose]); // Centralized ESC key handling for the entire wizard useKeypress( (key) => { if (key.name !== 'escape') { return; } // LLM DescriptionInput handles its own ESC logic when generating const kind = getStepKind(state.generationMethod, state.currentStep); if (kind === 'LLM_DESC' && state.isGenerating) { return; // Let DescriptionInput handle it } if (state.currentStep === WIZARD_STEPS.LOCATION_SELECTION) { // On first step, ESC cancels the entire wizard handleCancel(); } else { // On other steps, ESC goes back to previous step handlePrevious(); } }, { isActive: true }, ); const stepProps: WizardStepProps = useMemo( () => ({ state, dispatch, onNext: handleNext, onPrevious: handlePrevious, onCancel: handleCancel, config, }), [state, dispatch, handleNext, handlePrevious, handleCancel, config], ); const renderStepHeader = useCallback(() => { const getStepHeaderText = () => { const kind = getStepKind(state.generationMethod, state.currentStep); const n = state.currentStep; switch (kind) { case 'LOCATION': return `Step ${n}: Choose Location`; case 'GEN_METHOD': return `Step ${n}: Choose Generation Method`; case 'LLM_DESC': return `Step ${n}: Describe Your Subagent`; case 'MANUAL_NAME': return `Step ${n}: Enter Subagent Name`; case 'MANUAL_PROMPT': return `Step ${n}: Enter System Prompt`; case 'MANUAL_DESC': return `Step ${n}: Enter Description`; case 'TOOLS': return `Step ${n}: Select Tools`; case 'COLOR': return `Step ${n}: Choose Background Color`; case 'FINAL': return `Step ${n}: Confirm and Save`; default: return 'Unknown Step'; } }; return ( {getStepHeaderText()} ); }, [state.currentStep, state.generationMethod]); const renderDebugContent = useCallback(() => { if (process.env['NODE_ENV'] !== 'development') { return null; } return ( Debug Info: Step: {state.currentStep} Can Proceed: {state.canProceed ? 'Yes' : 'No'} Generating: {state.isGenerating ? 'Yes' : 'No'} Location: {state.location} Method: {state.generationMethod} {state.validationErrors.length > 0 && ( Errors: {state.validationErrors.join(', ')} )} ); }, [ state.currentStep, state.canProceed, state.isGenerating, state.location, state.generationMethod, state.validationErrors, ]); const renderStepFooter = useCallback(() => { const getNavigationInstructions = () => { // Special case: During generation in description input step, only show cancel option const kind = getStepKind(state.generationMethod, state.currentStep); if (kind === 'LLM_DESC' && state.isGenerating) { return 'Esc to cancel'; } if (getStepKind(state.generationMethod, state.currentStep) === 'FINAL') { return 'Press Enter to save, e to save and edit, Esc to go back'; } // Steps that have ↑↓ navigation (RadioButtonSelect components) const kindForNav = getStepKind(state.generationMethod, state.currentStep); const hasNavigation = kindForNav === 'LOCATION' || kindForNav === 'GEN_METHOD' || kindForNav === 'TOOLS' || kindForNav === 'COLOR'; const navigationPart = hasNavigation ? '↑↓ to navigate, ' : ''; const escAction = state.currentStep === WIZARD_STEPS.LOCATION_SELECTION ? 'cancel' : 'go back'; return `Press Enter to continue, ${navigationPart}Esc to ${escAction}`; }; return ( {getNavigationInstructions()} ); }, [state.currentStep, state.isGenerating, state.generationMethod]); const renderStepContent = useCallback(() => { const kind = getStepKind(state.generationMethod, state.currentStep); switch (kind) { case 'LOCATION': return ; case 'GEN_METHOD': return ; case 'LLM_DESC': return ; case 'MANUAL_NAME': return ( { const value = t; // keep raw, trim later when validating dispatch({ type: 'SET_GENERATED_NAME', name: value }); }} validate={(t) => t.trim().length === 0 ? 'Name cannot be empty.' : null } /> ); case 'MANUAL_PROMPT': return ( { dispatch({ type: 'SET_GENERATED_SYSTEM_PROMPT', systemPrompt: t, }); }} validate={(t) => t.trim().length === 0 ? 'System prompt cannot be empty.' : null } /> ); case 'MANUAL_DESC': return ( { dispatch({ type: 'SET_GENERATED_DESCRIPTION', description: t }); }} validate={(t) => t.trim().length === 0 ? 'Description cannot be empty.' : null } /> ); case 'TOOLS': return ( { dispatch({ type: 'SET_TOOLS', tools }); handleNext(); }} config={config} /> ); case 'COLOR': return ( { dispatch({ type: 'SET_BACKGROUND_COLOR', color }); handleNext(); }} /> ); case 'FINAL': return ; default: return ( Invalid step: {state.currentStep} ); } }, [stepProps, state, config, handleNext, dispatch]); return ( {/* Main content wrapped in bounding box */} {renderStepHeader()} {renderStepContent()} {renderDebugContent()} {renderStepFooter()} ); }