diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index b040c363..b5abf4cd 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -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}
/>
+ ) : isAgentsManagerDialogOpen ? (
+
+
+
) : isAuthenticating ? (
<>
{isQwenAuth && isQwenAuthenticating ? (
diff --git a/packages/cli/src/ui/commands/agentsCommand.ts b/packages/cli/src/ui/commands/agentsCommand.ts
index 307a1cdf..b0781c3c 100644
--- a/packages/cli/src/ui/commands/agentsCommand.ts
+++ b/packages/cli/src/ui/commands/agentsCommand.ts
@@ -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 => {
- 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 => {
- if (!args || args.trim() === '') {
- return {
- type: 'message',
- messageType: 'error',
- content: 'Usage: /agents show ',
- };
- }
-
- 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 => {
- if (!args || args.trim() === '') {
- return {
- type: 'message',
- messageType: 'error',
- content: 'Usage: /agents edit ',
- };
- }
-
- 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 => {
- if (!args || args.trim() === '') {
- return {
- type: 'message',
- messageType: 'error',
- content: 'Usage: /agents delete ',
- };
- }
-
- context.ui.addItem(
- {
- type: MessageType.INFO,
- text: `Deleting subagent: ${args.trim()} (not implemented yet)`,
- },
- Date.now(),
- );
- },
- },
],
};
diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts
index 91f42a2e..f0411e03 100644
--- a/packages/cli/src/ui/commands/types.ts
+++ b/packages/cli/src/ui/commands/types.ts
@@ -111,7 +111,8 @@ export interface OpenDialogActionReturn {
| 'editor'
| 'privacy'
| 'settings'
- | 'subagent_create';
+ | 'subagent_create'
+ | 'subagent_list';
}
/**
diff --git a/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx b/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx
new file mode 100644
index 00000000..f3e14637
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx b/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx
new file mode 100644
index 00000000..3e68a0d9
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx
@@ -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({
+ 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 (
+
+ Loading agents...
+
+ );
+ }
+
+ if (state.error) {
+ return (
+
+ {state.error}
+
+ );
+ }
+
+ if (state.availableAgents.length === 0) {
+ return (
+
+ No subagents found.
+
+ Use '/agents create' to create your first subagent.
+
+
+ );
+ }
+
+ // 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 (
+
+
+
+ {isSelected ? '●' : ' '}
+
+
+
+ {agent.name}
+ {agent.level === 'user' && projectNames.has(agent.name) && (
+
+ {' '}
+ (overridden by project level agent)
+
+ )}
+
+
+ );
+ };
+
+ // Calculate enabled agents count (excluding conflicted user-level agents)
+ const enabledAgentsCount =
+ projectAgents.length +
+ userAgents.filter((agent) => !projectNames.has(agent.name)).length;
+
+ return (
+
+ {/* Project Level Agents */}
+ {projectAgents.length > 0 && (
+
+
+ Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')})
+
+
+ {projectAgents.map((agent, index) => {
+ const isSelected =
+ navigation.currentBlock === 'project' &&
+ navigation.projectIndex === index;
+ return renderAgentItem(agent, index, isSelected);
+ })}
+
+
+ )}
+
+ {/* User Level Agents */}
+ {userAgents.length > 0 && (
+
+
+ User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')})
+
+
+ {userAgents.map((agent, index) => {
+ const isSelected =
+ navigation.currentBlock === 'user' &&
+ navigation.userIndex === index;
+ return renderAgentItem(agent, index, isSelected);
+ })}
+
+
+ )}
+
+ {/* Agent count summary */}
+ {(projectAgents.length > 0 || userAgents.length > 0) && (
+
+
+ Using: {enabledAgentsCount} agents
+
+
+ )}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx b/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx
new file mode 100644
index 00000000..3aa48f8e
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx
@@ -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 (
+
+ No agent selected
+
+ );
+ }
+
+ const agent = state.selectedAgent;
+
+ const toolsDisplay = agent.tools ? agent.tools.join(', ') : '*';
+
+ return (
+
+
+
+ Location:
+
+ {agent.level === 'project'
+ ? 'Project Level (.qwen/agents/)'
+ : 'User Level (~/.qwen/agents/)'}
+
+
+
+
+ File Path:
+ {agent.filePath}
+
+
+
+ Tools:
+ {toolsDisplay}
+
+
+ {shouldShowColor(agent.backgroundColor) && (
+
+ Color:
+
+ {` ${agent.name} `}
+
+
+ )}
+
+
+ Description:
+
+
+ {agent.description}
+
+
+
+ System Prompt:
+
+
+ {agent.systemPrompt}
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx
new file mode 100644
index 00000000..699e80e7
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx
@@ -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 (
+
+ {getStepHeaderText()}
+
+ );
+ }, [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 (
+
+ {getNavigationInstructions()}
+
+ );
+ }, [state.currentStep, state.availableAgents.length]);
+
+ const renderStepContent = useCallback(() => {
+ switch (state.currentStep) {
+ case MANAGEMENT_STEPS.AGENT_SELECTION:
+ return ;
+ case MANAGEMENT_STEPS.ACTION_SELECTION:
+ return ;
+ case MANAGEMENT_STEPS.AGENT_VIEWER:
+ return ;
+ case MANAGEMENT_STEPS.AGENT_EDITOR:
+ return (
+
+
+ Agent editing not yet implemented
+
+
+ );
+ case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
+ return (
+
+
+ Agent deletion not yet implemented
+
+
+ );
+ default:
+ return (
+
+
+ Invalid step: {state.currentStep}
+
+
+ );
+ }
+ }, [stepProps, state.currentStep]);
+
+ return (
+
+ {/* Main content wrapped in bounding box */}
+
+ {renderStepHeader()}
+ {renderStepContent()}
+ {renderStepFooter()}
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/subagents/ColorSelector.tsx b/packages/cli/src/ui/components/subagents/ColorSelector.tsx
index 451216ad..7e318803 100644
--- a/packages/cli/src/ui/components/subagents/ColorSelector.tsx
+++ b/packages/cli/src/ui/components/subagents/ColorSelector.tsx
@@ -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({
Preview:
-
- {state.generatedName}
+
+ {` ${state.generatedName} `}
diff --git a/packages/cli/src/ui/components/subagents/CreationSummary.tsx b/packages/cli/src/ui/components/subagents/CreationSummary.tsx
index 5dadb7da..10af42f4 100644
--- a/packages/cli/src/ui/components/subagents/CreationSummary.tsx
+++ b/packages/cli/src/ui/components/subagents/CreationSummary.tsx
@@ -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({
{toolsDisplay}
+ {shouldShowColor(state.backgroundColor) && (
+
+ Color:
+
+ {` ${state.generatedName} `}
+
+
+ )}
+
Description:
@@ -337,11 +346,11 @@ export function CreationSummary({
{saveError && (
-
+
❌ Error saving subagent:
-
+
{saveError}
diff --git a/packages/cli/src/ui/components/subagents/DescriptionInput.tsx b/packages/cli/src/ui/components/subagents/DescriptionInput.tsx
index 8c1c7b8e..1d1d7f72 100644
--- a/packages/cli/src/ui/components/subagents/DescriptionInput.tsx
+++ b/packages/cli/src/ui/components/subagents/DescriptionInput.tsx
@@ -302,7 +302,7 @@ export function DescriptionInput({
{state.validationErrors.length > 0 && (
{state.validationErrors.map((error, index) => (
-
+
⚠ {error}
))}
diff --git a/packages/cli/src/ui/components/subagents/ErrorBoundary.tsx b/packages/cli/src/ui/components/subagents/ErrorBoundary.tsx
deleted file mode 100644
index e5716383..00000000
--- a/packages/cli/src/ui/components/subagents/ErrorBoundary.tsx
+++ /dev/null
@@ -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 (
-
-
-
- ❌ Subagent Wizard Error
-
-
-
-
-
- An unexpected error occurred in the subagent creation wizard.
-
-
-
- {this.state.error && (
-
-
- Error Details:
-
-
- {this.state.error.message}
-
-
- )}
-
-
-
- Press Esc to close the wizard and try
- again.
-
-
-
- {process.env['NODE_ENV'] === 'development' &&
- this.state.errorInfo && (
-
-
- Stack Trace (Development):
-
-
- {this.state.errorInfo.componentStack}
-
-
- )}
-
- );
- }
-
- return this.props.children;
- }
-}
diff --git a/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx b/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx
index 23934f93..3d660506 100644
--- a/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx
+++ b/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx
@@ -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 (
-
+
-
+
Debug Info:
Step: {state.currentStep}
@@ -128,7 +127,9 @@ export function SubagentCreationWizard({
Location: {state.location}
Method: {state.generationMethod}
{state.validationErrors.length > 0 && (
- Errors: {state.validationErrors.join(', ')}
+
+ Errors: {state.validationErrors.join(', ')}
+
)}
@@ -201,35 +202,30 @@ export function SubagentCreationWizard({
default:
return (
- Invalid step: {state.currentStep}
+
+ Invalid step: {state.currentStep}
+
);
}
}, [stepProps, state.currentStep]);
return (
- {
- // Additional error handling if needed
- console.error('Subagent wizard error:', error, errorInfo);
- }}
- >
-
- {/* Main content wrapped in bounding box */}
-
- {renderStepHeader()}
- {renderStepContent()}
- {renderDebugContent()}
- {renderStepFooter()}
-
+
+ {/* Main content wrapped in bounding box */}
+
+ {renderStepHeader()}
+ {renderStepContent()}
+ {renderDebugContent()}
+ {renderStepFooter()}
-
+
);
}
diff --git a/packages/cli/src/ui/components/subagents/constants.ts b/packages/cli/src/ui/components/subagents/constants.ts
index 59c44243..0aecfefe 100644
--- a/packages/cli/src/ui/components/subagents/constants.ts
+++ b/packages/cli/src/ui/components/subagents/constants.ts
@@ -30,3 +30,42 @@ export const STEP_NAMES: Record = {
[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',
+ },
+];
diff --git a/packages/cli/src/ui/components/subagents/index.ts b/packages/cli/src/ui/components/subagents/index.ts
index eec7e0d6..eb4aa00d 100644
--- a/packages/cli/src/ui/components/subagents/index.ts
+++ b/packages/cli/src/ui/components/subagents/index.ts
@@ -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';
diff --git a/packages/cli/src/ui/components/subagents/reducers.tsx b/packages/cli/src/ui/components/subagents/reducers.tsx
new file mode 100644
index 00000000..a7f57a3f
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/reducers.tsx
@@ -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;
+ }
+}
diff --git a/packages/cli/src/ui/components/subagents/types.ts b/packages/cli/src/ui/components/subagents/types.ts
index 444b228e..d3393b98 100644
--- a/packages/cli/src/ui/components/subagents/types.ts
+++ b/packages/cli/src/ui/components/subagents/types.ts
@@ -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;
+ 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;
diff --git a/packages/cli/src/ui/components/subagents/utils.ts b/packages/cli/src/ui/components/subagents/utils.ts
new file mode 100644
index 00000000..5f777bf9
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/utils.ts
@@ -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;
diff --git a/packages/cli/src/ui/components/subagents/wizardReducer.ts b/packages/cli/src/ui/components/subagents/wizardReducer.ts
index 8fb21bed..a7f57a3f 100644
--- a/packages/cli/src/ui/components/subagents/wizardReducer.ts
+++ b/packages/cli/src/ui/components/subagents/wizardReducer.ts
@@ -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;
+ }
+}
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
index 6ed2d5f3..b94c95cd 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
@@ -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
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index c96f6d97..c9cb8dd1 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -52,6 +52,7 @@ export const useSlashCommandProcessor = (
openPrivacyNotice: () => void,
openSettingsDialog: () => void,
openSubagentCreateDialog: () => void,
+ openAgentsManagerDialog: () => void,
toggleVimEnabled: () => Promise,
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,
diff --git a/packages/cli/src/ui/hooks/useAgentsManagerDialog.ts b/packages/cli/src/ui/hooks/useAgentsManagerDialog.ts
new file mode 100644
index 00000000..b5904af6
--- /dev/null
+++ b/packages/cli/src/ui/hooks/useAgentsManagerDialog.ts
@@ -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,
+ };
+};
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 0b04c875..b0db59c8 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -54,6 +54,7 @@ import {
} from '../services/fileSystemService.js';
import { logCliConfiguration, logIdeConnection } from '../telemetry/loggers.js';
import { IdeConnectionEvent, IdeConnectionType } from '../telemetry/types.js';
+import { SubagentManager } from '../subagents/subagent-manager.js';
// Re-export OAuth config type
export type { MCPOAuthConfig };
@@ -316,6 +317,7 @@ export class Config {
private readonly shouldUseNodePtyShell: boolean;
private readonly skipNextSpeakerCheck: boolean;
private initialized: boolean = false;
+ private subagentManager: SubagentManager | null = null;
constructor(params: ConfigParameters) {
this.sessionId = params.sessionId;
@@ -865,6 +867,13 @@ export class Config {
return this.gitService;
}
+ getSubagentManager(): SubagentManager {
+ if (!this.subagentManager) {
+ this.subagentManager = new SubagentManager(this.targetDir);
+ }
+ return this.subagentManager;
+ }
+
async createToolRegistry(): Promise {
const registry = new ToolRegistry(this);
diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts
index e07995bf..a2d94b96 100644
--- a/packages/core/src/subagents/subagent-manager.ts
+++ b/packages/core/src/subagents/subagent-manager.ts
@@ -111,12 +111,28 @@ export class SubagentManager {
/**
* Loads a subagent configuration by name.
- * Searches project-level first, then user-level.
+ * If level is specified, only searches that level.
+ * If level is omitted, searches project-level first, then user-level.
*
* @param name - Name of the subagent to load
+ * @param level - Optional level to limit search to specific level
* @returns SubagentConfig or null if not found
*/
- async loadSubagent(name: string): Promise {
+ async loadSubagent(
+ name: string,
+ level?: SubagentLevel,
+ ): Promise {
+ if (level) {
+ // Search only the specified level
+ const path = this.getSubagentPath(name, level);
+ try {
+ const config = await this.parseSubagentFile(path);
+ return config;
+ } catch (_error) {
+ return null;
+ }
+ }
+
// Try project level first
const projectPath = this.getSubagentPath(name, 'project');
try {
@@ -147,8 +163,9 @@ export class SubagentManager {
async updateSubagent(
name: string,
updates: Partial,
+ level?: SubagentLevel,
): Promise {
- const existing = await this.loadSubagent(name);
+ const existing = await this.loadSubagent(name, level);
if (!existing) {
throw new SubagentError(
`Subagent "${name}" not found`,
@@ -287,8 +304,11 @@ export class SubagentManager {
* @param name - Name of the subagent to find
* @returns SubagentMetadata or null if not found
*/
- async findSubagentByName(name: string): Promise {
- const config = await this.loadSubagent(name);
+ async findSubagentByName(
+ name: string,
+ level?: SubagentLevel,
+ ): Promise {
+ const config = await this.loadSubagent(name, level);
if (!config) {
return null;
}
@@ -361,6 +381,9 @@ export class SubagentManager {
const runConfig = frontmatter['runConfig'] as
| Record
| undefined;
+ const backgroundColor = frontmatter['backgroundColor'] as
+ | string
+ | undefined;
// Determine level from file path
// Project level paths contain the project root, user level paths are in home directory
@@ -382,6 +405,7 @@ export class SubagentManager {
runConfig: runConfig as Partial<
import('../core/subagent.js').RunConfig
>,
+ backgroundColor,
};
// Validate the parsed configuration
@@ -424,6 +448,10 @@ export class SubagentManager {
frontmatter['runConfig'] = config.runConfig;
}
+ if (config.backgroundColor && config.backgroundColor !== 'auto') {
+ frontmatter['backgroundColor'] = config.backgroundColor;
+ }
+
// Serialize to YAML
const yamlContent = stringifyYaml(frontmatter, {
lineWidth: 0, // Disable line wrapping
@@ -616,7 +644,7 @@ export class SubagentManager {
* @returns True if name is available
*/
async isNameAvailable(name: string, level?: SubagentLevel): Promise {
- const existing = await this.loadSubagent(name);
+ const existing = await this.loadSubagent(name, level);
if (!existing) {
return true; // Name is available
diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts
index 5e61fccf..955112f6 100644
--- a/packages/core/src/subagents/types.ts
+++ b/packages/core/src/subagents/types.ts
@@ -59,6 +59,12 @@ export interface SubagentConfig {
* Can specify max_time_minutes and max_turns.
*/
runConfig?: Partial;
+
+ /**
+ * Optional background color for runtime display.
+ * If 'auto' or omitted, uses automatic color assignment.
+ */
+ backgroundColor?: string;
}
/**