mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent list dialog - done
This commit is contained in:
@@ -4,16 +4,23 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||||
import { ManagementStepProps } from './types.js';
|
import { MANAGEMENT_STEPS } from './types.js';
|
||||||
|
|
||||||
|
interface ActionSelectionStepProps {
|
||||||
|
onNavigateToStep: (step: string) => void;
|
||||||
|
onNavigateBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const ActionSelectionStep = ({
|
export const ActionSelectionStep = ({
|
||||||
state,
|
onNavigateToStep,
|
||||||
dispatch,
|
onNavigateBack,
|
||||||
onNext,
|
}: ActionSelectionStepProps) => {
|
||||||
onPrevious,
|
const [selectedAction, setSelectedAction] = useState<
|
||||||
}: ManagementStepProps) => {
|
'view' | 'edit' | 'delete' | null
|
||||||
|
>(null);
|
||||||
const actions = [
|
const actions = [
|
||||||
{ label: 'View Agent', value: 'view' as const },
|
{ label: 'View Agent', value: 'view' as const },
|
||||||
{ label: 'Edit Agent', value: 'edit' as const },
|
{ label: 'Edit Agent', value: 'edit' as const },
|
||||||
@@ -23,16 +30,24 @@ export const ActionSelectionStep = ({
|
|||||||
|
|
||||||
const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => {
|
const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => {
|
||||||
if (value === 'back') {
|
if (value === 'back') {
|
||||||
onPrevious();
|
onNavigateBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SELECT_ACTION', payload: value });
|
setSelectedAction(value);
|
||||||
onNext();
|
|
||||||
|
// Navigate to appropriate step based on action
|
||||||
|
if (value === 'view') {
|
||||||
|
onNavigateToStep(MANAGEMENT_STEPS.AGENT_VIEWER);
|
||||||
|
} else if (value === 'edit') {
|
||||||
|
onNavigateToStep(MANAGEMENT_STEPS.EDIT_OPTIONS);
|
||||||
|
} else if (value === 'delete') {
|
||||||
|
onNavigateToStep(MANAGEMENT_STEPS.DELETE_CONFIRMATION);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedIndex = state.selectedAction
|
const selectedIndex = selectedAction
|
||||||
? actions.findIndex((action) => action.value === state.selectedAction)
|
? actions.findIndex((action) => action.value === selectedAction)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
57
packages/cli/src/ui/components/subagents/AgentDeleteStep.tsx
Normal file
57
packages/cli/src/ui/components/subagents/AgentDeleteStep.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
import { StepNavigationProps } from './types.js';
|
||||||
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||||
|
|
||||||
|
interface AgentDeleteStepProps extends StepNavigationProps {
|
||||||
|
selectedAgent: SubagentConfig | null;
|
||||||
|
onDelete: (agent: SubagentConfig) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentDeleteStep({
|
||||||
|
selectedAgent,
|
||||||
|
onDelete,
|
||||||
|
onNavigateBack,
|
||||||
|
}: AgentDeleteStepProps) {
|
||||||
|
useKeypress(
|
||||||
|
async (key) => {
|
||||||
|
if (!selectedAgent) return;
|
||||||
|
|
||||||
|
if (key.name === 'y' || key.name === 'return') {
|
||||||
|
try {
|
||||||
|
await onDelete(selectedAgent);
|
||||||
|
// Navigation will be handled by the parent component after successful deletion
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete agent:', error);
|
||||||
|
}
|
||||||
|
} else if (key.name === 'n') {
|
||||||
|
onNavigateBack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ isActive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedAgent) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.status.error}>No agent selected</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Text color={theme.status.error}>
|
||||||
|
Are you sure you want to delete agent “{selectedAgent.name}
|
||||||
|
”?
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
packages/cli/src/ui/components/subagents/AgentEditStep.tsx
Normal file
111
packages/cli/src/ui/components/subagents/AgentEditStep.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||||
|
import { MANAGEMENT_STEPS } from './types.js';
|
||||||
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
import { useLaunchEditor } from './useLaunchEditor.js';
|
||||||
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
|
interface EditOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editOptions: EditOption[] = [
|
||||||
|
{
|
||||||
|
id: 'editor',
|
||||||
|
label: 'Open in editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tools',
|
||||||
|
label: 'Edit tools',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'color',
|
||||||
|
label: 'Edit color',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface EditOptionsStepProps {
|
||||||
|
selectedAgent: SubagentConfig | null;
|
||||||
|
onNavigateToStep: (step: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit options selection step - choose what to edit about the agent.
|
||||||
|
*/
|
||||||
|
export function EditOptionsStep({
|
||||||
|
selectedAgent,
|
||||||
|
onNavigateToStep,
|
||||||
|
}: EditOptionsStepProps) {
|
||||||
|
const [selectedOption, setSelectedOption] = useState<string>('editor');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const launchEditor = useLaunchEditor();
|
||||||
|
|
||||||
|
const handleHighlight = (selectedValue: string) => {
|
||||||
|
setSelectedOption(selectedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
async (selectedValue: string) => {
|
||||||
|
if (!selectedAgent) return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (selectedValue === 'editor') {
|
||||||
|
// Launch editor directly
|
||||||
|
try {
|
||||||
|
await launchEditor(selectedAgent?.filePath);
|
||||||
|
} catch (err) {
|
||||||
|
setError(
|
||||||
|
`Failed to launch editor: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (selectedValue === 'tools') {
|
||||||
|
onNavigateToStep(MANAGEMENT_STEPS.EDIT_TOOLS);
|
||||||
|
} else if (selectedValue === 'color') {
|
||||||
|
onNavigateToStep(MANAGEMENT_STEPS.EDIT_COLOR);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedAgent, onNavigateToStep, launchEditor],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<RadioButtonSelect
|
||||||
|
items={editOptions.map((option) => ({
|
||||||
|
label: option.label,
|
||||||
|
value: option.id,
|
||||||
|
}))}
|
||||||
|
initialIndex={editOptions.findIndex(
|
||||||
|
(opt) => opt.id === selectedOption,
|
||||||
|
)}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onHighlight={handleHighlight}
|
||||||
|
isFocused={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text bold color={theme.status.error}>
|
||||||
|
❌ Error:
|
||||||
|
</Text>
|
||||||
|
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||||
|
<Text color={theme.status.error} wrap="wrap">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { ManagementStepProps } from './types.js';
|
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../colors.js';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||||
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface NavigationState {
|
interface NavigationState {
|
||||||
currentBlock: 'project' | 'user';
|
currentBlock: 'project' | 'user';
|
||||||
@@ -17,13 +17,15 @@ interface NavigationState {
|
|||||||
userIndex: number;
|
userIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AgentSelectionStepProps {
|
||||||
|
availableAgents: SubagentConfig[];
|
||||||
|
onAgentSelect: (agentIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const AgentSelectionStep = ({
|
export const AgentSelectionStep = ({
|
||||||
state,
|
availableAgents,
|
||||||
dispatch,
|
onAgentSelect,
|
||||||
onNext,
|
}: AgentSelectionStepProps) => {
|
||||||
config,
|
|
||||||
}: ManagementStepProps) => {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [navigation, setNavigation] = useState<NavigationState>({
|
const [navigation, setNavigation] = useState<NavigationState>({
|
||||||
currentBlock: 'project',
|
currentBlock: 'project',
|
||||||
projectIndex: 0,
|
projectIndex: 0,
|
||||||
@@ -31,60 +33,27 @@ export const AgentSelectionStep = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Group agents by level
|
// Group agents by level
|
||||||
const projectAgents = state.availableAgents.filter(
|
const projectAgents = useMemo(
|
||||||
(agent) => agent.level === 'project',
|
() => availableAgents.filter((agent) => agent.level === 'project'),
|
||||||
|
[availableAgents],
|
||||||
);
|
);
|
||||||
const userAgents = state.availableAgents.filter(
|
const userAgents = useMemo(
|
||||||
(agent) => agent.level === 'user',
|
() => availableAgents.filter((agent) => agent.level === 'user'),
|
||||||
|
[availableAgents],
|
||||||
|
);
|
||||||
|
const projectNames = useMemo(
|
||||||
|
() => new Set(projectAgents.map((agent) => agent.name)),
|
||||||
|
[projectAgents],
|
||||||
);
|
);
|
||||||
const projectNames = new Set(projectAgents.map((agent) => agent.name));
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Initialize navigation state when agents are loaded (only once)
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (projectAgents.length > 0) {
|
if (projectAgents.length > 0) {
|
||||||
setNavigation((prev) => ({ ...prev, currentBlock: 'project' }));
|
setNavigation((prev) => ({ ...prev, currentBlock: 'project' }));
|
||||||
} else if (userAgents.length > 0) {
|
} else if (userAgents.length > 0) {
|
||||||
setNavigation((prev) => ({ ...prev, currentBlock: 'user' }));
|
setNavigation((prev) => ({ ...prev, currentBlock: 'user' }));
|
||||||
}
|
}
|
||||||
}, [projectAgents.length, userAgents.length]);
|
}, [projectAgents, userAgents]);
|
||||||
|
|
||||||
// Custom keyboard navigation
|
// Custom keyboard navigation
|
||||||
useKeypress(
|
useKeypress(
|
||||||
@@ -148,71 +117,24 @@ export const AgentSelectionStep = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (name === 'return' || name === 'space') {
|
} else if (name === 'return' || name === 'space') {
|
||||||
// Select current item
|
// Calculate global index and select current item
|
||||||
const currentAgent =
|
let globalIndex: number;
|
||||||
navigation.currentBlock === 'project'
|
if (navigation.currentBlock === 'project') {
|
||||||
? projectAgents[navigation.projectIndex]
|
globalIndex = navigation.projectIndex;
|
||||||
: userAgents[navigation.userIndex];
|
} else {
|
||||||
|
// User agents come after project agents in the availableAgents array
|
||||||
|
globalIndex = projectAgents.length + navigation.userIndex;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentAgent) {
|
if (globalIndex >= 0 && globalIndex < availableAgents.length) {
|
||||||
const agentIndex = state.availableAgents.indexOf(currentAgent);
|
onAgentSelect(globalIndex);
|
||||||
handleAgentSelect(agentIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ isActive: true },
|
{ isActive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAgentSelect = async (index: number) => {
|
if (availableAgents.length === 0) {
|
||||||
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 (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text color={theme.text.secondary}>No subagents found.</Text>
|
<Text color={theme.text.secondary}>No subagents found.</Text>
|
||||||
|
|||||||
@@ -4,20 +4,17 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { ManagementStepProps } from './types.js';
|
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||||
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
export const AgentViewerStep = ({ state, onPrevious }: ManagementStepProps) => {
|
interface AgentViewerStepProps {
|
||||||
// Handle keyboard input
|
selectedAgent: SubagentConfig | null;
|
||||||
useInput((input, key) => {
|
|
||||||
if (key.escape || input === 'b') {
|
|
||||||
onPrevious();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (!state.selectedAgent) {
|
export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
|
||||||
|
if (!selectedAgent) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.status.error}>No agent selected</Text>
|
<Text color={theme.status.error}>No agent selected</Text>
|
||||||
@@ -25,7 +22,7 @@ export const AgentViewerStep = ({ state, onPrevious }: ManagementStepProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = state.selectedAgent;
|
const agent = selectedAgent;
|
||||||
|
|
||||||
const toolsDisplay = agent.tools ? agent.tools.join(', ') : '*';
|
const toolsDisplay = agent.tools ? agent.tools.join(', ') : '*';
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,19 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useReducer, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { managementReducer, initialManagementState } from './reducers.js';
|
|
||||||
import { AgentSelectionStep } from './AgentSelectionStep.js';
|
import { AgentSelectionStep } from './AgentSelectionStep.js';
|
||||||
import { ActionSelectionStep } from './ActionSelectionStep.js';
|
import { ActionSelectionStep } from './ActionSelectionStep.js';
|
||||||
import { AgentViewerStep } from './AgentViewerStep.js';
|
import { AgentViewerStep } from './AgentViewerStep.js';
|
||||||
import { ManagementStepProps, MANAGEMENT_STEPS } from './types.js';
|
import { EditOptionsStep } from './AgentEditStep.js';
|
||||||
|
import { AgentDeleteStep } from './AgentDeleteStep.js';
|
||||||
|
import { ToolSelector } from './ToolSelector.js';
|
||||||
|
import { ColorSelector } from './ColorSelector.js';
|
||||||
|
import { MANAGEMENT_STEPS } from './types.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../colors.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { Config } from '@qwen-code/qwen-code-core';
|
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface AgentsManagerDialogProps {
|
interface AgentsManagerDialogProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -27,67 +30,132 @@ export function AgentsManagerDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
config,
|
config,
|
||||||
}: AgentsManagerDialogProps) {
|
}: AgentsManagerDialogProps) {
|
||||||
const [state, dispatch] = useReducer(
|
// Simple state management with useState hooks
|
||||||
managementReducer,
|
const [availableAgents, setAvailableAgents] = useState<SubagentConfig[]>([]);
|
||||||
initialManagementState,
|
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],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
// Function to load agents
|
||||||
dispatch({ type: 'GO_TO_NEXT_STEP' });
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectAgent = useCallback((agentIndex: number) => {
|
||||||
|
setSelectedAgentIndex(agentIndex);
|
||||||
|
setNavigationStack((prev) => [...prev, MANAGEMENT_STEPS.ACTION_SELECTION]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePrevious = useCallback(() => {
|
const handleNavigateToStep = useCallback((step: string) => {
|
||||||
dispatch({ type: 'GO_TO_PREVIOUS_STEP' });
|
setNavigationStack((prev) => [...prev, step]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleNavigateBack = useCallback(() => {
|
||||||
dispatch({ type: 'RESET_DIALOG' });
|
setNavigationStack((prev) => {
|
||||||
onClose();
|
if (prev.length <= 1) {
|
||||||
}, [onClose]);
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
// Centralized ESC key handling for the entire dialog
|
// Centralized ESC key handling for the entire dialog
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
if (key.escape) {
|
if (key.escape) {
|
||||||
// Agent viewer step handles its own ESC logic
|
const currentStep = getCurrentStep();
|
||||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
|
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||||
return; // Let AgentViewerStep handle it
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
|
||||||
// On first step, ESC cancels the entire dialog
|
// On first step, ESC cancels the entire dialog
|
||||||
handleCancel();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
// On other steps, ESC goes back to previous step
|
// On other steps, ESC goes back to previous step in navigation stack
|
||||||
handlePrevious();
|
handleNavigateBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const stepProps: ManagementStepProps = useMemo(
|
// Props for child components - now using direct state and callbacks
|
||||||
|
const commonProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
state,
|
onNavigateToStep: handleNavigateToStep,
|
||||||
config,
|
onNavigateBack: handleNavigateBack,
|
||||||
dispatch,
|
|
||||||
onNext: handleNext,
|
|
||||||
onPrevious: handlePrevious,
|
|
||||||
onCancel: handleCancel,
|
|
||||||
}),
|
}),
|
||||||
[state, dispatch, handleNext, handlePrevious, handleCancel, config],
|
[handleNavigateToStep, handleNavigateBack],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderStepHeader = useCallback(() => {
|
const renderStepHeader = useCallback(() => {
|
||||||
|
const currentStep = getCurrentStep();
|
||||||
const getStepHeaderText = () => {
|
const getStepHeaderText = () => {
|
||||||
switch (state.currentStep) {
|
switch (currentStep) {
|
||||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||||
return 'Agents';
|
return 'Agents';
|
||||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||||
return 'Choose Action';
|
return 'Choose Action';
|
||||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||||
return state.selectedAgent?.name;
|
return selectedAgent?.name;
|
||||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
case MANAGEMENT_STEPS.EDIT_OPTIONS:
|
||||||
return `Editing: ${state.selectedAgent?.name || 'Unknown'}`;
|
return `Edit ${selectedAgent?.name}`;
|
||||||
|
case MANAGEMENT_STEPS.EDIT_TOOLS:
|
||||||
|
return `Edit Tools: ${selectedAgent?.name}`;
|
||||||
|
case MANAGEMENT_STEPS.EDIT_COLOR:
|
||||||
|
return `Edit Color: ${selectedAgent?.name}`;
|
||||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||||
return `Delete: ${state.selectedAgent?.name || 'Unknown'}`;
|
return `Delete ${selectedAgent?.name}`;
|
||||||
default:
|
default:
|
||||||
return 'Unknown Step';
|
return 'Unknown Step';
|
||||||
}
|
}
|
||||||
@@ -98,22 +166,27 @@ export function AgentsManagerDialog({
|
|||||||
<Text bold>{getStepHeaderText()}</Text>
|
<Text bold>{getStepHeaderText()}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}, [state.currentStep, state.selectedAgent?.name]);
|
}, [getCurrentStep, selectedAgent]);
|
||||||
|
|
||||||
const renderStepFooter = useCallback(() => {
|
const renderStepFooter = useCallback(() => {
|
||||||
|
const currentStep = getCurrentStep();
|
||||||
const getNavigationInstructions = () => {
|
const getNavigationInstructions = () => {
|
||||||
if (state.currentStep === MANAGEMENT_STEPS.ACTION_SELECTION) {
|
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||||
return 'Enter to select, ↑↓ to navigate, Esc to go back';
|
if (availableAgents.length === 0) {
|
||||||
}
|
|
||||||
|
|
||||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
|
||||||
if (state.availableAgents.length === 0) {
|
|
||||||
return 'Esc to close';
|
return 'Esc to close';
|
||||||
}
|
}
|
||||||
return 'Enter to select, ↑↓ to navigate, Esc to close';
|
return 'Enter to select, ↑↓ to navigate, Esc to close';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
|
||||||
return 'Esc to go back';
|
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';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -121,42 +194,110 @@ export function AgentsManagerDialog({
|
|||||||
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
|
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}, [state.currentStep, state.availableAgents.length]);
|
}, [getCurrentStep, availableAgents]);
|
||||||
|
|
||||||
const renderStepContent = useCallback(() => {
|
const renderStepContent = useCallback(() => {
|
||||||
switch (state.currentStep) {
|
const currentStep = getCurrentStep();
|
||||||
|
switch (currentStep) {
|
||||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
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 (
|
return (
|
||||||
<Box>
|
<AgentSelectionStep
|
||||||
<Text color={theme.status.warning}>
|
availableAgents={availableAgents}
|
||||||
Agent editing not yet implemented
|
onAgentSelect={handleSelectAgent}
|
||||||
</Text>
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||||
|
return <ActionSelectionStep {...commonProps} />;
|
||||||
|
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||||
|
return (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case MANAGEMENT_STEPS.EDIT_COLOR:
|
||||||
|
return (
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||||
return (
|
return (
|
||||||
<Box>
|
<AgentDeleteStep
|
||||||
<Text color={theme.status.warning}>
|
selectedAgent={selectedAgent}
|
||||||
Agent deletion not yet implemented
|
onDelete={handleDeleteAgent}
|
||||||
</Text>
|
{...commonProps}
|
||||||
</Box>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.status.error}>
|
<Text color={theme.status.error}>Invalid step: {currentStep}</Text>
|
||||||
Invalid step: {state.currentStep}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [stepProps, state.currentStep]);
|
}, [
|
||||||
|
getCurrentStep,
|
||||||
|
availableAgents,
|
||||||
|
selectedAgent,
|
||||||
|
commonProps,
|
||||||
|
config,
|
||||||
|
loadAgents,
|
||||||
|
handleNavigateBack,
|
||||||
|
handleSelectAgent,
|
||||||
|
handleDeleteAgent,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
|
|||||||
@@ -4,25 +4,43 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||||
import { WizardStepProps, ColorOption } from './types.js';
|
import { ColorOption } from './types.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../colors.js';
|
||||||
import { COLOR_OPTIONS } from './constants.js';
|
import { COLOR_OPTIONS } from './constants.js';
|
||||||
|
|
||||||
const colorOptions: ColorOption[] = COLOR_OPTIONS;
|
const colorOptions: ColorOption[] = COLOR_OPTIONS;
|
||||||
|
|
||||||
|
interface ColorSelectorProps {
|
||||||
|
backgroundColor?: string;
|
||||||
|
agentName?: string;
|
||||||
|
onSelect: (color: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 5: Background color selection with preview.
|
* Color selection with preview.
|
||||||
*/
|
*/
|
||||||
export function ColorSelector({
|
export function ColorSelector({
|
||||||
state,
|
backgroundColor = 'auto',
|
||||||
dispatch,
|
agentName = 'Agent',
|
||||||
onNext,
|
onSelect,
|
||||||
onPrevious: _onPrevious,
|
}: ColorSelectorProps) {
|
||||||
}: WizardStepProps) {
|
const [selectedColor, setSelectedColor] = useState<string>(backgroundColor);
|
||||||
const handleSelect = (_selectedValue: string) => {
|
|
||||||
onNext();
|
// Update selected color when backgroundColor prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedColor(backgroundColor);
|
||||||
|
}, [backgroundColor]);
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue: string) => {
|
||||||
|
const colorOption = colorOptions.find(
|
||||||
|
(option) => option.id === selectedValue,
|
||||||
|
);
|
||||||
|
if (colorOption) {
|
||||||
|
onSelect(colorOption.name);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHighlight = (selectedValue: string) => {
|
const handleHighlight = (selectedValue: string) => {
|
||||||
@@ -30,12 +48,12 @@ export function ColorSelector({
|
|||||||
(option) => option.id === selectedValue,
|
(option) => option.id === selectedValue,
|
||||||
);
|
);
|
||||||
if (colorOption) {
|
if (colorOption) {
|
||||||
dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.name });
|
setSelectedColor(colorOption.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentColor =
|
const currentColor =
|
||||||
colorOptions.find((option) => option.name === state.backgroundColor) ||
|
colorOptions.find((option) => option.name === selectedColor) ||
|
||||||
colorOptions[0];
|
colorOptions[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,7 +76,7 @@ export function ColorSelector({
|
|||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Text color={Colors.Gray}>Preview:</Text>
|
<Text color={Colors.Gray}>Preview:</Text>
|
||||||
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
||||||
<Text color="black">{` ${state.generatedName} `}</Text>
|
<Text color="black">{` ${agentName} `}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -5,18 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useState, useEffect } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { Box, Text, useInput, useStdin } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from './types.js';
|
||||||
import { validateSubagentConfig } from './validation.js';
|
import { SubagentManager, SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
import {
|
|
||||||
SubagentManager,
|
|
||||||
SubagentConfig,
|
|
||||||
EditorType,
|
|
||||||
} from '@qwen-code/qwen-code-core';
|
|
||||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
|
||||||
import { spawnSync } from 'child_process';
|
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||||
|
import { useLaunchEditor } from './useLaunchEditor.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 6: Final confirmation and actions.
|
* Step 6: Final confirmation and actions.
|
||||||
@@ -31,8 +25,7 @@ export function CreationSummary({
|
|||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
const [warnings, setWarnings] = useState<string[]>([]);
|
const [warnings, setWarnings] = useState<string[]>([]);
|
||||||
|
|
||||||
const settings = useSettings();
|
const launchEditor = useLaunchEditor();
|
||||||
const { stdin, setRawMode } = useStdin();
|
|
||||||
|
|
||||||
const truncateText = (text: string, maxLength: number): string => {
|
const truncateText = (text: string, maxLength: number): string => {
|
||||||
if (text.length <= maxLength) return text;
|
if (text.length <= maxLength) return text;
|
||||||
@@ -113,19 +106,6 @@ export function CreationSummary({
|
|||||||
|
|
||||||
// Common method to save subagent configuration
|
// Common method to save subagent configuration
|
||||||
const saveSubagent = useCallback(async (): Promise<SubagentManager> => {
|
const saveSubagent = useCallback(async (): Promise<SubagentManager> => {
|
||||||
// Validate configuration before saving
|
|
||||||
const configToValidate = {
|
|
||||||
name: state.generatedName,
|
|
||||||
description: state.generatedDescription,
|
|
||||||
systemPrompt: state.generatedSystemPrompt,
|
|
||||||
tools: state.selectedTools,
|
|
||||||
};
|
|
||||||
|
|
||||||
const validation = validateSubagentConfig(configToValidate);
|
|
||||||
if (!validation.isValid) {
|
|
||||||
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create SubagentManager instance
|
// Create SubagentManager instance
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('Configuration not available');
|
throw new Error('Configuration not available');
|
||||||
@@ -190,62 +170,11 @@ export function CreationSummary({
|
|||||||
state.location,
|
state.location,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine editor to use
|
|
||||||
const preferredEditor = settings.merged.preferredEditor as
|
|
||||||
| EditorType
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
let editor: string;
|
|
||||||
if (preferredEditor) {
|
|
||||||
editor = preferredEditor;
|
|
||||||
} else {
|
|
||||||
// Platform-specific defaults with UI preference for macOS
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'darwin':
|
|
||||||
editor = 'open -t'; // TextEdit in plain text mode
|
|
||||||
break;
|
|
||||||
case 'win32':
|
|
||||||
editor = 'notepad';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
editor = process.env['VISUAL'] || process.env['EDITOR'] || 'vi';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch editor with the actual subagent file
|
// Launch editor with the actual subagent file
|
||||||
const wasRaw = stdin?.isRaw ?? false;
|
await launchEditor(subagentFilePath);
|
||||||
try {
|
|
||||||
setRawMode?.(false);
|
|
||||||
|
|
||||||
// Handle different editor command formats
|
|
||||||
let editorCommand: string;
|
|
||||||
let editorArgs: string[];
|
|
||||||
|
|
||||||
if (editor === 'open -t') {
|
|
||||||
// macOS TextEdit in plain text mode
|
|
||||||
editorCommand = 'open';
|
|
||||||
editorArgs = ['-t', subagentFilePath];
|
|
||||||
} else {
|
|
||||||
// Standard editor command
|
|
||||||
editorCommand = editor;
|
|
||||||
editorArgs = [subagentFilePath];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, error } = spawnSync(editorCommand, editorArgs, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
if (typeof status === 'number' && status !== 0) {
|
|
||||||
throw new Error(`Editor exited with status ${status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show success UI and auto-close after successful edit
|
// Show success UI and auto-close after successful edit
|
||||||
showSuccessAndClose();
|
showSuccessAndClose();
|
||||||
} finally {
|
|
||||||
if (wasRaw) setRawMode?.(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveError(
|
setSaveError(
|
||||||
`Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
`Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
@@ -256,9 +185,7 @@ export function CreationSummary({
|
|||||||
showSuccessAndClose,
|
showSuccessAndClose,
|
||||||
state.generatedName,
|
state.generatedName,
|
||||||
state.location,
|
state.location,
|
||||||
settings.merged.preferredEditor,
|
launchEditor,
|
||||||
stdin,
|
|
||||||
setRawMode,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handle keyboard input
|
// Handle keyboard input
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { useState, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { WizardStepProps, WizardAction } from './types.js';
|
import { WizardStepProps, WizardAction } from './types.js';
|
||||||
import { sanitizeInput } from './validation.js';
|
import { sanitizeInput } from './utils.js';
|
||||||
import { Config, subagentGenerator } from '@qwen-code/qwen-code-core';
|
import { Config, subagentGenerator } from '@qwen-code/qwen-code-core';
|
||||||
import { useTextBuffer } from '../shared/text-buffer.js';
|
import { useTextBuffer } from '../shared/text-buffer.js';
|
||||||
import { useKeypress, Key } from '../../hooks/useKeypress.js';
|
import { useKeypress, Key } from '../../hooks/useKeypress.js';
|
||||||
|
|||||||
@@ -194,9 +194,27 @@ export function SubagentCreationWizard({
|
|||||||
case WIZARD_STEPS.DESCRIPTION_INPUT:
|
case WIZARD_STEPS.DESCRIPTION_INPUT:
|
||||||
return <DescriptionInput {...stepProps} />;
|
return <DescriptionInput {...stepProps} />;
|
||||||
case WIZARD_STEPS.TOOL_SELECTION:
|
case WIZARD_STEPS.TOOL_SELECTION:
|
||||||
return <ToolSelector {...stepProps} />;
|
return (
|
||||||
|
<ToolSelector
|
||||||
|
tools={state.selectedTools}
|
||||||
|
onSelect={(tools) => {
|
||||||
|
dispatch({ type: 'SET_TOOLS', tools });
|
||||||
|
handleNext();
|
||||||
|
}}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case WIZARD_STEPS.COLOR_SELECTION:
|
case WIZARD_STEPS.COLOR_SELECTION:
|
||||||
return <ColorSelector {...stepProps} />;
|
return (
|
||||||
|
<ColorSelector
|
||||||
|
backgroundColor={state.backgroundColor}
|
||||||
|
agentName={state.generatedName}
|
||||||
|
onSelect={(color) => {
|
||||||
|
dispatch({ type: 'SET_BACKGROUND_COLOR', color });
|
||||||
|
handleNext();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case WIZARD_STEPS.FINAL_CONFIRMATION:
|
case WIZARD_STEPS.FINAL_CONFIRMATION:
|
||||||
return <CreationSummary {...stepProps} />;
|
return <CreationSummary {...stepProps} />;
|
||||||
default:
|
default:
|
||||||
@@ -208,7 +226,16 @@ export function SubagentCreationWizard({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [stepProps, state.currentStep]);
|
}, [
|
||||||
|
stepProps,
|
||||||
|
state.currentStep,
|
||||||
|
state.selectedTools,
|
||||||
|
state.backgroundColor,
|
||||||
|
state.generatedName,
|
||||||
|
config,
|
||||||
|
handleNext,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||||
import { WizardStepProps, ToolCategory } from './types.js';
|
import { ToolCategory } from './types.js';
|
||||||
import { Kind } from '@qwen-code/qwen-code-core';
|
import { Kind, Config } from '@qwen-code/qwen-code-core';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../colors.js';
|
||||||
|
|
||||||
interface ToolOption {
|
interface ToolOption {
|
||||||
@@ -17,20 +17,28 @@ interface ToolOption {
|
|||||||
category: ToolCategory;
|
category: ToolCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ToolSelectorProps {
|
||||||
|
tools?: string[];
|
||||||
|
onSelect: (tools: string[]) => void;
|
||||||
|
config: Config | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 4: Tool selection with categories.
|
* Tool selection with categories.
|
||||||
*/
|
*/
|
||||||
export function ToolSelector({
|
export function ToolSelector({
|
||||||
state: _state,
|
tools = [],
|
||||||
dispatch,
|
onSelect,
|
||||||
onNext,
|
|
||||||
onPrevious: _onPrevious,
|
|
||||||
config,
|
config,
|
||||||
}: WizardStepProps) {
|
}: ToolSelectorProps) {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
|
||||||
|
|
||||||
// Generate tool categories from actual tool registry
|
// Generate tool categories from actual tool registry
|
||||||
const { toolCategories, readTools, editTools, executeTools } = useMemo(() => {
|
const {
|
||||||
|
toolCategories,
|
||||||
|
readTools,
|
||||||
|
editTools,
|
||||||
|
executeTools,
|
||||||
|
initialCategory,
|
||||||
|
} = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
// Fallback categories if config not available
|
// Fallback categories if config not available
|
||||||
return {
|
return {
|
||||||
@@ -44,6 +52,7 @@ export function ToolSelector({
|
|||||||
readTools: [],
|
readTools: [],
|
||||||
editTools: [],
|
editTools: [],
|
||||||
executeTools: [],
|
executeTools: [],
|
||||||
|
initialCategory: 'all',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +109,49 @@ export function ToolSelector({
|
|||||||
},
|
},
|
||||||
].filter((category) => category.id === 'all' || category.tools.length > 0);
|
].filter((category) => category.id === 'all' || category.tools.length > 0);
|
||||||
|
|
||||||
return { toolCategories, readTools, editTools, executeTools };
|
// Determine initial category based on tools prop
|
||||||
}, [config]);
|
let initialCategory = 'all'; // default to first option
|
||||||
|
|
||||||
|
if (tools.length === 0) {
|
||||||
|
// Empty array represents all tools
|
||||||
|
initialCategory = 'all';
|
||||||
|
} else {
|
||||||
|
// Try to match tools array to a category
|
||||||
|
const matchingCategory = toolCategories.find((category) => {
|
||||||
|
if (category.id === 'all') return false;
|
||||||
|
|
||||||
|
// Check if the tools array exactly matches this category's tools
|
||||||
|
const categoryToolsSet = new Set(category.tools);
|
||||||
|
const inputToolsSet = new Set(tools);
|
||||||
|
|
||||||
|
return (
|
||||||
|
categoryToolsSet.size === inputToolsSet.size &&
|
||||||
|
[...categoryToolsSet].every((tool) => inputToolsSet.has(tool))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingCategory) {
|
||||||
|
initialCategory = matchingCategory.id;
|
||||||
|
}
|
||||||
|
// If no exact match found, keep default 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolCategories,
|
||||||
|
readTools,
|
||||||
|
editTools,
|
||||||
|
executeTools,
|
||||||
|
initialCategory,
|
||||||
|
};
|
||||||
|
}, [config, tools]);
|
||||||
|
|
||||||
|
const [selectedCategory, setSelectedCategory] =
|
||||||
|
useState<string>(initialCategory);
|
||||||
|
|
||||||
|
// Update selected category when initialCategory changes (when tools prop changes)
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedCategory(initialCategory);
|
||||||
|
}, [initialCategory]);
|
||||||
|
|
||||||
const toolOptions: ToolOption[] = toolCategories.map((category) => ({
|
const toolOptions: ToolOption[] = toolCategories.map((category) => ({
|
||||||
label: category.name,
|
label: category.name,
|
||||||
@@ -117,11 +167,10 @@ export function ToolSelector({
|
|||||||
const category = toolCategories.find((cat) => cat.id === selectedValue);
|
const category = toolCategories.find((cat) => cat.id === selectedValue);
|
||||||
if (category) {
|
if (category) {
|
||||||
if (category.id === 'all') {
|
if (category.id === 'all') {
|
||||||
dispatch({ type: 'SET_TOOLS', tools: 'all' });
|
onSelect([]); // Empty array for 'all'
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: 'SET_TOOLS', tools: category.tools });
|
onSelect(category.tools);
|
||||||
}
|
}
|
||||||
onNext();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export { AgentsManagerDialog } from './AgentsManagerDialog.js';
|
|||||||
export { AgentSelectionStep } from './AgentSelectionStep.js';
|
export { AgentSelectionStep } from './AgentSelectionStep.js';
|
||||||
export { ActionSelectionStep } from './ActionSelectionStep.js';
|
export { ActionSelectionStep } from './ActionSelectionStep.js';
|
||||||
export { AgentViewerStep } from './AgentViewerStep.js';
|
export { AgentViewerStep } from './AgentViewerStep.js';
|
||||||
|
export { AgentDeleteStep } from './AgentDeleteStep.js';
|
||||||
|
|
||||||
// Creation Wizard Types and State
|
// Creation Wizard Types and State
|
||||||
export type {
|
export type {
|
||||||
@@ -30,16 +31,3 @@ export type {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
export { wizardReducer, initialWizardState } from './reducers.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';
|
|
||||||
|
|||||||
@@ -4,17 +4,9 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { CreationWizardState, WizardAction } from './types.js';
|
||||||
CreationWizardState,
|
|
||||||
WizardAction,
|
|
||||||
ManagementDialogState,
|
|
||||||
ManagementAction,
|
|
||||||
MANAGEMENT_STEPS,
|
|
||||||
} from './types.js';
|
|
||||||
import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js';
|
import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js';
|
||||||
|
|
||||||
export { MANAGEMENT_STEPS };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial state for the creation wizard.
|
* Initial state for the creation wizard.
|
||||||
*/
|
*/
|
||||||
@@ -26,7 +18,7 @@ export const initialWizardState: CreationWizardState = {
|
|||||||
generatedSystemPrompt: '',
|
generatedSystemPrompt: '',
|
||||||
generatedDescription: '',
|
generatedDescription: '',
|
||||||
generatedName: '',
|
generatedName: '',
|
||||||
selectedTools: 'all',
|
selectedTools: [],
|
||||||
backgroundColor: 'auto',
|
backgroundColor: 'auto',
|
||||||
isGenerating: false,
|
isGenerating: false,
|
||||||
validationErrors: [],
|
validationErrors: [],
|
||||||
@@ -171,124 +163,3 @@ function validateStep(step: number, state: CreationWizardState): boolean {
|
|||||||
return false;
|
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,12 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { SubagentLevel, Config } from '@qwen-code/qwen-code-core';
|
||||||
SubagentLevel,
|
|
||||||
SubagentConfig,
|
|
||||||
SubagentMetadata,
|
|
||||||
Config,
|
|
||||||
} from '@qwen-code/qwen-code-core';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State management for the subagent creation wizard.
|
* State management for the subagent creation wizard.
|
||||||
@@ -37,7 +32,7 @@ export interface CreationWizardState {
|
|||||||
generatedName: string;
|
generatedName: string;
|
||||||
|
|
||||||
/** Selected tools for the subagent */
|
/** Selected tools for the subagent */
|
||||||
selectedTools: string[] | 'all';
|
selectedTools: string[];
|
||||||
|
|
||||||
/** Background color for runtime display */
|
/** Background color for runtime display */
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
@@ -84,7 +79,7 @@ export type WizardAction =
|
|||||||
description: string;
|
description: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
}
|
}
|
||||||
| { type: 'SET_TOOLS'; tools: string[] | 'all' }
|
| { type: 'SET_TOOLS'; tools: string[] }
|
||||||
| { type: 'SET_BACKGROUND_COLOR'; color: string }
|
| { type: 'SET_BACKGROUND_COLOR'; color: string }
|
||||||
| { type: 'SET_GENERATING'; isGenerating: boolean }
|
| { type: 'SET_GENERATING'; isGenerating: boolean }
|
||||||
| { type: 'SET_VALIDATION_ERRORS'; errors: string[] }
|
| { type: 'SET_VALIDATION_ERRORS'; errors: string[] }
|
||||||
@@ -116,54 +111,20 @@ export interface WizardResult {
|
|||||||
backgroundColor: 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 = {
|
export const MANAGEMENT_STEPS = {
|
||||||
AGENT_SELECTION: 1,
|
AGENT_SELECTION: 'agent-selection',
|
||||||
ACTION_SELECTION: 2,
|
ACTION_SELECTION: 'action-selection',
|
||||||
AGENT_VIEWER: 3,
|
AGENT_VIEWER: 'agent-viewer',
|
||||||
AGENT_EDITOR: 4,
|
EDIT_OPTIONS: 'edit-options',
|
||||||
DELETE_CONFIRMATION: 5,
|
EDIT_TOOLS: 'edit-tools',
|
||||||
|
EDIT_COLOR: 'edit-color',
|
||||||
|
DELETE_CONFIRMATION: 'delete-confirmation',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common props for step navigation.
|
||||||
|
*/
|
||||||
|
export interface StepNavigationProps {
|
||||||
|
onNavigateToStep: (step: string) => void;
|
||||||
|
onNavigateBack: () => void;
|
||||||
|
}
|
||||||
|
|||||||
82
packages/cli/src/ui/components/subagents/useLaunchEditor.ts
Normal file
82
packages/cli/src/ui/components/subagents/useLaunchEditor.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useStdin } from 'ink';
|
||||||
|
import { EditorType } from '@qwen-code/qwen-code-core';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the editor command to use based on user preferences and platform.
|
||||||
|
*/
|
||||||
|
function getEditorCommand(preferredEditor?: EditorType): string {
|
||||||
|
if (preferredEditor) {
|
||||||
|
return preferredEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-specific defaults with UI preference for macOS
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return 'open -t'; // TextEdit in plain text mode
|
||||||
|
case 'win32':
|
||||||
|
return 'notepad';
|
||||||
|
default:
|
||||||
|
return process.env['VISUAL'] || process.env['EDITOR'] || 'vi';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook that provides an editor launcher function.
|
||||||
|
* Uses settings context and stdin management internally.
|
||||||
|
*/
|
||||||
|
export function useLaunchEditor() {
|
||||||
|
const settings = useSettings();
|
||||||
|
const { stdin, setRawMode } = useStdin();
|
||||||
|
|
||||||
|
const launchEditor = useCallback(
|
||||||
|
async (filePath: string): Promise<void> => {
|
||||||
|
const preferredEditor = settings.merged.preferredEditor as
|
||||||
|
| EditorType
|
||||||
|
| undefined;
|
||||||
|
const editor = getEditorCommand(preferredEditor);
|
||||||
|
|
||||||
|
// Handle different editor command formats
|
||||||
|
let editorCommand: string;
|
||||||
|
let editorArgs: string[];
|
||||||
|
|
||||||
|
if (editor === 'open -t') {
|
||||||
|
// macOS TextEdit in plain text mode
|
||||||
|
editorCommand = 'open';
|
||||||
|
editorArgs = ['-t', filePath];
|
||||||
|
} else {
|
||||||
|
// Standard editor command
|
||||||
|
editorCommand = editor;
|
||||||
|
editorArgs = [filePath];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily disable raw mode for editor
|
||||||
|
const wasRaw = stdin?.isRaw ?? false;
|
||||||
|
try {
|
||||||
|
setRawMode?.(false);
|
||||||
|
|
||||||
|
const { status, error } = spawnSync(editorCommand, editorArgs, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
if (typeof status === 'number' && status !== 0) {
|
||||||
|
throw new Error(`Editor exited with status ${status}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (wasRaw) setRawMode?.(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[settings.merged.preferredEditor, setRawMode, stdin],
|
||||||
|
);
|
||||||
|
|
||||||
|
return launchEditor;
|
||||||
|
}
|
||||||
@@ -7,3 +7,16 @@ export const getColorForDisplay = (colorName?: string): string | undefined =>
|
|||||||
!colorName || colorName === 'auto'
|
!colorName || colorName === 'auto'
|
||||||
? undefined
|
? undefined
|
||||||
: COLOR_OPTIONS.find((color) => color.name === colorName)?.value;
|
: COLOR_OPTIONS.find((color) => color.name === colorName)?.value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes user input by removing dangerous characters and normalizing whitespace.
|
||||||
|
*/
|
||||||
|
export function sanitizeInput(input: string): string {
|
||||||
|
return (
|
||||||
|
input
|
||||||
|
.trim()
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters
|
||||||
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||||
|
); // Limit length
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validation result interface.
|
|
||||||
*/
|
|
||||||
export interface ValidationResult {
|
|
||||||
isValid: boolean;
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes user input by removing dangerous characters and normalizing whitespace.
|
|
||||||
*/
|
|
||||||
export function sanitizeInput(input: string): string {
|
|
||||||
return (
|
|
||||||
input
|
|
||||||
.trim()
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control characters
|
|
||||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
||||||
); // Limit length
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a system prompt.
|
|
||||||
*/
|
|
||||||
export function validateSystemPrompt(prompt: string): ValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
const sanitized = sanitizeInput(prompt);
|
|
||||||
|
|
||||||
if (sanitized.length === 0) {
|
|
||||||
errors.push('System prompt cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sanitized.length > 5000) {
|
|
||||||
errors.push('System prompt is too long (maximum 5000 characters)');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: errors.length === 0,
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates tool selection.
|
|
||||||
*/
|
|
||||||
export function validateToolSelection(
|
|
||||||
tools: string[] | 'all',
|
|
||||||
): ValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (Array.isArray(tools)) {
|
|
||||||
if (tools.length === 0) {
|
|
||||||
errors.push('At least one tool must be selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for valid tool names (basic validation)
|
|
||||||
const invalidTools = tools.filter(
|
|
||||||
(tool) =>
|
|
||||||
typeof tool !== 'string' ||
|
|
||||||
tool.trim().length === 0 ||
|
|
||||||
!/^[a-zA-Z0-9_-]+$/.test(tool),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (invalidTools.length > 0) {
|
|
||||||
errors.push(`Invalid tool names: ${invalidTools.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: errors.length === 0,
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comprehensive validation for the entire subagent configuration.
|
|
||||||
*/
|
|
||||||
export function validateSubagentConfig(config: {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
systemPrompt: string;
|
|
||||||
tools: string[] | 'all';
|
|
||||||
}): ValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
const promptValidation = validateSystemPrompt(config.systemPrompt);
|
|
||||||
if (!promptValidation.isValid) {
|
|
||||||
errors.push(...promptValidation.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolsValidation = validateToolSelection(config.tools);
|
|
||||||
if (!toolsValidation.isValid) {
|
|
||||||
errors.push(...toolsValidation.errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: errors.length === 0,
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
// Core types and interfaces
|
// Core types and interfaces
|
||||||
export type {
|
export type {
|
||||||
SubagentConfig,
|
SubagentConfig,
|
||||||
SubagentMetadata,
|
|
||||||
SubagentLevel,
|
SubagentLevel,
|
||||||
SubagentRuntimeConfig,
|
SubagentRuntimeConfig,
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
SubagentConfig,
|
SubagentConfig,
|
||||||
SubagentRuntimeConfig,
|
SubagentRuntimeConfig,
|
||||||
SubagentMetadata,
|
|
||||||
SubagentLevel,
|
SubagentLevel,
|
||||||
ListSubagentsOptions,
|
ListSubagentsOptions,
|
||||||
CreateSubagentOptions,
|
CreateSubagentOptions,
|
||||||
@@ -235,8 +234,8 @@ export class SubagentManager {
|
|||||||
*/
|
*/
|
||||||
async listSubagents(
|
async listSubagents(
|
||||||
options: ListSubagentsOptions = {},
|
options: ListSubagentsOptions = {},
|
||||||
): Promise<SubagentMetadata[]> {
|
): Promise<SubagentConfig[]> {
|
||||||
const subagents: SubagentMetadata[] = [];
|
const subagents: SubagentConfig[] = [];
|
||||||
const seenNames = new Set<string>();
|
const seenNames = new Set<string>();
|
||||||
|
|
||||||
const levelsToCheck: SubagentLevel[] = options.level
|
const levelsToCheck: SubagentLevel[] = options.level
|
||||||
@@ -275,12 +274,6 @@ export class SubagentManager {
|
|||||||
case 'name':
|
case 'name':
|
||||||
comparison = a.name.localeCompare(b.name);
|
comparison = a.name.localeCompare(b.name);
|
||||||
break;
|
break;
|
||||||
case 'lastModified': {
|
|
||||||
const aTime = a.lastModified?.getTime() || 0;
|
|
||||||
const bTime = b.lastModified?.getTime() || 0;
|
|
||||||
comparison = aTime - bTime;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'level':
|
case 'level':
|
||||||
// Project comes before user
|
// Project comes before user
|
||||||
comparison =
|
comparison =
|
||||||
@@ -302,18 +295,18 @@ export class SubagentManager {
|
|||||||
* Finds a subagent by name and returns its metadata.
|
* Finds a subagent by name and returns its metadata.
|
||||||
*
|
*
|
||||||
* @param name - Name of the subagent to find
|
* @param name - Name of the subagent to find
|
||||||
* @returns SubagentMetadata or null if not found
|
* @returns SubagentConfig or null if not found
|
||||||
*/
|
*/
|
||||||
async findSubagentByName(
|
async findSubagentByName(
|
||||||
name: string,
|
name: string,
|
||||||
level?: SubagentLevel,
|
level?: SubagentLevel,
|
||||||
): Promise<SubagentMetadata | null> {
|
): Promise<SubagentConfig | null> {
|
||||||
const config = await this.loadSubagent(name, level);
|
const config = await this.loadSubagent(name, level);
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.configToMetadata(config);
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -584,7 +577,7 @@ export class SubagentManager {
|
|||||||
*/
|
*/
|
||||||
private async listSubagentsAtLevel(
|
private async listSubagentsAtLevel(
|
||||||
level: SubagentLevel,
|
level: SubagentLevel,
|
||||||
): Promise<SubagentMetadata[]> {
|
): Promise<SubagentConfig[]> {
|
||||||
const baseDir =
|
const baseDir =
|
||||||
level === 'project'
|
level === 'project'
|
||||||
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
||||||
@@ -592,7 +585,7 @@ export class SubagentManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(baseDir);
|
const files = await fs.readdir(baseDir);
|
||||||
const subagents: SubagentMetadata[] = [];
|
const subagents: SubagentConfig[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith('.md')) continue;
|
if (!file.endsWith('.md')) continue;
|
||||||
@@ -601,8 +594,7 @@ export class SubagentManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const config = await this.parseSubagentFile(filePath);
|
const config = await this.parseSubagentFile(filePath);
|
||||||
const metadata = this.configToMetadata(config);
|
subagents.push(config);
|
||||||
subagents.push(metadata);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Skip invalid files but log the error
|
// Skip invalid files but log the error
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -618,24 +610,6 @@ export class SubagentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a SubagentConfig to SubagentMetadata.
|
|
||||||
*
|
|
||||||
* @param config - Full configuration
|
|
||||||
* @returns Metadata object
|
|
||||||
*/
|
|
||||||
private configToMetadata(config: SubagentConfig): SubagentMetadata {
|
|
||||||
return {
|
|
||||||
name: config.name,
|
|
||||||
description: config.description,
|
|
||||||
tools: config.tools,
|
|
||||||
level: config.level,
|
|
||||||
filePath: config.filePath,
|
|
||||||
// Add file stats if available
|
|
||||||
lastModified: undefined, // Would need to stat the file
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that a subagent name is available (not already in use).
|
* Validates that a subagent name is available (not already in use).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -67,33 +67,6 @@ export interface SubagentConfig {
|
|||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata extracted from a subagent configuration file.
|
|
||||||
* Used for listing and discovery without loading full configuration.
|
|
||||||
*/
|
|
||||||
export interface SubagentMetadata {
|
|
||||||
/** Unique name identifier */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** Human-readable description */
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
/** Optional list of allowed tools */
|
|
||||||
tools?: string[];
|
|
||||||
|
|
||||||
/** Storage level */
|
|
||||||
level: SubagentLevel;
|
|
||||||
|
|
||||||
/** File path */
|
|
||||||
filePath: string;
|
|
||||||
|
|
||||||
/** File modification time for sorting */
|
|
||||||
lastModified?: Date;
|
|
||||||
|
|
||||||
/** Additional metadata from YAML frontmatter */
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime configuration that converts file-based config to existing SubAgentScope.
|
* Runtime configuration that converts file-based config to existing SubAgentScope.
|
||||||
* This interface maps SubagentConfig to the existing runtime interfaces.
|
* This interface maps SubagentConfig to the existing runtime interfaces.
|
||||||
|
|||||||
Reference in New Issue
Block a user