feat: subagent feature - add manual creation subagent steps

This commit is contained in:
tanzhenxin
2025-09-10 15:26:47 +08:00
parent 4839cb9320
commit 22dfefc9f1
8 changed files with 504 additions and 234 deletions

View File

@@ -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>
);