feat: subagent feature wip

This commit is contained in:
tanzhenxin
2025-09-10 13:41:28 +08:00
parent 549f296eb5
commit 6b09aee32b
30 changed files with 329 additions and 239 deletions

View File

@@ -44,7 +44,7 @@ import { FolderTrustDialog } from './components/FolderTrustDialog.js';
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
import {
SubagentCreationWizard,
AgentCreationWizard,
AgentsManagerDialog,
} from './components/subagents/index.js';
import { Colors } from './colors.js';
@@ -1093,7 +1093,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Box>
) : isSubagentCreateDialogOpen ? (
<Box flexDirection="column">
<SubagentCreationWizard
<AgentCreationWizard
onClose={closeSubagentCreateDialog}
config={config}
/>

View File

@@ -40,7 +40,7 @@ vi.mock('../../utils/MarkdownDisplay.js', () => ({
},
}));
vi.mock('../subagents/index.js', () => ({
SubagentExecutionDisplay: function MockSubagentExecutionDisplay({
AgentExecutionDisplay: function MockAgentExecutionDisplay({
data,
}: {
data: { subagentName: string; taskDescription: string };

View File

@@ -17,7 +17,7 @@ import {
TodoResultDisplay,
TaskResultDisplay,
} from '@qwen-code/qwen-code-core';
import { SubagentExecutionDisplay } from '../subagents/index.js';
import { AgentExecutionDisplay } from '../subagents/index.js';
const STATIC_HEIGHT = 1;
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
@@ -106,7 +106,7 @@ const SubagentExecutionRenderer: React.FC<{
data: TaskResultDisplay;
availableHeight?: number;
childWidth: number;
}> = ({ data }) => <SubagentExecutionDisplay data={data} />;
}> = ({ data }) => <AgentExecutionDisplay data={data} />;
/**
* Component to render string results (markdown or plain text)

View File

@@ -6,20 +6,20 @@
import { useReducer, useCallback, useMemo } from 'react';
import { Box, Text, useInput } from 'ink';
import { wizardReducer, initialWizardState } from './reducers.js';
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 { WizardStepProps } from '../types.js';
import { WIZARD_STEPS } from '../constants.js';
import { Config } from '@qwen-code/qwen-code-core';
import { Colors } from '../../colors.js';
import { theme } from '../../semantic-colors.js';
import { Colors } from '../../../colors.js';
import { theme } from '../../../semantic-colors.js';
interface SubagentCreationWizardProps {
interface AgentCreationWizardProps {
onClose: () => void;
config: Config | null;
}
@@ -27,10 +27,10 @@ interface SubagentCreationWizardProps {
/**
* Main orchestrator component for the subagent creation wizard.
*/
export function SubagentCreationWizard({
export function AgentCreationWizard({
onClose,
config,
}: SubagentCreationWizardProps) {
}: AgentCreationWizardProps) {
const [state, dispatch] = useReducer(wizardReducer, initialWizardState);
const handleNext = useCallback(() => {

View File

@@ -6,10 +6,10 @@
import { useState, useEffect } from 'react';
import { Box, Text } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { ColorOption } from './types.js';
import { Colors } from '../../colors.js';
import { COLOR_OPTIONS } from './constants.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { ColorOption } from '../types.js';
import { Colors } from '../../../colors.js';
import { COLOR_OPTIONS } from '../constants.js';
const colorOptions: ColorOption[] = COLOR_OPTIONS;

View File

@@ -6,11 +6,11 @@
import { useCallback, useState, useEffect } from 'react';
import { Box, Text, useInput } from 'ink';
import { WizardStepProps } from './types.js';
import { WizardStepProps } from '../types.js';
import { SubagentManager, SubagentConfig } from '@qwen-code/qwen-code-core';
import { theme } from '../../semantic-colors.js';
import { shouldShowColor, getColorForDisplay } from './utils.js';
import { useLaunchEditor } from './useLaunchEditor.js';
import { theme } from '../../../semantic-colors.js';
import { shouldShowColor, getColorForDisplay } from '../utils.js';
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
/**
* Step 6: Final confirmation and actions.

View File

@@ -6,17 +6,17 @@
import { useState, useCallback, useRef } from 'react';
import { Box, Text } from 'ink';
import { WizardStepProps, WizardAction } from './types.js';
import { sanitizeInput } from './utils.js';
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 { 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 { Colors } from '../../../colors.js';
/**
* Step 3: Description input with LLM generation.

View File

@@ -5,8 +5,8 @@
*/
import { Box } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { WizardStepProps } from './types.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { WizardStepProps } from '../types.js';
interface GenerationOption {
label: string;

View File

@@ -5,8 +5,8 @@
*/
import { Box } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { WizardStepProps } from './types.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { WizardStepProps } from '../types.js';
interface LocationOption {
label: string;

View File

@@ -6,10 +6,10 @@
import { useState, useMemo, useEffect } from 'react';
import { Box, Text } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { ToolCategory } from './types.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { ToolCategory } from '../types.js';
import { Kind, Config } from '@qwen-code/qwen-code-core';
import { Colors } from '../../colors.js';
import { Colors } from '../../../colors.js';
interface ToolOption {
label: string;

View File

@@ -4,33 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
// Creation Wizard Components
export { SubagentCreationWizard } from './SubagentCreationWizard.js';
export { LocationSelector } from './LocationSelector.js';
export { GenerationMethodSelector } from './GenerationMethodSelector.js';
export { DescriptionInput } from './DescriptionInput.js';
export { ToolSelector } from './ToolSelector.js';
export { ColorSelector } from './ColorSelector.js';
export { CreationSummary } from './CreationSummary.js';
// Creation Wizard
export { AgentCreationWizard } from './create/AgentCreationWizard.js';
// Management Dialog Components
export { AgentsManagerDialog } from './AgentsManagerDialog.js';
export { AgentSelectionStep } from './AgentSelectionStep.js';
export { ActionSelectionStep } from './ActionSelectionStep.js';
export { AgentViewerStep } from './AgentViewerStep.js';
export { AgentDeleteStep } from './AgentDeleteStep.js';
// Management Dialog
export { AgentsManagerDialog } from './view/AgentsManagerDialog.js';
// Execution Display Components
export { SubagentExecutionDisplay } from './SubagentExecutionDisplay.js';
// Creation Wizard Types and State
export type {
CreationWizardState,
WizardAction,
WizardStepProps,
WizardResult,
ToolCategory,
ColorOption,
} from './types.js';
export { wizardReducer, initialWizardState } from './reducers.js';
// Execution Display
export { AgentExecutionDisplay } from './runtime/AgentExecutionDisplay.js';

View File

@@ -6,19 +6,19 @@
import React, { useMemo } from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../../colors.js';
import { Colors } from '../../../colors.js';
import {
TaskResultDisplay,
SubagentStatsSummary,
} from '@qwen-code/qwen-code-core';
import { theme } from '../../semantic-colors.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import { COLOR_OPTIONS } from './constants.js';
import { fmtDuration } from './utils.js';
import { theme } from '../../../semantic-colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js';
import { COLOR_OPTIONS } from '../constants.js';
import { fmtDuration } from '../utils.js';
export type DisplayMode = 'compact' | 'default' | 'verbose';
export type DisplayMode = 'default' | 'verbose';
export interface SubagentExecutionDisplayProps {
export interface AgentExecutionDisplayProps {
data: TaskResultDisplay;
}
@@ -32,6 +32,8 @@ const getStatusColor = (
case 'completed':
case 'success':
return theme.status.success;
case 'cancelled':
return theme.status.warning;
case 'failed':
return theme.status.error;
default:
@@ -45,6 +47,8 @@ const getStatusText = (status: TaskResultDisplay['status']) => {
return 'Running';
case 'completed':
return 'Completed';
case 'cancelled':
return 'User Cancelled';
case 'failed':
return 'Failed';
default:
@@ -52,14 +56,17 @@ const getStatusText = (status: TaskResultDisplay['status']) => {
}
};
const MAX_TOOL_CALLS = 5;
const MAX_TASK_PROMPT_LINES = 5;
/**
* Component to display subagent execution progress and results.
* This is now a pure component that renders the provided SubagentExecutionResultDisplay data.
* Real-time updates are handled by the parent component updating the data prop.
*/
export const SubagentExecutionDisplay: React.FC<
SubagentExecutionDisplayProps
> = ({ data }) => {
export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
data,
}) => {
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('default');
const agentColor = useMemo(() => {
@@ -76,27 +83,25 @@ export const SubagentExecutionDisplay: React.FC<
if (displayMode === 'verbose') return 'Press ctrl+r to show less.';
if (displayMode === 'default') {
const hasMoreLines = data.taskPrompt.split('\n').length > 10;
const hasMoreToolCalls = data.toolCalls && data.toolCalls.length > 5;
const hasMoreLines =
data.taskPrompt.split('\n').length > MAX_TASK_PROMPT_LINES;
const hasMoreToolCalls =
data.toolCalls && data.toolCalls.length > MAX_TOOL_CALLS;
if (hasMoreToolCalls || hasMoreLines) {
return 'Press ctrl+s to show more.';
return 'Press ctrl+r to show more.';
}
return '';
}
return '';
}, [displayMode, data.toolCalls, data.taskPrompt, data.status]);
// Handle ctrl+s and ctrl+r keypresses to control display mode
// Handle ctrl+r keypresses to control display mode
useKeypress(
(key) => {
if (key.ctrl && key.name === 's') {
if (key.ctrl && key.name === 'r') {
setDisplayMode((current) =>
current === 'default' ? 'verbose' : 'verbose',
);
} else if (key.ctrl && key.name === 'r') {
setDisplayMode((current) =>
current === 'verbose' ? 'default' : 'default',
current === 'default' ? 'verbose' : 'default',
);
}
},
@@ -133,7 +138,9 @@ export const SubagentExecutionDisplay: React.FC<
)}
{/* Results section for completed/failed tasks */}
{(data.status === 'completed' || data.status === 'failed') && (
{(data.status === 'completed' ||
data.status === 'failed' ||
data.status === 'cancelled') && (
<ResultsSection data={data} displayMode={displayMode} />
)}
@@ -157,7 +164,7 @@ const TaskPromptSection: React.FC<{
const lines = taskPrompt.split('\n');
const shouldTruncate = lines.length > 10;
const showFull = displayMode === 'verbose';
const displayLines = showFull ? lines : lines.slice(0, 10);
const displayLines = showFull ? lines : lines.slice(0, MAX_TASK_PROMPT_LINES);
return (
<Box flexDirection="column" gap={1}>
@@ -206,9 +213,9 @@ const ToolCallsList: React.FC<{
displayMode: DisplayMode;
}> = ({ toolCalls, displayMode }) => {
const calls = toolCalls || [];
const shouldTruncate = calls.length > 5;
const shouldTruncate = calls.length > MAX_TOOL_CALLS;
const showAll = displayMode === 'verbose';
const displayCalls = showAll ? calls : calls.slice(-5); // Show last 5
const displayCalls = showAll ? calls : calls.slice(-MAX_TOOL_CALLS); // Show last 5
// Reverse the order to show most recent first
const reversedDisplayCalls = [...displayCalls].reverse();
@@ -220,7 +227,7 @@ const ToolCallsList: React.FC<{
{shouldTruncate && displayMode === 'default' && (
<Text color={Colors.Gray}>
{' '}
Showing the last 5 of {calls.length} tools.
Showing the last {MAX_TOOL_CALLS} of {calls.length} tools.
</Text>
)}
</Box>
@@ -390,16 +397,18 @@ const ResultsSection: React.FC<{
<ToolCallsList toolCalls={data.toolCalls} displayMode={displayMode} />
)}
{/* Execution Summary section */}
<Box flexDirection="column">
<Box flexDirection="row" marginBottom={1}>
<Text color={theme.text.primary}>Execution Summary:</Text>
{/* Execution Summary section - hide when cancelled */}
{data.status !== 'cancelled' && (
<Box flexDirection="column">
<Box flexDirection="row" marginBottom={1}>
<Text color={theme.text.primary}>Execution Summary:</Text>
</Box>
<ExecutionSummaryDetails data={data} displayMode={displayMode} />
</Box>
<ExecutionSummaryDetails data={data} displayMode={displayMode} />
</Box>
)}
{/* Tool Usage section */}
{data.executionSummary && (
{/* Tool Usage section - hide when cancelled */}
{data.status !== 'cancelled' && data.executionSummary && (
<Box flexDirection="column">
<Box flexDirection="row" marginBottom={1}>
<Text color={theme.text.primary}>Tool Usage:</Text>
@@ -409,11 +418,18 @@ const ResultsSection: React.FC<{
)}
{/* Error reason for failed tasks */}
{data.status === 'failed' && data.terminateReason && (
{data.status === 'cancelled' && (
<Box flexDirection="row">
<Text color={Colors.AccentRed}> Failed: </Text>
<Text color={Colors.Gray}>{data.terminateReason}</Text>
<Text color={theme.status.warning}> User Cancelled</Text>
</Box>
)}
{data.status === 'failed' &&
data.terminateReason &&
data.terminateReason !== 'CANCELLED' && (
<Box flexDirection="row">
<Text color={Colors.AccentRed}> Failed: </Text>
<Text color={Colors.Gray}>{data.terminateReason}</Text>
</Box>
)}
</Box>
);

View File

@@ -6,8 +6,8 @@
import { useState } from 'react';
import { Box } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { MANAGEMENT_STEPS } from './types.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { MANAGEMENT_STEPS } from '../types.js';
import { SubagentConfig } from '@qwen-code/qwen-code-core';
interface ActionSelectionStepProps {

View File

@@ -6,9 +6,9 @@
import { Box, Text } from 'ink';
import { SubagentConfig } from '@qwen-code/qwen-code-core';
import { StepNavigationProps } from './types.js';
import { theme } from '../../semantic-colors.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import { StepNavigationProps } from '../types.js';
import { theme } from '../../../semantic-colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js';
interface AgentDeleteStepProps extends StepNavigationProps {
selectedAgent: SubagentConfig | null;

View File

@@ -6,10 +6,10 @@
import { useState, useCallback } from 'react';
import { Box, Text } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { MANAGEMENT_STEPS } from './types.js';
import { theme } from '../../semantic-colors.js';
import { useLaunchEditor } from './useLaunchEditor.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { MANAGEMENT_STEPS } from '../types.js';
import { theme } from '../../../semantic-colors.js';
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
import { SubagentConfig } from '@qwen-code/qwen-code-core';
interface EditOption {

View File

@@ -6,9 +6,9 @@
import { useState, useEffect, useMemo } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import { Colors } from '../../colors.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import { theme } from '../../../semantic-colors.js';
import { Colors } from '../../../colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js';
import { SubagentConfig } from '@qwen-code/qwen-code-core';
interface NavigationState {

View File

@@ -5,8 +5,8 @@
*/
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import { shouldShowColor, getColorForDisplay } from './utils.js';
import { theme } from '../../../semantic-colors.js';
import { shouldShowColor, getColorForDisplay } from '../utils.js';
import { SubagentConfig } from '@qwen-code/qwen-code-core';
interface AgentViewerStepProps {

View File

@@ -11,11 +11,11 @@ import { ActionSelectionStep } from './ActionSelectionStep.js';
import { AgentViewerStep } from './AgentViewerStep.js';
import { EditOptionsStep } from './AgentEditStep.js';
import { AgentDeleteStep } from './AgentDeleteStep.js';
import { ToolSelector } from './ToolSelector.js';
import { ColorSelector } from './ColorSelector.js';
import { MANAGEMENT_STEPS } from './types.js';
import { Colors } from '../../colors.js';
import { theme } from '../../semantic-colors.js';
import { ToolSelector } from '../create/ToolSelector.js';
import { ColorSelector } from '../create/ColorSelector.js';
import { MANAGEMENT_STEPS } from '../types.js';
import { Colors } from '../../../colors.js';
import { theme } from '../../../semantic-colors.js';
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
interface AgentsManagerDialogProps {

View File

@@ -8,7 +8,7 @@ import { useCallback } from 'react';
import { useStdin } from 'ink';
import { EditorType } from '@qwen-code/qwen-code-core';
import { spawnSync } from 'child_process';
import { useSettings } from '../../contexts/SettingsContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
/**
* Determines the editor command to use based on user preferences and platform.