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';
+ }
+}