diff --git a/packages/cli/src/ui/components/shared/TextInput.tsx b/packages/cli/src/ui/components/shared/TextInput.tsx new file mode 100644 index 00000000..40d47129 --- /dev/null +++ b/packages/cli/src/ui/components/shared/TextInput.tsx @@ -0,0 +1,194 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +// no hooks needed beyond keypress handled inside +import { Box, Text } from 'ink'; +import chalk from 'chalk'; +import stringWidth from 'string-width'; +import { useTextBuffer } from './text-buffer.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; +import { keyMatchers, Command } from '../../keyMatchers.js'; +import { cpSlice, cpLen } from '../../utils/textUtils.js'; +import { theme } from '../../semantic-colors.js'; +import { Colors } from '../../colors.js'; +import type { Key } from '../../hooks/useKeypress.js'; +import { useCallback, useRef, useEffect } from 'react'; + +export interface TextInputProps { + value: string; + onChange: (text: string) => void; + onSubmit?: () => void; + placeholder?: string; + height?: number; // lines in viewport; >1 enables multiline + isActive?: boolean; // when false, ignore keypresses + validationErrors?: string[]; + inputWidth?: number; +} + +export function TextInput({ + value, + onChange, + onSubmit, + placeholder, + height = 1, + isActive = true, + validationErrors = [], + inputWidth = 80, +}: TextInputProps) { + const allowMultiline = height > 1; + + // Stabilize onChange to avoid triggering useTextBuffer's onChange effect every render + const onChangeRef = useRef(onChange); + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + const stableOnChange = useCallback((text: string) => { + onChangeRef.current?.(text); + }, []); + + const buffer = useTextBuffer({ + initialText: value || '', + viewport: { height, width: inputWidth }, + isValidPath: () => false, + onChange: stableOnChange, + }); + + const handleSubmit = () => { + if (!onSubmit) return; + onSubmit(); + }; + + useKeypress( + (key: Key) => { + if (!buffer || !isActive) return; + + // Submit on Enter + if (keyMatchers[Command.SUBMIT](key) || key.name === 'return') { + if (allowMultiline) { + const [row, col] = buffer.cursor; + const line = buffer.lines[row]; + const charBefore = col > 0 ? cpSlice(line, col - 1, col) : ''; + if (charBefore === '\\') { + buffer.backspace(); + buffer.newline(); + } else { + handleSubmit(); + } + } else { + handleSubmit(); + } + return; + } + + // Multiline newline insertion (Shift+Enter etc.) + if (allowMultiline && keyMatchers[Command.NEWLINE](key)) { + buffer.newline(); + return; + } + + // Navigation helpers + if (keyMatchers[Command.HOME](key)) { + buffer.move('home'); + return; + } + if (keyMatchers[Command.END](key)) { + buffer.move('end'); + buffer.moveToOffset(cpLen(buffer.text)); + return; + } + + if (keyMatchers[Command.CLEAR_INPUT](key)) { + if (buffer.text.length > 0) buffer.setText(''); + return; + } + if (keyMatchers[Command.KILL_LINE_RIGHT](key)) { + buffer.killLineRight(); + return; + } + if (keyMatchers[Command.KILL_LINE_LEFT](key)) { + buffer.killLineLeft(); + return; + } + + if (keyMatchers[Command.OPEN_EXTERNAL_EDITOR](key)) { + buffer.openInExternalEditor(); + return; + } + + buffer.handleInput(key); + }, + { isActive }, + ); + + if (!buffer) return null; + + const linesToRender = buffer.viewportVisualLines; + const [cursorVisualRowAbsolute, cursorVisualColAbsolute] = + buffer.visualCursor; + const scrollVisualRow = buffer.visualScrollRow; + + return ( + + + {'> '} + + {buffer.text.length === 0 && placeholder ? ( + + {chalk.inverse(placeholder.slice(0, 1))} + {placeholder.slice(1)} + + ) : ( + linesToRender.map((lineText, visualIdxInRenderedSet) => { + const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow; + let display = cpSlice(lineText, 0, inputWidth); + const currentVisualWidth = stringWidth(display); + if (currentVisualWidth < inputWidth) { + display = display + ' '.repeat(inputWidth - currentVisualWidth); + } + + if (visualIdxInRenderedSet === cursorVisualRow) { + const relativeVisualColForHighlight = cursorVisualColAbsolute; + if (relativeVisualColForHighlight >= 0) { + if (relativeVisualColForHighlight < cpLen(display)) { + const charToHighlight = + cpSlice( + display, + relativeVisualColForHighlight, + relativeVisualColForHighlight + 1, + ) || ' '; + const highlighted = chalk.inverse(charToHighlight); + display = + cpSlice(display, 0, relativeVisualColForHighlight) + + highlighted + + cpSlice(display, relativeVisualColForHighlight + 1); + } else if ( + relativeVisualColForHighlight === cpLen(display) && + cpLen(display) === inputWidth + ) { + display = display + chalk.inverse(' '); + } + } + } + return ( + {display} + ); + }) + )} + + + + {validationErrors.length > 0 && ( + + {validationErrors.map((error, index) => ( + + ⚠ {error} + + ))} + + )} + + ); +} diff --git a/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx b/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx index a9446da9..6756856b 100644 --- a/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx +++ b/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx @@ -15,9 +15,11 @@ 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'; interface AgentCreationWizardProps { onClose: () => void; @@ -49,11 +51,9 @@ export function AgentCreationWizard({ // Centralized ESC key handling for the entire wizard useInput((input, key) => { if (key.escape) { - // Step 3 (DescriptionInput) handles its own ESC logic when generating - if ( - state.currentStep === WIZARD_STEPS.DESCRIPTION_INPUT && - state.isGenerating - ) { + // 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 } @@ -81,19 +81,27 @@ export function AgentCreationWizard({ const renderStepHeader = useCallback(() => { const getStepHeaderText = () => { - switch (state.currentStep) { - case WIZARD_STEPS.LOCATION_SELECTION: - return 'Step 1: Choose Location'; - case WIZARD_STEPS.GENERATION_METHOD: - return 'Step 2: Choose Generation Method'; - case WIZARD_STEPS.DESCRIPTION_INPUT: - return 'Step 3: Describe Your Subagent'; - case WIZARD_STEPS.TOOL_SELECTION: - return 'Step 4: Select Tools'; - case WIZARD_STEPS.COLOR_SELECTION: - return 'Step 5: Choose Background Color'; - case WIZARD_STEPS.FINAL_CONFIRMATION: - return 'Step 6: Confirm and Save'; + 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'; } @@ -104,7 +112,7 @@ export function AgentCreationWizard({ {getStepHeaderText()} ); - }, [state.currentStep]); + }, [state.currentStep, state.generationMethod]); const renderDebugContent = useCallback(() => { if (process.env['NODE_ENV'] !== 'development') { @@ -146,28 +154,22 @@ export function AgentCreationWizard({ const renderStepFooter = useCallback(() => { const getNavigationInstructions = () => { // Special case: During generation in description input step, only show cancel option - if ( - state.currentStep === WIZARD_STEPS.DESCRIPTION_INPUT && - state.isGenerating - ) { + const kind = getStepKind(state.generationMethod, state.currentStep); + if (kind === 'LLM_DESC' && state.isGenerating) { return 'Esc to cancel'; } - if (state.currentStep === WIZARD_STEPS.FINAL_CONFIRMATION) { + 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 stepsWithNavigation = [ - WIZARD_STEPS.LOCATION_SELECTION, - WIZARD_STEPS.GENERATION_METHOD, - WIZARD_STEPS.TOOL_SELECTION, - WIZARD_STEPS.COLOR_SELECTION, - ] as const; - - const hasNavigation = (stepsWithNavigation as readonly number[]).includes( - state.currentStep, - ); + 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 = @@ -183,17 +185,79 @@ export function AgentCreationWizard({ {getNavigationInstructions()} ); - }, [state.currentStep, state.isGenerating]); + }, [state.currentStep, state.isGenerating, state.generationMethod]); const renderStepContent = useCallback(() => { - switch (state.currentStep) { - case WIZARD_STEPS.LOCATION_SELECTION: + const kind = getStepKind(state.generationMethod, state.currentStep); + switch (kind) { + case 'LOCATION': return ; - case WIZARD_STEPS.GENERATION_METHOD: + case 'GEN_METHOD': return ; - case WIZARD_STEPS.DESCRIPTION_INPUT: + case 'LLM_DESC': return ; - case WIZARD_STEPS.TOOL_SELECTION: + 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 ( ); - case WIZARD_STEPS.COLOR_SELECTION: + case 'COLOR': return ( ); - case WIZARD_STEPS.FINAL_CONFIRMATION: + case 'FINAL': return ; default: return ( @@ -226,16 +290,7 @@ export function AgentCreationWizard({ ); } - }, [ - stepProps, - state.currentStep, - state.selectedTools, - state.color, - state.generatedName, - config, - handleNext, - dispatch, - ]); + }, [stepProps, state, config, handleNext, dispatch]); return ( diff --git a/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx b/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx index a4387397..24342cf8 100644 --- a/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx +++ b/packages/cli/src/ui/components/subagents/create/DescriptionInput.tsx @@ -4,20 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useState, useCallback, useRef } from 'react'; +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 { useTextBuffer } from '../../shared/text-buffer.js'; import { useKeypress, Key } from '../../../hooks/useKeypress.js'; import { keyMatchers, Command } from '../../../keyMatchers.js'; import { theme } from '../../../semantic-colors.js'; -import { cpSlice, cpLen } from '../../../utils/textUtils.js'; -import chalk from 'chalk'; -import stringWidth from 'string-width'; import { Colors } from '../../../colors.js'; +import { TextInput } from '../../shared/TextInput.js'; /** * Step 3: Description input with LLM generation. @@ -28,7 +25,6 @@ export function DescriptionInput({ onNext, config, }: WizardStepProps) { - const [inputWidth] = useState(80); // Fixed width for now const abortControllerRef = useRef(null); const handleTextChange = useCallback( @@ -42,12 +38,7 @@ export function DescriptionInput({ [dispatch], ); - const buffer = useTextBuffer({ - initialText: state.userDescription || '', - viewport: { height: 10, width: inputWidth }, - isValidPath: () => false, // For subagent description, we don't need file path validation - onChange: handleTextChange, - }); + // TextInput will manage its own buffer; we just pass value and handlers const handleGenerate = useCallback( async ( @@ -83,11 +74,11 @@ export function DescriptionInput({ ); const handleSubmit = useCallback(async () => { - if (!state.canProceed || state.isGenerating || !buffer) { + if (!state.canProceed || state.isGenerating) { return; } - const inputValue = buffer.text.trim(); + const inputValue = state.userDescription.trim(); if (!inputValue) { return; } @@ -120,7 +111,7 @@ export function DescriptionInput({ }, [ state.canProceed, state.isGenerating, - buffer, + state.userDescription, dispatch, config, handleGenerate, @@ -140,92 +131,11 @@ export function DescriptionInput({ [dispatch], ); - // Handle keyboard input for text editing - const handleInput = useCallback( - (key: Key) => { - if (!buffer) { - return; - } - - if (keyMatchers[Command.SUBMIT](key)) { - if (buffer.text.trim()) { - const [row, col] = buffer.cursor; - const line = buffer.lines[row]; - const charBefore = col > 0 ? cpSlice(line, col - 1, col) : ''; - if (charBefore === '\\') { - buffer.backspace(); - buffer.newline(); - } else { - handleSubmit(); - } - } - return; - } - - // Newline insertion - if (keyMatchers[Command.NEWLINE](key)) { - buffer.newline(); - return; - } - - // Ctrl+A (Home) / Ctrl+E (End) - if (keyMatchers[Command.HOME](key)) { - buffer.move('home'); - return; - } - if (keyMatchers[Command.END](key)) { - buffer.move('end'); - buffer.moveToOffset(cpLen(buffer.text)); - return; - } - - // Ctrl+C (Clear input) - if (keyMatchers[Command.CLEAR_INPUT](key)) { - if (buffer.text.length > 0) { - buffer.setText(''); - } - return; - } - - // Kill line commands - if (keyMatchers[Command.KILL_LINE_RIGHT](key)) { - buffer.killLineRight(); - return; - } - if (keyMatchers[Command.KILL_LINE_LEFT](key)) { - buffer.killLineLeft(); - return; - } - - // External editor - if (keyMatchers[Command.OPEN_EXTERNAL_EDITOR](key)) { - buffer.openInExternalEditor(); - return; - } - - // Fall back to the text buffer's default input handling for all other keys - buffer.handleInput(key); - }, - [buffer, handleSubmit], - ); - // Use separate keypress handlers for different states useKeypress(handleGenerationKeypress, { isActive: state.isGenerating, }); - useKeypress(handleInput, { - isActive: !state.isGenerating, - }); - - if (!buffer) { - return null; - } - - const linesToRender = buffer.viewportVisualLines; - const [cursorVisualRowAbsolute, cursorVisualColAbsolute] = - buffer.visualCursor; - const scrollVisualRow = buffer.visualScrollRow; const placeholder = 'e.g., Expert code reviewer that reviews code based on best practices...'; @@ -248,71 +158,15 @@ export function DescriptionInput({ ) : ( - <> - - {'> '} - - {buffer.text.length === 0 && placeholder ? ( - - {chalk.inverse(placeholder.slice(0, 1))} - {placeholder.slice(1)} - - ) : ( - linesToRender.map((lineText, visualIdxInRenderedSet) => { - const cursorVisualRow = - cursorVisualRowAbsolute - scrollVisualRow; - let display = cpSlice(lineText, 0, inputWidth); - const currentVisualWidth = stringWidth(display); - if (currentVisualWidth < inputWidth) { - display = - display + ' '.repeat(inputWidth - currentVisualWidth); - } - - if (visualIdxInRenderedSet === cursorVisualRow) { - const relativeVisualColForHighlight = - cursorVisualColAbsolute; - - if (relativeVisualColForHighlight >= 0) { - if (relativeVisualColForHighlight < cpLen(display)) { - const charToHighlight = - cpSlice( - display, - relativeVisualColForHighlight, - relativeVisualColForHighlight + 1, - ) || ' '; - const highlighted = chalk.inverse(charToHighlight); - display = - cpSlice(display, 0, relativeVisualColForHighlight) + - highlighted + - cpSlice(display, relativeVisualColForHighlight + 1); - } else if ( - relativeVisualColForHighlight === cpLen(display) && - cpLen(display) === inputWidth - ) { - display = display + chalk.inverse(' '); - } - } - } - return ( - - {display} - - ); - }) - )} - - - - {state.validationErrors.length > 0 && ( - - {state.validationErrors.map((error, index) => ( - - ⚠ {error} - - ))} - - )} - + )} ); diff --git a/packages/cli/src/ui/components/subagents/create/TextEntryStep.tsx b/packages/cli/src/ui/components/subagents/create/TextEntryStep.tsx new file mode 100644 index 00000000..35f3aca7 --- /dev/null +++ b/packages/cli/src/ui/components/subagents/create/TextEntryStep.tsx @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback } from 'react'; +import { Box, Text } from 'ink'; +import { WizardStepProps } from '../types.js'; +import { Colors } from '../../../colors.js'; +import { TextInput } from '../../shared/TextInput.js'; + +interface TextEntryStepProps + extends Pick { + description: string; + placeholder?: string; + /** + * Visual height of the input viewport in rows. Name entry can be 1, others can be larger. + */ + height?: number; + /** Initial text value when the step loads */ + initialText?: string; + /** + * Called on every text change to update state. + */ + onChange: (text: string) => void; + /** + * Optional validation. Return error message when invalid. + */ + validate?: (text: string) => string | null; +} + +export function TextEntryStep({ + state, + dispatch, + onNext, + description, + placeholder, + height = 1, + initialText = '', + onChange, + validate, +}: TextEntryStepProps) { + const submit = useCallback(() => { + const value = initialText ? initialText.trim() : ''; + const error = validate + ? validate(value) + : value.length === 0 + ? 'Please enter a value.' + : null; + if (error) { + dispatch({ type: 'SET_VALIDATION_ERRORS', errors: [error] }); + return; + } + dispatch({ type: 'SET_VALIDATION_ERRORS', errors: [] }); + onNext(); + }, [dispatch, onNext, validate, initialText]); + + return ( + + {description && ( + + {description} + + )} + + + + ); +} diff --git a/packages/cli/src/ui/components/subagents/index.ts b/packages/cli/src/ui/components/subagents/index.ts index feb51675..8f22a244 100644 --- a/packages/cli/src/ui/components/subagents/index.ts +++ b/packages/cli/src/ui/components/subagents/index.ts @@ -8,7 +8,7 @@ export { AgentCreationWizard } from './create/AgentCreationWizard.js'; // Management Dialog -export { AgentsManagerDialog } from './view/AgentsManagerDialog.js'; +export { AgentsManagerDialog } from './manage/AgentsManagerDialog.js'; // Execution Display export { AgentExecutionDisplay } from './runtime/AgentExecutionDisplay.js'; diff --git a/packages/cli/src/ui/components/subagents/reducers.tsx b/packages/cli/src/ui/components/subagents/reducers.tsx index ebd96ffc..2882b354 100644 --- a/packages/cli/src/ui/components/subagents/reducers.tsx +++ b/packages/cli/src/ui/components/subagents/reducers.tsx @@ -5,7 +5,8 @@ */ import { CreationWizardState, WizardAction } from './types.js'; -import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js'; +import { WIZARD_STEPS } from './constants.js'; +import { getStepKind, getTotalSteps } from './utils.js'; /** * Initial state for the creation wizard. @@ -38,7 +39,7 @@ export function wizardReducer( ...state, currentStep: Math.max( WIZARD_STEPS.LOCATION_SELECTION, - Math.min(TOTAL_WIZARD_STEPS, action.step), + Math.min(getTotalSteps(state.generationMethod), action.step), ), validationErrors: [], }; @@ -74,6 +75,27 @@ export function wizardReducer( canProceed: true, }; + case 'SET_GENERATED_NAME': + return { + ...state, + generatedName: action.name, + canProceed: action.name.trim().length > 0, + }; + + case 'SET_GENERATED_SYSTEM_PROMPT': + return { + ...state, + generatedSystemPrompt: action.systemPrompt, + canProceed: action.systemPrompt.trim().length > 0, + }; + + case 'SET_GENERATED_DESCRIPTION': + return { + ...state, + generatedDescription: action.description, + canProceed: action.description.trim().length > 0, + }; + case 'SET_TOOLS': return { ...state, @@ -103,7 +125,10 @@ export function wizardReducer( }; case 'GO_TO_NEXT_STEP': - if (state.canProceed && state.currentStep < TOTAL_WIZARD_STEPS) { + if ( + state.canProceed && + state.currentStep < getTotalSteps(state.generationMethod) + ) { return { ...state, currentStep: state.currentStep + 1, @@ -136,29 +161,29 @@ export function wizardReducer( * Validates whether a step can proceed based on current state. */ function validateStep(step: number, state: CreationWizardState): boolean { - switch (step) { - case WIZARD_STEPS.LOCATION_SELECTION: // Location selection - return true; // Always can proceed from location selection - - case WIZARD_STEPS.GENERATION_METHOD: // Generation method - return true; // Always can proceed from method selection - - case WIZARD_STEPS.DESCRIPTION_INPUT: // Description input + const kind = getStepKind(state.generationMethod, step); + switch (kind) { + case 'LOCATION': + case 'GEN_METHOD': + return true; + case 'LLM_DESC': return state.userDescription.trim().length >= 0; - - case WIZARD_STEPS.TOOL_SELECTION: // Tool selection + case 'MANUAL_NAME': + return state.generatedName.trim().length > 0; + case 'MANUAL_PROMPT': + return state.generatedSystemPrompt.trim().length > 0; + case 'MANUAL_DESC': + return state.generatedDescription.trim().length > 0; + case 'TOOLS': return ( state.generatedName.length > 0 && state.generatedDescription.length > 0 && state.generatedSystemPrompt.length > 0 ); - - case WIZARD_STEPS.COLOR_SELECTION: // Color selection - return true; // Always can proceed from tool selection - - case WIZARD_STEPS.FINAL_CONFIRMATION: // Final confirmation + case 'COLOR': + return true; + case 'FINAL': return state.color.length > 0; - default: return false; } diff --git a/packages/cli/src/ui/components/subagents/types.ts b/packages/cli/src/ui/components/subagents/types.ts index edad9656..7aaa101f 100644 --- a/packages/cli/src/ui/components/subagents/types.ts +++ b/packages/cli/src/ui/components/subagents/types.ts @@ -73,6 +73,9 @@ export type WizardAction = | { type: 'SET_LOCATION'; location: SubagentLevel } | { type: 'SET_GENERATION_METHOD'; method: 'qwen' | 'manual' } | { type: 'SET_USER_DESCRIPTION'; description: string } + | { type: 'SET_GENERATED_NAME'; name: string } + | { type: 'SET_GENERATED_SYSTEM_PROMPT'; systemPrompt: string } + | { type: 'SET_GENERATED_DESCRIPTION'; description: string } | { type: 'SET_GENERATED_CONTENT'; name: string; diff --git a/packages/cli/src/ui/components/subagents/utils.ts b/packages/cli/src/ui/components/subagents/utils.ts index 584a0ebc..73011662 100644 --- a/packages/cli/src/ui/components/subagents/utils.ts +++ b/packages/cli/src/ui/components/subagents/utils.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { COLOR_OPTIONS } from './constants.js'; +import { COLOR_OPTIONS, TOTAL_WIZARD_STEPS } from './constants.js'; export const shouldShowColor = (color?: string): boolean => color !== undefined && color !== 'auto'; @@ -39,3 +39,64 @@ export function fmtDuration(ms: number): string { const m = Math.floor((ms % 3600000) / 60000); return `${h}h ${m}m`; } + +// Dynamic step flow helpers (support manual and guided flows) +export type StepKind = + | 'LOCATION' + | 'GEN_METHOD' + | 'LLM_DESC' + | 'MANUAL_NAME' + | 'MANUAL_PROMPT' + | 'MANUAL_DESC' + | 'TOOLS' + | 'COLOR' + | 'FINAL'; + +export function getTotalSteps(method: 'qwen' | 'manual'): number { + return method === 'manual' ? 8 : TOTAL_WIZARD_STEPS; +} + +export function getStepKind( + method: 'qwen' | 'manual', + stepNumber: number, +): StepKind { + if (method === 'manual') { + switch (stepNumber) { + case 1: + return 'LOCATION'; + case 2: + return 'GEN_METHOD'; + case 3: + return 'MANUAL_NAME'; + case 4: + return 'MANUAL_PROMPT'; + case 5: + return 'MANUAL_DESC'; + case 6: + return 'TOOLS'; + case 7: + return 'COLOR'; + case 8: + return 'FINAL'; + default: + return 'FINAL'; + } + } + + switch (stepNumber) { + case 1: + return 'LOCATION'; + case 2: + return 'GEN_METHOD'; + case 3: + return 'LLM_DESC'; + case 4: + return 'TOOLS'; + case 5: + return 'COLOR'; + case 6: + return 'FINAL'; + default: + return 'FINAL'; + } +}