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';
|
2025-09-04 16:34:51 +08:00
|
|
|
import { Box, Text, useInput } from 'ink';
|
|
|
|
|
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';
|
|
|
|
|
import { ToolSelector } from './ToolSelector.js';
|
|
|
|
|
import { ColorSelector } from './ColorSelector.js';
|
|
|
|
|
import { MANAGEMENT_STEPS } from './types.js';
|
2025-09-04 16:34:51 +08:00
|
|
|
import { Colors } from '../../colors.js';
|
|
|
|
|
import { theme } from '../../semantic-colors.js';
|
2025-09-04 23:29:47 +08:00
|
|
|
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
|
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 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 || [])];
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
useInput((input, key) => {
|
|
|
|
|
if (key.escape) {
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box>
|
|
|
|
|
<Text bold>{getStepHeaderText()}</Text>
|
|
|
|
|
</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:
|
2025-09-04 23:29:47 +08:00
|
|
|
return <ActionSelectionStep {...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
|
|
|
|
|
backgroundColor={selectedAgent?.backgroundColor || 'auto'}
|
|
|
|
|
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,
|
|
|
|
|
{ backgroundColor: color },
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|