mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent feature - add manual creation subagent steps
This commit is contained in:
194
packages/cli/src/ui/components/shared/TextInput.tsx
Normal file
194
packages/cli/src/ui/components/shared/TextInput.tsx
Normal file
@@ -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 (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<Text color={theme.text.accent}>{'> '}</Text>
|
||||
<Box flexGrow={1} flexDirection="column">
|
||||
{buffer.text.length === 0 && placeholder ? (
|
||||
<Text>
|
||||
{chalk.inverse(placeholder.slice(0, 1))}
|
||||
<Text color={Colors.Gray}>{placeholder.slice(1)}</Text>
|
||||
</Text>
|
||||
) : (
|
||||
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 (
|
||||
<Text key={`line-${visualIdxInRenderedSet}`}>{display}</Text>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{validationErrors.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
{validationErrors.map((error, index) => (
|
||||
<Text key={index} color={theme.status.error}>
|
||||
⚠ {error}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
<Text bold>{getStepHeaderText()}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [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({
|
||||
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [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 <LocationSelector {...stepProps} />;
|
||||
case WIZARD_STEPS.GENERATION_METHOD:
|
||||
case 'GEN_METHOD':
|
||||
return <GenerationMethodSelector {...stepProps} />;
|
||||
case WIZARD_STEPS.DESCRIPTION_INPUT:
|
||||
case 'LLM_DESC':
|
||||
return <DescriptionInput {...stepProps} />;
|
||||
case WIZARD_STEPS.TOOL_SELECTION:
|
||||
case 'MANUAL_NAME':
|
||||
return (
|
||||
<TextEntryStep
|
||||
key="manual-name"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onNext={handleNext}
|
||||
description="Enter a clear, unique name for this subagent."
|
||||
placeholder="e.g., Code Reviewer"
|
||||
height={1}
|
||||
initialText={state.generatedName}
|
||||
onChange={(t) => {
|
||||
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 (
|
||||
<TextEntryStep
|
||||
key="manual-prompt"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onNext={handleNext}
|
||||
description="Write the system prompt that defines this subagent's behavior. Be comprehensive for best results."
|
||||
placeholder="e.g., You are an expert code reviewer..."
|
||||
height={10}
|
||||
initialText={state.generatedSystemPrompt}
|
||||
onChange={(t) => {
|
||||
dispatch({
|
||||
type: 'SET_GENERATED_SYSTEM_PROMPT',
|
||||
systemPrompt: t,
|
||||
});
|
||||
}}
|
||||
validate={(t) =>
|
||||
t.trim().length === 0 ? 'System prompt cannot be empty.' : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'MANUAL_DESC':
|
||||
return (
|
||||
<TextEntryStep
|
||||
key="manual-desc"
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onNext={handleNext}
|
||||
description="Describe when and how this subagent should be used."
|
||||
placeholder="e.g., Reviews code for best practices and potential bugs."
|
||||
height={6}
|
||||
initialText={state.generatedDescription}
|
||||
onChange={(t) => {
|
||||
dispatch({ type: 'SET_GENERATED_DESCRIPTION', description: t });
|
||||
}}
|
||||
validate={(t) =>
|
||||
t.trim().length === 0 ? 'Description cannot be empty.' : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'TOOLS':
|
||||
return (
|
||||
<ToolSelector
|
||||
tools={state.selectedTools}
|
||||
@@ -204,7 +268,7 @@ export function AgentCreationWizard({
|
||||
config={config}
|
||||
/>
|
||||
);
|
||||
case WIZARD_STEPS.COLOR_SELECTION:
|
||||
case 'COLOR':
|
||||
return (
|
||||
<ColorSelector
|
||||
color={state.color}
|
||||
@@ -215,7 +279,7 @@ export function AgentCreationWizard({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case WIZARD_STEPS.FINAL_CONFIRMATION:
|
||||
case 'FINAL':
|
||||
return <CreationSummary {...stepProps} />;
|
||||
default:
|
||||
return (
|
||||
@@ -226,16 +290,7 @@ export function AgentCreationWizard({
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}, [
|
||||
stepProps,
|
||||
state.currentStep,
|
||||
state.selectedTools,
|
||||
state.color,
|
||||
state.generatedName,
|
||||
config,
|
||||
handleNext,
|
||||
dispatch,
|
||||
]);
|
||||
}, [stepProps, state, config, handleNext, dispatch]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
||||
@@ -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<AbortController | null>(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({
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box>
|
||||
<Text color={theme.text.accent}>{'> '}</Text>
|
||||
<Box flexGrow={1} flexDirection="column">
|
||||
{buffer.text.length === 0 && placeholder ? (
|
||||
<Text>
|
||||
{chalk.inverse(placeholder.slice(0, 1))}
|
||||
<Text color={Colors.Gray}>{placeholder.slice(1)}</Text>
|
||||
</Text>
|
||||
) : (
|
||||
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 (
|
||||
<Text key={`line-${visualIdxInRenderedSet}`}>
|
||||
{display}
|
||||
</Text>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{state.validationErrors.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
{state.validationErrors.map((error, index) => (
|
||||
<Text key={index} color={theme.status.error}>
|
||||
⚠ {error}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
<TextInput
|
||||
value={state.userDescription || ''}
|
||||
onChange={handleTextChange}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder={placeholder}
|
||||
height={10}
|
||||
isActive={!state.isGenerating}
|
||||
validationErrors={state.validationErrors}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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<WizardStepProps, 'dispatch' | 'onNext' | 'state'> {
|
||||
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 (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{description && (
|
||||
<Box>
|
||||
<Text color={Colors.Gray}>{description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
value={initialText}
|
||||
onChange={onChange}
|
||||
onSubmit={submit}
|
||||
placeholder={placeholder}
|
||||
height={height}
|
||||
isActive={!state.isGenerating}
|
||||
validationErrors={state.validationErrors}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user