mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent list dialog - working
This commit is contained in:
@@ -25,6 +25,7 @@ import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
||||
import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js';
|
||||
import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js';
|
||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
|
||||
@@ -42,7 +43,10 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
||||
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
||||
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
||||
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
|
||||
import { SubagentCreationWizard } from './components/subagents/SubagentCreationWizard.js';
|
||||
import {
|
||||
SubagentCreationWizard,
|
||||
AgentsManagerDialog,
|
||||
} from './components/subagents/index.js';
|
||||
import { Colors } from './colors.js';
|
||||
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
||||
import { LoadedSettings, SettingScope } from '../config/settings.js';
|
||||
@@ -277,6 +281,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
closeSubagentCreateDialog,
|
||||
} = useSubagentCreateDialog();
|
||||
|
||||
const {
|
||||
isAgentsManagerDialogOpen,
|
||||
openAgentsManagerDialog,
|
||||
closeAgentsManagerDialog,
|
||||
} = useAgentsManagerDialog();
|
||||
|
||||
const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust(
|
||||
settings,
|
||||
setIsTrustedFolder,
|
||||
@@ -574,6 +584,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
openPrivacyNotice,
|
||||
openSettingsDialog,
|
||||
openSubagentCreateDialog,
|
||||
openAgentsManagerDialog,
|
||||
toggleVimEnabled,
|
||||
setIsProcessing,
|
||||
setGeminiMdFileCount,
|
||||
@@ -1087,6 +1098,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
config={config}
|
||||
/>
|
||||
</Box>
|
||||
) : isAgentsManagerDialogOpen ? (
|
||||
<Box flexDirection="column">
|
||||
<AgentsManagerDialog
|
||||
onClose={closeAgentsManagerDialog}
|
||||
config={config}
|
||||
/>
|
||||
</Box>
|
||||
) : isAuthenticating ? (
|
||||
<>
|
||||
{isQwenAuth && isQwenAuthenticating ? (
|
||||
|
||||
@@ -4,19 +4,22 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
CommandKind,
|
||||
SlashCommand,
|
||||
SlashCommandActionReturn,
|
||||
OpenDialogActionReturn,
|
||||
} from './types.js';
|
||||
import { CommandKind, SlashCommand, OpenDialogActionReturn } from './types.js';
|
||||
|
||||
export const agentsCommand: SlashCommand = {
|
||||
name: 'agents',
|
||||
description: 'Manage subagents for specialized task delegation.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'list',
|
||||
description: 'Manage existing subagents (view, edit, delete).',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (): OpenDialogActionReturn => ({
|
||||
type: 'dialog',
|
||||
dialog: 'subagent_list',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'create',
|
||||
description: 'Create a new subagent with guided setup.',
|
||||
@@ -26,94 +29,5 @@ export const agentsCommand: SlashCommand = {
|
||||
dialog: 'subagent_create',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
description: 'List all available subagents.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context): Promise<SlashCommandActionReturn | void> => {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Listing subagents... (not implemented yet)',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show',
|
||||
description: 'Show detailed information about a subagent.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context,
|
||||
args,
|
||||
): Promise<SlashCommandActionReturn | void> => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /agents show <subagent-name>',
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Showing details for subagent: ${args.trim()} (not implemented yet)`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
description: 'Edit an existing subagent configuration.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context,
|
||||
args,
|
||||
): Promise<SlashCommandActionReturn | void> => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /agents edit <subagent-name>',
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Editing subagent: ${args.trim()} (not implemented yet)`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
description: 'Delete a subagent configuration.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context,
|
||||
args,
|
||||
): Promise<SlashCommandActionReturn | void> => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /agents delete <subagent-name>',
|
||||
};
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Deleting subagent: ${args.trim()} (not implemented yet)`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -111,7 +111,8 @@ export interface OpenDialogActionReturn {
|
||||
| 'editor'
|
||||
| 'privacy'
|
||||
| 'settings'
|
||||
| 'subagent_create';
|
||||
| 'subagent_create'
|
||||
| 'subagent_list';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box } from 'ink';
|
||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { ManagementStepProps } from './types.js';
|
||||
|
||||
export const ActionSelectionStep = ({
|
||||
state,
|
||||
dispatch,
|
||||
onNext,
|
||||
onPrevious,
|
||||
}: ManagementStepProps) => {
|
||||
const actions = [
|
||||
{ label: 'View Agent', value: 'view' as const },
|
||||
{ label: 'Edit Agent', value: 'edit' as const },
|
||||
{ label: 'Delete Agent', value: 'delete' as const },
|
||||
{ label: 'Back', value: 'back' as const },
|
||||
];
|
||||
|
||||
const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => {
|
||||
if (value === 'back') {
|
||||
onPrevious();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'SELECT_ACTION', payload: value });
|
||||
onNext();
|
||||
};
|
||||
|
||||
const selectedIndex = state.selectedAction
|
||||
? actions.findIndex((action) => action.value === state.selectedAction)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<RadioButtonSelect
|
||||
items={actions}
|
||||
initialIndex={selectedIndex}
|
||||
onSelect={handleActionSelect}
|
||||
showNumbers={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
305
packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx
Normal file
305
packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ManagementStepProps } from './types.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
|
||||
interface NavigationState {
|
||||
currentBlock: 'project' | 'user';
|
||||
projectIndex: number;
|
||||
userIndex: number;
|
||||
}
|
||||
|
||||
export const AgentSelectionStep = ({
|
||||
state,
|
||||
dispatch,
|
||||
onNext,
|
||||
config,
|
||||
}: ManagementStepProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [navigation, setNavigation] = useState<NavigationState>({
|
||||
currentBlock: 'project',
|
||||
projectIndex: 0,
|
||||
userIndex: 0,
|
||||
});
|
||||
|
||||
// Group agents by level
|
||||
const projectAgents = state.availableAgents.filter(
|
||||
(agent) => agent.level === 'project',
|
||||
);
|
||||
const userAgents = state.availableAgents.filter(
|
||||
(agent) => agent.level === 'user',
|
||||
);
|
||||
const projectNames = new Set(projectAgents.map((agent) => agent.name));
|
||||
|
||||
useEffect(() => {
|
||||
const loadAgents = async () => {
|
||||
setIsLoading(true);
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
|
||||
try {
|
||||
if (!config) {
|
||||
throw new Error('Configuration not available');
|
||||
}
|
||||
const manager = config.getSubagentManager();
|
||||
|
||||
// Load agents from both levels separately to show all agents including conflicts
|
||||
const [projectAgents, userAgents] = await Promise.all([
|
||||
manager.listSubagents({ level: 'project' }),
|
||||
manager.listSubagents({ level: 'user' }),
|
||||
]);
|
||||
|
||||
// Combine all agents (project and user level)
|
||||
const allAgents = [...projectAgents, ...userAgents];
|
||||
|
||||
dispatch({ type: 'SET_AVAILABLE_AGENTS', payload: allAgents });
|
||||
dispatch({ type: 'SET_ERROR', payload: null });
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: `Failed to load agents: ${errorMessage}`,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
loadAgents();
|
||||
}, [dispatch, config]);
|
||||
|
||||
// Initialize navigation state when agents are loaded
|
||||
useEffect(() => {
|
||||
if (projectAgents.length > 0) {
|
||||
setNavigation((prev) => ({ ...prev, currentBlock: 'project' }));
|
||||
} else if (userAgents.length > 0) {
|
||||
setNavigation((prev) => ({ ...prev, currentBlock: 'user' }));
|
||||
}
|
||||
}, [projectAgents.length, userAgents.length]);
|
||||
|
||||
// Custom keyboard navigation
|
||||
useKeypress(
|
||||
(key) => {
|
||||
const { name } = key;
|
||||
|
||||
if (name === 'up' || name === 'k') {
|
||||
setNavigation((prev) => {
|
||||
if (prev.currentBlock === 'project') {
|
||||
if (prev.projectIndex > 0) {
|
||||
return { ...prev, projectIndex: prev.projectIndex - 1 };
|
||||
} else if (userAgents.length > 0) {
|
||||
// Move to last item in user block
|
||||
return {
|
||||
...prev,
|
||||
currentBlock: 'user',
|
||||
userIndex: userAgents.length - 1,
|
||||
};
|
||||
} else {
|
||||
// Wrap to last item in project block
|
||||
return { ...prev, projectIndex: projectAgents.length - 1 };
|
||||
}
|
||||
} else {
|
||||
if (prev.userIndex > 0) {
|
||||
return { ...prev, userIndex: prev.userIndex - 1 };
|
||||
} else if (projectAgents.length > 0) {
|
||||
// Move to last item in project block
|
||||
return {
|
||||
...prev,
|
||||
currentBlock: 'project',
|
||||
projectIndex: projectAgents.length - 1,
|
||||
};
|
||||
} else {
|
||||
// Wrap to last item in user block
|
||||
return { ...prev, userIndex: userAgents.length - 1 };
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (name === 'down' || name === 'j') {
|
||||
setNavigation((prev) => {
|
||||
if (prev.currentBlock === 'project') {
|
||||
if (prev.projectIndex < projectAgents.length - 1) {
|
||||
return { ...prev, projectIndex: prev.projectIndex + 1 };
|
||||
} else if (userAgents.length > 0) {
|
||||
// Move to first item in user block
|
||||
return { ...prev, currentBlock: 'user', userIndex: 0 };
|
||||
} else {
|
||||
// Wrap to first item in project block
|
||||
return { ...prev, projectIndex: 0 };
|
||||
}
|
||||
} else {
|
||||
if (prev.userIndex < userAgents.length - 1) {
|
||||
return { ...prev, userIndex: prev.userIndex + 1 };
|
||||
} else if (projectAgents.length > 0) {
|
||||
// Move to first item in project block
|
||||
return { ...prev, currentBlock: 'project', projectIndex: 0 };
|
||||
} else {
|
||||
// Wrap to first item in user block
|
||||
return { ...prev, userIndex: 0 };
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (name === 'return' || name === 'space') {
|
||||
// Select current item
|
||||
const currentAgent =
|
||||
navigation.currentBlock === 'project'
|
||||
? projectAgents[navigation.projectIndex]
|
||||
: userAgents[navigation.userIndex];
|
||||
|
||||
if (currentAgent) {
|
||||
const agentIndex = state.availableAgents.indexOf(currentAgent);
|
||||
handleAgentSelect(agentIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const handleAgentSelect = async (index: number) => {
|
||||
const selectedMetadata = state.availableAgents[index];
|
||||
if (!selectedMetadata) return;
|
||||
|
||||
try {
|
||||
if (!config) {
|
||||
throw new Error('Configuration not available');
|
||||
}
|
||||
const manager = config.getSubagentManager();
|
||||
const agent = await manager.loadSubagent(
|
||||
selectedMetadata.name,
|
||||
selectedMetadata.level,
|
||||
);
|
||||
|
||||
if (agent) {
|
||||
dispatch({ type: 'SELECT_AGENT', payload: { agent, index } });
|
||||
onNext();
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: `Failed to load agent: ${selectedMetadata.name}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: `Failed to load agent: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>Loading agents...</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>{state.error}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.availableAgents.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>No subagents found.</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Use '/agents create' to create your first subagent.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Render custom radio button items
|
||||
const renderAgentItem = (
|
||||
agent: { name: string; level: 'project' | 'user' },
|
||||
index: number,
|
||||
isSelected: boolean,
|
||||
) => {
|
||||
const textColor = isSelected ? theme.text.accent : theme.text.primary;
|
||||
|
||||
return (
|
||||
<Box key={agent.name} alignItems="center">
|
||||
<Box minWidth={2} flexShrink={0}>
|
||||
<Text color={isSelected ? theme.text.accent : theme.text.primary}>
|
||||
{isSelected ? '●' : ' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color={textColor} wrap="truncate">
|
||||
{agent.name}
|
||||
{agent.level === 'user' && projectNames.has(agent.name) && (
|
||||
<Text color={isSelected ? theme.status.warning : Colors.Gray}>
|
||||
{' '}
|
||||
(overridden by project level agent)
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Calculate enabled agents count (excluding conflicted user-level agents)
|
||||
const enabledAgentsCount =
|
||||
projectAgents.length +
|
||||
userAgents.filter((agent) => !projectNames.has(agent.name)).length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* Project Level Agents */}
|
||||
{projectAgents.length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.text.primary} bold>
|
||||
Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')})
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{projectAgents.map((agent, index) => {
|
||||
const isSelected =
|
||||
navigation.currentBlock === 'project' &&
|
||||
navigation.projectIndex === index;
|
||||
return renderAgentItem(agent, index, isSelected);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* User Level Agents */}
|
||||
{userAgents.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.primary} bold>
|
||||
User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')})
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{userAgents.map((agent, index) => {
|
||||
const isSelected =
|
||||
navigation.currentBlock === 'user' &&
|
||||
navigation.userIndex === index;
|
||||
return renderAgentItem(agent, index, isSelected);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Agent count summary */}
|
||||
{(projectAgents.length > 0 || userAgents.length > 0) && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
Using: {enabledAgentsCount} agents
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
79
packages/cli/src/ui/components/subagents/AgentViewerStep.tsx
Normal file
79
packages/cli/src/ui/components/subagents/AgentViewerStep.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { ManagementStepProps } from './types.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||
|
||||
export const AgentViewerStep = ({ state, onPrevious }: ManagementStepProps) => {
|
||||
// Handle keyboard input
|
||||
useInput((input, key) => {
|
||||
if (key.escape || input === 'b') {
|
||||
onPrevious();
|
||||
}
|
||||
});
|
||||
|
||||
if (!state.selectedAgent) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>No agent selected</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const agent = state.selectedAgent;
|
||||
|
||||
const toolsDisplay = agent.tools ? agent.tools.join(', ') : '*';
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text bold>Location: </Text>
|
||||
<Text>
|
||||
{agent.level === 'project'
|
||||
? 'Project Level (.qwen/agents/)'
|
||||
: 'User Level (~/.qwen/agents/)'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text bold>File Path: </Text>
|
||||
<Text>{agent.filePath}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text bold>Tools: </Text>
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(agent.backgroundColor) && (
|
||||
<Box>
|
||||
<Text bold>Color: </Text>
|
||||
<Box backgroundColor={getColorForDisplay(agent.backgroundColor)}>
|
||||
<Text color="black">{` ${agent.name} `}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text bold>Description:</Text>
|
||||
</Box>
|
||||
<Box padding={1} paddingBottom={0}>
|
||||
<Text wrap="wrap">{agent.description}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text bold>System Prompt:</Text>
|
||||
</Box>
|
||||
<Box padding={1} paddingBottom={0}>
|
||||
<Text wrap="wrap">{agent.systemPrompt}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
178
packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx
Normal file
178
packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useReducer, useCallback, useMemo } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { managementReducer, initialManagementState } from './reducers.js';
|
||||
import { AgentSelectionStep } from './AgentSelectionStep.js';
|
||||
import { ActionSelectionStep } from './ActionSelectionStep.js';
|
||||
import { AgentViewerStep } from './AgentViewerStep.js';
|
||||
import { ManagementStepProps, MANAGEMENT_STEPS } from './types.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { Config } from '@qwen-code/qwen-code-core';
|
||||
|
||||
interface AgentsManagerDialogProps {
|
||||
onClose: () => void;
|
||||
config: Config | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main orchestrator component for the agents management dialog.
|
||||
*/
|
||||
export function AgentsManagerDialog({
|
||||
onClose,
|
||||
config,
|
||||
}: AgentsManagerDialogProps) {
|
||||
const [state, dispatch] = useReducer(
|
||||
managementReducer,
|
||||
initialManagementState,
|
||||
);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
dispatch({ type: 'GO_TO_NEXT_STEP' });
|
||||
}, []);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
dispatch({ type: 'GO_TO_PREVIOUS_STEP' });
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dispatch({ type: 'RESET_DIALOG' });
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
// Centralized ESC key handling for the entire dialog
|
||||
useInput((input, key) => {
|
||||
if (key.escape) {
|
||||
// Agent viewer step handles its own ESC logic
|
||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
|
||||
return; // Let AgentViewerStep handle it
|
||||
}
|
||||
|
||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
// On first step, ESC cancels the entire dialog
|
||||
handleCancel();
|
||||
} else {
|
||||
// On other steps, ESC goes back to previous step
|
||||
handlePrevious();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const stepProps: ManagementStepProps = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
config,
|
||||
dispatch,
|
||||
onNext: handleNext,
|
||||
onPrevious: handlePrevious,
|
||||
onCancel: handleCancel,
|
||||
}),
|
||||
[state, dispatch, handleNext, handlePrevious, handleCancel, config],
|
||||
);
|
||||
|
||||
const renderStepHeader = useCallback(() => {
|
||||
const getStepHeaderText = () => {
|
||||
switch (state.currentStep) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return 'Agents';
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return 'Choose Action';
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return state.selectedAgent?.name;
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return `Editing: ${state.selectedAgent?.name || 'Unknown'}`;
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return `Delete: ${state.selectedAgent?.name || 'Unknown'}`;
|
||||
default:
|
||||
return 'Unknown Step';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text bold>{getStepHeaderText()}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [state.currentStep, state.selectedAgent?.name]);
|
||||
|
||||
const renderStepFooter = useCallback(() => {
|
||||
const getNavigationInstructions = () => {
|
||||
if (state.currentStep === MANAGEMENT_STEPS.ACTION_SELECTION) {
|
||||
return 'Enter to select, ↑↓ to navigate, Esc to go back';
|
||||
}
|
||||
|
||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
if (state.availableAgents.length === 0) {
|
||||
return 'Esc to close';
|
||||
}
|
||||
return 'Enter to select, ↑↓ to navigate, Esc to close';
|
||||
}
|
||||
|
||||
return 'Esc to go back';
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [state.currentStep, state.availableAgents.length]);
|
||||
|
||||
const renderStepContent = useCallback(() => {
|
||||
switch (state.currentStep) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return <AgentSelectionStep {...stepProps} />;
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return <ActionSelectionStep {...stepProps} />;
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return <AgentViewerStep {...stepProps} />;
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.warning}>
|
||||
Agent editing not yet implemented
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.warning}>
|
||||
Agent deletion not yet implemented
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>
|
||||
Invalid step: {state.currentStep}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}, [stepProps, state.currentStep]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* Main content wrapped in bounding box */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
gap={1}
|
||||
>
|
||||
{renderStepHeader()}
|
||||
{renderStepContent()}
|
||||
{renderStepFooter()}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -8,44 +8,9 @@ import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { WizardStepProps, ColorOption } from './types.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { COLOR_OPTIONS } from './constants.js';
|
||||
|
||||
const colorOptions: ColorOption[] = [
|
||||
{
|
||||
id: 'auto',
|
||||
name: 'Automatic Color',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
id: 'blue',
|
||||
name: 'Blue',
|
||||
value: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'green',
|
||||
name: 'Green',
|
||||
value: '#10b981',
|
||||
},
|
||||
{
|
||||
id: 'purple',
|
||||
name: 'Purple',
|
||||
value: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
id: 'orange',
|
||||
name: 'Orange',
|
||||
value: '#f59e0b',
|
||||
},
|
||||
{
|
||||
id: 'red',
|
||||
name: 'Red',
|
||||
value: '#ef4444',
|
||||
},
|
||||
{
|
||||
id: 'cyan',
|
||||
name: 'Cyan',
|
||||
value: '#06b6d4',
|
||||
},
|
||||
];
|
||||
const colorOptions: ColorOption[] = COLOR_OPTIONS;
|
||||
|
||||
/**
|
||||
* Step 5: Background color selection with preview.
|
||||
@@ -65,12 +30,12 @@ export function ColorSelector({
|
||||
(option) => option.id === selectedValue,
|
||||
);
|
||||
if (colorOption) {
|
||||
dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.value });
|
||||
dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.name });
|
||||
}
|
||||
};
|
||||
|
||||
const currentColor =
|
||||
colorOptions.find((option) => option.value === state.backgroundColor) ||
|
||||
colorOptions.find((option) => option.name === state.backgroundColor) ||
|
||||
colorOptions[0];
|
||||
|
||||
return (
|
||||
@@ -92,8 +57,8 @@ export function ColorSelector({
|
||||
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.Gray}>Preview:</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text color={currentColor.value}>{state.generatedName}</Text>
|
||||
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
||||
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||
|
||||
/**
|
||||
* Step 6: Final confirmation and actions.
|
||||
@@ -47,8 +48,7 @@ export function CreationSummary({
|
||||
|
||||
try {
|
||||
// Get project root from config
|
||||
const projectRoot = config.getProjectRoot();
|
||||
const subagentManager = new SubagentManager(projectRoot);
|
||||
const subagentManager = config.getSubagentManager();
|
||||
|
||||
// Check for name conflicts
|
||||
const isAvailable = await subagentManager.isNameAvailable(
|
||||
@@ -130,8 +130,7 @@ export function CreationSummary({
|
||||
if (!config) {
|
||||
throw new Error('Configuration not available');
|
||||
}
|
||||
const projectRoot = config.getProjectRoot();
|
||||
const subagentManager = new SubagentManager(projectRoot);
|
||||
const subagentManager = config.getSubagentManager();
|
||||
|
||||
// Build subagent configuration
|
||||
const subagentConfig: SubagentConfig = {
|
||||
@@ -143,6 +142,7 @@ export function CreationSummary({
|
||||
tools: Array.isArray(state.selectedTools)
|
||||
? state.selectedTools
|
||||
: undefined,
|
||||
backgroundColor: state.backgroundColor,
|
||||
};
|
||||
|
||||
// Create the subagent
|
||||
@@ -316,6 +316,15 @@ export function CreationSummary({
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(state.backgroundColor) && (
|
||||
<Box>
|
||||
<Text bold>Color: </Text>
|
||||
<Box backgroundColor={getColorForDisplay(state.backgroundColor)}>
|
||||
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text bold>Description:</Text>
|
||||
</Box>
|
||||
@@ -337,11 +346,11 @@ export function CreationSummary({
|
||||
|
||||
{saveError && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color="red">
|
||||
<Text bold color={theme.status.error}>
|
||||
❌ Error saving subagent:
|
||||
</Text>
|
||||
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||
<Text color="red" wrap="wrap">
|
||||
<Text color={theme.status.error} wrap="wrap">
|
||||
{saveError}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -302,7 +302,7 @@ export function DescriptionInput({
|
||||
{state.validationErrors.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
{state.validationErrors.map((error, index) => (
|
||||
<Text key={index} color="red">
|
||||
<Text key={index} color={theme.status.error}>
|
||||
⚠ {error}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
errorInfo?: React.ErrorInfo;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error boundary component for graceful error handling in the subagent wizard.
|
||||
*/
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
});
|
||||
|
||||
// Call optional error handler
|
||||
this.props.onError?.(error, errorInfo);
|
||||
|
||||
// Log error for debugging
|
||||
console.error(
|
||||
'SubagentWizard Error Boundary caught an error:',
|
||||
error,
|
||||
errorInfo,
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.hasError) {
|
||||
// Custom fallback UI or default error display
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<Text bold color="red">
|
||||
❌ Subagent Wizard Error
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text>
|
||||
An unexpected error occurred in the subagent creation wizard.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{this.state.error && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color="yellow">
|
||||
Error Details:
|
||||
</Text>
|
||||
<Text color="gray" wrap="wrap">
|
||||
{this.state.error.message}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray">
|
||||
Press <Text color="cyan">Esc</Text> to close the wizard and try
|
||||
again.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{process.env['NODE_ENV'] === 'development' &&
|
||||
this.state.errorInfo && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color="yellow">
|
||||
Stack Trace (Development):
|
||||
</Text>
|
||||
<Text color="gray" wrap="wrap">
|
||||
{this.state.errorInfo.componentStack}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,13 @@
|
||||
|
||||
import { useReducer, useCallback, useMemo } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { wizardReducer, initialWizardState } from './wizardReducer.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 { ErrorBoundary } from './ErrorBoundary.js';
|
||||
import { WizardStepProps } from './types.js';
|
||||
import { WIZARD_STEPS } from './constants.js';
|
||||
import { Config } from '@qwen-code/qwen-code-core';
|
||||
@@ -113,9 +112,9 @@ export function SubagentCreationWizard({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box borderStyle="single" borderColor="yellow" padding={1}>
|
||||
<Box borderStyle="single" borderColor={theme.status.warning} padding={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow" bold>
|
||||
<Text color={theme.status.warning} bold>
|
||||
Debug Info:
|
||||
</Text>
|
||||
<Text color={Colors.Gray}>Step: {state.currentStep}</Text>
|
||||
@@ -128,7 +127,9 @@ export function SubagentCreationWizard({
|
||||
<Text color={Colors.Gray}>Location: {state.location}</Text>
|
||||
<Text color={Colors.Gray}>Method: {state.generationMethod}</Text>
|
||||
{state.validationErrors.length > 0 && (
|
||||
<Text color="red">Errors: {state.validationErrors.join(', ')}</Text>
|
||||
<Text color={theme.status.error}>
|
||||
Errors: {state.validationErrors.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -201,35 +202,30 @@ export function SubagentCreationWizard({
|
||||
default:
|
||||
return (
|
||||
<Box>
|
||||
<Text color="red">Invalid step: {state.currentStep}</Text>
|
||||
<Text color={theme.status.error}>
|
||||
Invalid step: {state.currentStep}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}, [stepProps, state.currentStep]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
onError={(error, errorInfo) => {
|
||||
// Additional error handling if needed
|
||||
console.error('Subagent wizard error:', error, errorInfo);
|
||||
}}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{/* Main content wrapped in bounding box */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
gap={1}
|
||||
>
|
||||
{renderStepHeader()}
|
||||
{renderStepContent()}
|
||||
{renderDebugContent()}
|
||||
{renderStepFooter()}
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
{/* Main content wrapped in bounding box */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
gap={1}
|
||||
>
|
||||
{renderStepHeader()}
|
||||
{renderStepContent()}
|
||||
{renderDebugContent()}
|
||||
{renderStepFooter()}
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,3 +30,42 @@ export const STEP_NAMES: Record<number, string> = {
|
||||
[WIZARD_STEPS.COLOR_SELECTION]: 'Color Selection',
|
||||
[WIZARD_STEPS.FINAL_CONFIRMATION]: 'Final Confirmation',
|
||||
};
|
||||
|
||||
// Color options for subagent display
|
||||
export const COLOR_OPTIONS = [
|
||||
{
|
||||
id: 'auto',
|
||||
name: 'Automatic Color',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
id: 'blue',
|
||||
name: 'Blue',
|
||||
value: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'green',
|
||||
name: 'Green',
|
||||
value: '#10b981',
|
||||
},
|
||||
{
|
||||
id: 'purple',
|
||||
name: 'Purple',
|
||||
value: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
id: 'orange',
|
||||
name: 'Orange',
|
||||
value: '#f59e0b',
|
||||
},
|
||||
{
|
||||
id: 'red',
|
||||
name: 'Red',
|
||||
value: '#ef4444',
|
||||
},
|
||||
{
|
||||
id: 'cyan',
|
||||
name: 'Cyan',
|
||||
value: '#06b6d4',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Creation Wizard Components
|
||||
export { SubagentCreationWizard } from './SubagentCreationWizard.js';
|
||||
export { LocationSelector } from './LocationSelector.js';
|
||||
export { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
||||
@@ -12,6 +13,13 @@ export { ToolSelector } from './ToolSelector.js';
|
||||
export { ColorSelector } from './ColorSelector.js';
|
||||
export { CreationSummary } from './CreationSummary.js';
|
||||
|
||||
// Management Dialog Components
|
||||
export { AgentsManagerDialog } from './AgentsManagerDialog.js';
|
||||
export { AgentSelectionStep } from './AgentSelectionStep.js';
|
||||
export { ActionSelectionStep } from './ActionSelectionStep.js';
|
||||
export { AgentViewerStep } from './AgentViewerStep.js';
|
||||
|
||||
// Creation Wizard Types and State
|
||||
export type {
|
||||
CreationWizardState,
|
||||
WizardAction,
|
||||
@@ -21,4 +29,17 @@ export type {
|
||||
ColorOption,
|
||||
} from './types.js';
|
||||
|
||||
export { wizardReducer, initialWizardState } from './wizardReducer.js';
|
||||
export { wizardReducer, initialWizardState } from './reducers.js';
|
||||
|
||||
// Management Dialog Types and State
|
||||
export type {
|
||||
ManagementDialogState,
|
||||
ManagementAction,
|
||||
ManagementStepProps,
|
||||
} from './types.js';
|
||||
|
||||
export {
|
||||
managementReducer,
|
||||
initialManagementState,
|
||||
MANAGEMENT_STEPS,
|
||||
} from './reducers.js';
|
||||
|
||||
294
packages/cli/src/ui/components/subagents/reducers.tsx
Normal file
294
packages/cli/src/ui/components/subagents/reducers.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
CreationWizardState,
|
||||
WizardAction,
|
||||
ManagementDialogState,
|
||||
ManagementAction,
|
||||
MANAGEMENT_STEPS,
|
||||
} from './types.js';
|
||||
import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js';
|
||||
|
||||
export { MANAGEMENT_STEPS };
|
||||
|
||||
/**
|
||||
* Initial state for the creation wizard.
|
||||
*/
|
||||
export const initialWizardState: CreationWizardState = {
|
||||
currentStep: WIZARD_STEPS.LOCATION_SELECTION,
|
||||
location: 'project',
|
||||
generationMethod: 'qwen',
|
||||
userDescription: '',
|
||||
generatedSystemPrompt: '',
|
||||
generatedDescription: '',
|
||||
generatedName: '',
|
||||
selectedTools: 'all',
|
||||
backgroundColor: 'auto',
|
||||
isGenerating: false,
|
||||
validationErrors: [],
|
||||
canProceed: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for managing wizard state transitions.
|
||||
*/
|
||||
export function wizardReducer(
|
||||
state: CreationWizardState,
|
||||
action: WizardAction,
|
||||
): CreationWizardState {
|
||||
switch (action.type) {
|
||||
case 'SET_STEP':
|
||||
return {
|
||||
...state,
|
||||
currentStep: Math.max(
|
||||
WIZARD_STEPS.LOCATION_SELECTION,
|
||||
Math.min(TOTAL_WIZARD_STEPS, action.step),
|
||||
),
|
||||
validationErrors: [],
|
||||
};
|
||||
|
||||
case 'SET_LOCATION':
|
||||
return {
|
||||
...state,
|
||||
location: action.location,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SET_GENERATION_METHOD':
|
||||
return {
|
||||
...state,
|
||||
generationMethod: action.method,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SET_USER_DESCRIPTION':
|
||||
return {
|
||||
...state,
|
||||
userDescription: action.description,
|
||||
canProceed: action.description.trim().length >= 0,
|
||||
};
|
||||
|
||||
case 'SET_GENERATED_CONTENT':
|
||||
return {
|
||||
...state,
|
||||
generatedName: action.name,
|
||||
generatedDescription: action.description,
|
||||
generatedSystemPrompt: action.systemPrompt,
|
||||
isGenerating: false,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SET_TOOLS':
|
||||
return {
|
||||
...state,
|
||||
selectedTools: action.tools,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SET_BACKGROUND_COLOR':
|
||||
return {
|
||||
...state,
|
||||
backgroundColor: action.color,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SET_GENERATING':
|
||||
return {
|
||||
...state,
|
||||
isGenerating: action.isGenerating,
|
||||
canProceed: !action.isGenerating,
|
||||
};
|
||||
|
||||
case 'SET_VALIDATION_ERRORS':
|
||||
return {
|
||||
...state,
|
||||
validationErrors: action.errors,
|
||||
canProceed: action.errors.length === 0,
|
||||
};
|
||||
|
||||
case 'GO_TO_NEXT_STEP':
|
||||
if (state.canProceed && state.currentStep < TOTAL_WIZARD_STEPS) {
|
||||
return {
|
||||
...state,
|
||||
currentStep: state.currentStep + 1,
|
||||
validationErrors: [],
|
||||
canProceed: validateStep(state.currentStep + 1, state),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
||||
case 'GO_TO_PREVIOUS_STEP':
|
||||
if (state.currentStep > WIZARD_STEPS.LOCATION_SELECTION) {
|
||||
return {
|
||||
...state,
|
||||
currentStep: state.currentStep - 1,
|
||||
validationErrors: [],
|
||||
canProceed: validateStep(state.currentStep - 1, state),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
||||
case 'RESET_WIZARD':
|
||||
return initialWizardState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
return state.userDescription.trim().length >= 0;
|
||||
|
||||
case WIZARD_STEPS.TOOL_SELECTION: // Tool selection
|
||||
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
|
||||
return state.backgroundColor.length > 0;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state for the management dialog.
|
||||
*/
|
||||
export const initialManagementState: ManagementDialogState = {
|
||||
currentStep: MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||
availableAgents: [],
|
||||
selectedAgent: null,
|
||||
selectedAgentIndex: -1,
|
||||
selectedAction: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
canProceed: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for managing management dialog state transitions.
|
||||
*/
|
||||
export function managementReducer(
|
||||
state: ManagementDialogState,
|
||||
action: ManagementAction,
|
||||
): ManagementDialogState {
|
||||
switch (action.type) {
|
||||
case 'SET_AVAILABLE_AGENTS':
|
||||
return {
|
||||
...state,
|
||||
availableAgents: action.payload,
|
||||
canProceed: action.payload.length > 0,
|
||||
};
|
||||
|
||||
case 'SELECT_AGENT':
|
||||
return {
|
||||
...state,
|
||||
selectedAgent: action.payload.agent,
|
||||
selectedAgentIndex: action.payload.index,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SELECT_ACTION':
|
||||
return {
|
||||
...state,
|
||||
selectedAction: action.payload,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'GO_TO_NEXT_STEP': {
|
||||
const nextStep = state.currentStep + 1;
|
||||
return {
|
||||
...state,
|
||||
currentStep: nextStep,
|
||||
canProceed: getCanProceedForStep(nextStep, state),
|
||||
};
|
||||
}
|
||||
|
||||
case 'GO_TO_PREVIOUS_STEP': {
|
||||
const prevStep = Math.max(
|
||||
MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||
state.currentStep - 1,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
currentStep: prevStep,
|
||||
canProceed: getCanProceedForStep(prevStep, state),
|
||||
};
|
||||
}
|
||||
|
||||
case 'GO_TO_STEP':
|
||||
return {
|
||||
...state,
|
||||
currentStep: action.payload,
|
||||
canProceed: getCanProceedForStep(action.payload, state),
|
||||
};
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
};
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
};
|
||||
|
||||
case 'SET_CAN_PROCEED':
|
||||
return {
|
||||
...state,
|
||||
canProceed: action.payload,
|
||||
};
|
||||
|
||||
case 'RESET_DIALOG':
|
||||
return initialManagementState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a management step can proceed based on current state.
|
||||
*/
|
||||
function getCanProceedForStep(
|
||||
step: number,
|
||||
state: ManagementDialogState,
|
||||
): boolean {
|
||||
switch (step) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return state.availableAgents.length > 0 && state.selectedAgent !== null;
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return state.selectedAction !== null;
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return true; // Can always go back from viewer
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return true; // TODO: Add validation for editor
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return true; // Can always proceed from confirmation
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { SubagentLevel, Config } from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
SubagentLevel,
|
||||
SubagentConfig,
|
||||
SubagentMetadata,
|
||||
Config,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
||||
/**
|
||||
* State management for the subagent creation wizard.
|
||||
@@ -110,3 +115,55 @@ export interface WizardResult {
|
||||
tools?: string[];
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State management for the subagent management dialog.
|
||||
*/
|
||||
export interface ManagementDialogState {
|
||||
currentStep: number;
|
||||
availableAgents: SubagentMetadata[];
|
||||
selectedAgent: SubagentConfig | null;
|
||||
selectedAgentIndex: number;
|
||||
selectedAction: 'view' | 'edit' | 'delete' | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
canProceed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions that can be dispatched to update management dialog state.
|
||||
*/
|
||||
export type ManagementAction =
|
||||
| { type: 'SET_AVAILABLE_AGENTS'; payload: SubagentMetadata[] }
|
||||
| { type: 'SELECT_AGENT'; payload: { agent: SubagentConfig; index: number } }
|
||||
| { type: 'SELECT_ACTION'; payload: 'view' | 'edit' | 'delete' }
|
||||
| { type: 'GO_TO_NEXT_STEP' }
|
||||
| { type: 'GO_TO_PREVIOUS_STEP' }
|
||||
| { type: 'GO_TO_STEP'; payload: number }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_CAN_PROCEED'; payload: boolean }
|
||||
| { type: 'RESET_DIALOG' };
|
||||
|
||||
/**
|
||||
* Props for management dialog step components.
|
||||
*/
|
||||
export interface ManagementStepProps {
|
||||
state: ManagementDialogState;
|
||||
dispatch: React.Dispatch<ManagementAction>;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
onCancel: () => void;
|
||||
config: Config | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constants for management dialog steps.
|
||||
*/
|
||||
export const MANAGEMENT_STEPS = {
|
||||
AGENT_SELECTION: 1,
|
||||
ACTION_SELECTION: 2,
|
||||
AGENT_VIEWER: 3,
|
||||
AGENT_EDITOR: 4,
|
||||
DELETE_CONFIRMATION: 5,
|
||||
} as const;
|
||||
|
||||
9
packages/cli/src/ui/components/subagents/utils.ts
Normal file
9
packages/cli/src/ui/components/subagents/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { COLOR_OPTIONS } from './constants.js';
|
||||
|
||||
export const shouldShowColor = (backgroundColor?: string): boolean =>
|
||||
backgroundColor !== undefined && backgroundColor !== 'auto';
|
||||
|
||||
export const getColorForDisplay = (colorName?: string): string | undefined =>
|
||||
!colorName || colorName === 'auto'
|
||||
? undefined
|
||||
: COLOR_OPTIONS.find((color) => color.name === colorName)?.value;
|
||||
@@ -4,9 +4,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CreationWizardState, WizardAction } from './types.js';
|
||||
import {
|
||||
CreationWizardState,
|
||||
WizardAction,
|
||||
ManagementDialogState,
|
||||
ManagementAction,
|
||||
MANAGEMENT_STEPS,
|
||||
} from './types.js';
|
||||
import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js';
|
||||
|
||||
export { MANAGEMENT_STEPS };
|
||||
|
||||
/**
|
||||
* Initial state for the creation wizard.
|
||||
*/
|
||||
@@ -163,3 +171,124 @@ function validateStep(step: number, state: CreationWizardState): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state for the management dialog.
|
||||
*/
|
||||
export const initialManagementState: ManagementDialogState = {
|
||||
currentStep: MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||
availableAgents: [],
|
||||
selectedAgent: null,
|
||||
selectedAgentIndex: -1,
|
||||
selectedAction: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
canProceed: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for managing management dialog state transitions.
|
||||
*/
|
||||
export function managementReducer(
|
||||
state: ManagementDialogState,
|
||||
action: ManagementAction,
|
||||
): ManagementDialogState {
|
||||
switch (action.type) {
|
||||
case 'SET_AVAILABLE_AGENTS':
|
||||
return {
|
||||
...state,
|
||||
availableAgents: action.payload,
|
||||
canProceed: action.payload.length > 0,
|
||||
};
|
||||
|
||||
case 'SELECT_AGENT':
|
||||
return {
|
||||
...state,
|
||||
selectedAgent: action.payload.agent,
|
||||
selectedAgentIndex: action.payload.index,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SELECT_ACTION':
|
||||
return {
|
||||
...state,
|
||||
selectedAction: action.payload,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'GO_TO_NEXT_STEP': {
|
||||
const nextStep = state.currentStep + 1;
|
||||
return {
|
||||
...state,
|
||||
currentStep: nextStep,
|
||||
canProceed: getCanProceedForStep(nextStep, state),
|
||||
};
|
||||
}
|
||||
|
||||
case 'GO_TO_PREVIOUS_STEP': {
|
||||
const prevStep = Math.max(
|
||||
MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||
state.currentStep - 1,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
currentStep: prevStep,
|
||||
canProceed: getCanProceedForStep(prevStep, state),
|
||||
};
|
||||
}
|
||||
|
||||
case 'GO_TO_STEP':
|
||||
return {
|
||||
...state,
|
||||
currentStep: action.payload,
|
||||
canProceed: getCanProceedForStep(action.payload, state),
|
||||
};
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
};
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
};
|
||||
|
||||
case 'SET_CAN_PROCEED':
|
||||
return {
|
||||
...state,
|
||||
canProceed: action.payload,
|
||||
};
|
||||
|
||||
case 'RESET_DIALOG':
|
||||
return initialManagementState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a management step can proceed based on current state.
|
||||
*/
|
||||
function getCanProceedForStep(
|
||||
step: number,
|
||||
state: ManagementDialogState,
|
||||
): boolean {
|
||||
switch (step) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return state.availableAgents.length > 0 && state.selectedAgent !== null;
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return state.selectedAction !== null;
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return true; // Can always go back from viewer
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return true; // TODO: Add validation for editor
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return true; // Can always proceed from confirmation
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
vi.fn(), // openPrivacyNotice
|
||||
vi.fn(), // openSettingsDialog
|
||||
vi.fn(), // openSubagentCreateDialog
|
||||
vi.fn(), // openAgentsManagerDialog
|
||||
vi.fn(), // toggleVimEnabled
|
||||
setIsProcessing,
|
||||
vi.fn(), // setGeminiMdFileCount
|
||||
@@ -898,6 +899,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
vi.fn(), // openPrivacyNotice
|
||||
vi.fn(), // openSettingsDialog
|
||||
vi.fn(), // openSubagentCreateDialog
|
||||
vi.fn(), // openAgentsManagerDialog
|
||||
vi.fn(), // toggleVimEnabled
|
||||
vi.fn(), // setIsProcessing
|
||||
vi.fn(), // setGeminiMdFileCount
|
||||
|
||||
@@ -52,6 +52,7 @@ export const useSlashCommandProcessor = (
|
||||
openPrivacyNotice: () => void,
|
||||
openSettingsDialog: () => void,
|
||||
openSubagentCreateDialog: () => void,
|
||||
openAgentsManagerDialog: () => void,
|
||||
toggleVimEnabled: () => Promise<boolean>,
|
||||
setIsProcessing: (isProcessing: boolean) => void,
|
||||
setGeminiMdFileCount: (count: number) => void,
|
||||
@@ -352,16 +353,19 @@ export const useSlashCommandProcessor = (
|
||||
toolArgs: result.toolArgs,
|
||||
};
|
||||
case 'message':
|
||||
addItem(
|
||||
{
|
||||
type:
|
||||
result.messageType === 'error'
|
||||
? MessageType.ERROR
|
||||
: MessageType.INFO,
|
||||
text: result.content,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
if (result.messageType === 'info') {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: result.content,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} else {
|
||||
addMessage({
|
||||
type: MessageType.ERROR,
|
||||
content: result.content,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
return { type: 'handled' };
|
||||
case 'dialog':
|
||||
switch (result.dialog) {
|
||||
@@ -383,6 +387,9 @@ export const useSlashCommandProcessor = (
|
||||
case 'subagent_create':
|
||||
openSubagentCreateDialog();
|
||||
return { type: 'handled' };
|
||||
case 'subagent_list':
|
||||
openAgentsManagerDialog();
|
||||
return { type: 'handled' };
|
||||
case 'help':
|
||||
return { type: 'handled' };
|
||||
default: {
|
||||
@@ -558,6 +565,7 @@ export const useSlashCommandProcessor = (
|
||||
setQuittingMessages,
|
||||
openSettingsDialog,
|
||||
openSubagentCreateDialog,
|
||||
openAgentsManagerDialog,
|
||||
setShellConfirmationRequest,
|
||||
setSessionShellAllowlist,
|
||||
setIsProcessing,
|
||||
|
||||
32
packages/cli/src/ui/hooks/useAgentsManagerDialog.ts
Normal file
32
packages/cli/src/ui/hooks/useAgentsManagerDialog.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export interface UseAgentsManagerDialogReturn {
|
||||
isAgentsManagerDialogOpen: boolean;
|
||||
openAgentsManagerDialog: () => void;
|
||||
closeAgentsManagerDialog: () => void;
|
||||
}
|
||||
|
||||
export const useAgentsManagerDialog = (): UseAgentsManagerDialogReturn => {
|
||||
const [isAgentsManagerDialogOpen, setIsAgentsManagerDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const openAgentsManagerDialog = useCallback(() => {
|
||||
setIsAgentsManagerDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeAgentsManagerDialog = useCallback(() => {
|
||||
setIsAgentsManagerDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isAgentsManagerDialogOpen,
|
||||
openAgentsManagerDialog,
|
||||
closeAgentsManagerDialog,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user