qwen-code/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx

344 lines
10 KiB
TypeScript
Raw Normal View History

2025-09-04 16:34:51 +08:00
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
2025-09-04 23:29:47 +08:00
import { useState, useCallback, useMemo, useEffect } from 'react';
import { Box, Text } from 'ink';
2025-09-04 16:34:51 +08:00
import { AgentSelectionStep } from './AgentSelectionStep.js';
import { ActionSelectionStep } from './ActionSelectionStep.js';
import { AgentViewerStep } from './AgentViewerStep.js';
2025-09-04 23:29:47 +08:00
import { EditOptionsStep } from './AgentEditStep.js';
import { AgentDeleteStep } from './AgentDeleteStep.js';
2025-09-10 13:41:28 +08:00
import { ToolSelector } from '../create/ToolSelector.js';
import { ColorSelector } from '../create/ColorSelector.js';
import { MANAGEMENT_STEPS } from '../types.js';
import { Colors } from '../../../colors.js';
import { theme } from '../../../semantic-colors.js';
2025-09-10 14:35:08 +08:00
import { getColorForDisplay, shouldShowColor } from '../utils.js';
2025-09-04 23:29:47 +08:00
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
import { useKeypress } from '../../../hooks/useKeypress.js';
2025-09-04 16:34:51 +08:00
interface AgentsManagerDialogProps {
onClose: () => void;
config: Config | null;
}
/**
* Main orchestrator component for the agents management dialog.
*/
export function AgentsManagerDialog({
onClose,
config,
}: AgentsManagerDialogProps) {
2025-09-04 23:29:47 +08:00
// Simple state management with useState hooks
const [availableAgents, setAvailableAgents] = useState<SubagentConfig[]>([]);
const [selectedAgentIndex, setSelectedAgentIndex] = useState<number>(-1);
const [navigationStack, setNavigationStack] = useState<string[]>([
MANAGEMENT_STEPS.AGENT_SELECTION,
]);
// Memoized selectedAgent based on index
const selectedAgent = useMemo(
() =>
selectedAgentIndex >= 0 ? availableAgents[selectedAgentIndex] : null,
[availableAgents, selectedAgentIndex],
);
// Function to load agents
const loadAgents = useCallback(async () => {
if (!config) return;
const manager = config.getSubagentManager();
// Load agents from all levels separately to show all agents including conflicts
const [projectAgents, userAgents, builtinAgents] = await Promise.all([
2025-09-04 23:29:47 +08:00
manager.listSubagents({ level: 'project' }),
manager.listSubagents({ level: 'user' }),
manager.listSubagents({ level: 'builtin' }),
2025-09-04 23:29:47 +08:00
]);
// Combine all agents (project, user, and builtin level)
const allAgents = [
...(projectAgents || []),
...(userAgents || []),
...(builtinAgents || []),
];
2025-09-04 23:29:47 +08:00
setAvailableAgents(allAgents);
}, [config]);
// Load agents when component mounts or config changes
useEffect(() => {
loadAgents();
}, [loadAgents]);
// Helper to get current step
const getCurrentStep = useCallback(
() =>
navigationStack[navigationStack.length - 1] ||
MANAGEMENT_STEPS.AGENT_SELECTION,
[navigationStack],
2025-09-04 16:34:51 +08:00
);
2025-09-04 23:29:47 +08:00
const handleSelectAgent = useCallback((agentIndex: number) => {
setSelectedAgentIndex(agentIndex);
setNavigationStack((prev) => [...prev, MANAGEMENT_STEPS.ACTION_SELECTION]);
2025-09-04 16:34:51 +08:00
}, []);
2025-09-04 23:29:47 +08:00
const handleNavigateToStep = useCallback((step: string) => {
setNavigationStack((prev) => [...prev, step]);
2025-09-04 16:34:51 +08:00
}, []);
2025-09-04 23:29:47 +08:00
const handleNavigateBack = useCallback(() => {
setNavigationStack((prev) => {
if (prev.length <= 1) {
return prev; // Can't go back from root step
}
return prev.slice(0, -1);
});
}, []);
const handleDeleteAgent = useCallback(
async (agent: SubagentConfig) => {
if (!config) return;
try {
const subagentManager = config.getSubagentManager();
await subagentManager.deleteSubagent(agent.name, agent.level);
// Reload agents to get updated state
await loadAgents();
// Navigate back to agent selection after successful deletion
setNavigationStack([MANAGEMENT_STEPS.AGENT_SELECTION]);
setSelectedAgentIndex(-1);
} catch (error) {
console.error('Failed to delete agent:', error);
throw error; // Re-throw to let the component handle the error state
}
},
[config, loadAgents],
);
2025-09-04 16:34:51 +08:00
// Centralized ESC key handling for the entire dialog
useKeypress(
(key) => {
if (key.name !== 'escape') {
return;
}
2025-09-04 23:29:47 +08:00
const currentStep = getCurrentStep();
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
2025-09-04 16:34:51 +08:00
// On first step, ESC cancels the entire dialog
2025-09-04 23:29:47 +08:00
onClose();
2025-09-04 16:34:51 +08:00
} else {
2025-09-04 23:29:47 +08:00
// On other steps, ESC goes back to previous step in navigation stack
handleNavigateBack();
2025-09-04 16:34:51 +08:00
}
},
{ isActive: true },
);
2025-09-04 16:34:51 +08:00
2025-09-04 23:29:47 +08:00
// Props for child components - now using direct state and callbacks
const commonProps = useMemo(
2025-09-04 16:34:51 +08:00
() => ({
2025-09-04 23:29:47 +08:00
onNavigateToStep: handleNavigateToStep,
onNavigateBack: handleNavigateBack,
2025-09-04 16:34:51 +08:00
}),
2025-09-04 23:29:47 +08:00
[handleNavigateToStep, handleNavigateBack],
2025-09-04 16:34:51 +08:00
);
const renderStepHeader = useCallback(() => {
2025-09-04 23:29:47 +08:00
const currentStep = getCurrentStep();
2025-09-04 16:34:51 +08:00
const getStepHeaderText = () => {
2025-09-04 23:29:47 +08:00
switch (currentStep) {
2025-09-04 16:34:51 +08:00
case MANAGEMENT_STEPS.AGENT_SELECTION:
return 'Agents';
case MANAGEMENT_STEPS.ACTION_SELECTION:
return 'Choose Action';
case MANAGEMENT_STEPS.AGENT_VIEWER:
2025-09-04 23:29:47 +08:00
return selectedAgent?.name;
case MANAGEMENT_STEPS.EDIT_OPTIONS:
return `Edit ${selectedAgent?.name}`;
case MANAGEMENT_STEPS.EDIT_TOOLS:
return `Edit Tools: ${selectedAgent?.name}`;
case MANAGEMENT_STEPS.EDIT_COLOR:
return `Edit Color: ${selectedAgent?.name}`;
2025-09-04 16:34:51 +08:00
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
2025-09-04 23:29:47 +08:00
return `Delete ${selectedAgent?.name}`;
2025-09-04 16:34:51 +08:00
default:
return 'Unknown Step';
}
};
2025-09-10 14:35:08 +08:00
// Use agent color for the Agent Viewer header
const headerColor =
currentStep === MANAGEMENT_STEPS.AGENT_VIEWER &&
selectedAgent &&
shouldShowColor(selectedAgent.color)
? getColorForDisplay(selectedAgent.color)
: undefined;
2025-09-04 16:34:51 +08:00
return (
<Box>
2025-09-10 14:35:08 +08:00
<Text bold color={headerColor}>
{getStepHeaderText()}
</Text>
2025-09-04 16:34:51 +08:00
</Box>
);
2025-09-04 23:29:47 +08:00
}, [getCurrentStep, selectedAgent]);
2025-09-04 16:34:51 +08:00
const renderStepFooter = useCallback(() => {
2025-09-04 23:29:47 +08:00
const currentStep = getCurrentStep();
2025-09-04 16:34:51 +08:00
const getNavigationInstructions = () => {
2025-09-04 23:29:47 +08:00
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
if (availableAgents.length === 0) {
2025-09-04 16:34:51 +08:00
return 'Esc to close';
}
return 'Enter to select, ↑↓ to navigate, Esc to close';
}
2025-09-04 23:29:47 +08:00
if (currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
return 'Esc to go back';
}
if (currentStep === MANAGEMENT_STEPS.DELETE_CONFIRMATION) {
return 'Enter to confirm, Esc to cancel';
}
return 'Enter to select, ↑↓ to navigate, Esc to go back';
2025-09-04 16:34:51 +08:00
};
return (
<Box>
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
</Box>
);
2025-09-04 23:29:47 +08:00
}, [getCurrentStep, availableAgents]);
2025-09-04 16:34:51 +08:00
const renderStepContent = useCallback(() => {
2025-09-04 23:29:47 +08:00
const currentStep = getCurrentStep();
switch (currentStep) {
2025-09-04 16:34:51 +08:00
case MANAGEMENT_STEPS.AGENT_SELECTION:
2025-09-04 23:29:47 +08:00
return (
<AgentSelectionStep
availableAgents={availableAgents}
onAgentSelect={handleSelectAgent}
{...commonProps}
/>
);
2025-09-04 16:34:51 +08:00
case MANAGEMENT_STEPS.ACTION_SELECTION:
return (
<ActionSelectionStep selectedAgent={selectedAgent} {...commonProps} />
);
2025-09-04 16:34:51 +08:00
case MANAGEMENT_STEPS.AGENT_VIEWER:
return (
2025-09-04 23:29:47 +08:00
<AgentViewerStep selectedAgent={selectedAgent} {...commonProps} />
);
case MANAGEMENT_STEPS.EDIT_OPTIONS:
return (
<EditOptionsStep selectedAgent={selectedAgent} {...commonProps} />
);
case MANAGEMENT_STEPS.EDIT_TOOLS:
return (
<Box flexDirection="column" gap={1}>
<ToolSelector
tools={selectedAgent?.tools || []}
onSelect={async (tools) => {
if (selectedAgent && config) {
try {
// Save the changes using SubagentManager
const subagentManager = config.getSubagentManager();
await subagentManager.updateSubagent(
selectedAgent.name,
{ tools },
selectedAgent.level,
);
// Reload agents to get updated state
await loadAgents();
handleNavigateBack();
} catch (error) {
console.error('Failed to save agent changes:', error);
}
}
}}
config={config}
/>
2025-09-04 16:34:51 +08:00
</Box>
);
2025-09-04 23:29:47 +08:00
case MANAGEMENT_STEPS.EDIT_COLOR:
2025-09-04 16:34:51 +08:00
return (
2025-09-04 23:29:47 +08:00
<Box flexDirection="column" gap={1}>
<ColorSelector
color={selectedAgent?.color || 'auto'}
2025-09-04 23:29:47 +08:00
agentName={selectedAgent?.name || 'Agent'}
onSelect={async (color) => {
// Save changes and reload agents
if (selectedAgent && config) {
try {
// Save the changes using SubagentManager
const subagentManager = config.getSubagentManager();
await subagentManager.updateSubagent(
selectedAgent.name,
{ color },
2025-09-04 23:29:47 +08:00
selectedAgent.level,
);
// Reload agents to get updated state
await loadAgents();
handleNavigateBack();
} catch (error) {
console.error('Failed to save color changes:', error);
}
}
}}
/>
2025-09-04 16:34:51 +08:00
</Box>
);
2025-09-04 23:29:47 +08:00
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
return (
<AgentDeleteStep
selectedAgent={selectedAgent}
onDelete={handleDeleteAgent}
{...commonProps}
/>
);
2025-09-04 16:34:51 +08:00
default:
return (
<Box>
2025-09-04 23:29:47 +08:00
<Text color={theme.status.error}>Invalid step: {currentStep}</Text>
2025-09-04 16:34:51 +08:00
</Box>
);
}
2025-09-04 23:29:47 +08:00
}, [
getCurrentStep,
availableAgents,
selectedAgent,
commonProps,
config,
loadAgents,
handleNavigateBack,
handleSelectAgent,
handleDeleteAgent,
]);
2025-09-04 16:34:51 +08:00
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>
);
}