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
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Box } from 'ink';
|
||||
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 = ({
|
||||
state,
|
||||
dispatch,
|
||||
onNext,
|
||||
onPrevious,
|
||||
}: ManagementStepProps) => {
|
||||
onNavigateToStep,
|
||||
onNavigateBack,
|
||||
}: ActionSelectionStepProps) => {
|
||||
const [selectedAction, setSelectedAction] = useState<
|
||||
'view' | 'edit' | 'delete' | null
|
||||
>(null);
|
||||
const actions = [
|
||||
{ label: 'View Agent', value: 'view' as const },
|
||||
{ label: 'Edit Agent', value: 'edit' as const },
|
||||
@@ -23,16 +30,24 @@ export const ActionSelectionStep = ({
|
||||
|
||||
const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => {
|
||||
if (value === 'back') {
|
||||
onPrevious();
|
||||
onNavigateBack();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'SELECT_ACTION', payload: value });
|
||||
onNext();
|
||||
setSelectedAction(value);
|
||||
|
||||
// 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
|
||||
? actions.findIndex((action) => action.value === state.selectedAction)
|
||||
const selectedIndex = selectedAction
|
||||
? actions.findIndex((action) => action.value === selectedAction)
|
||||
: 0;
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ManagementStepProps } from './types.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
interface NavigationState {
|
||||
currentBlock: 'project' | 'user';
|
||||
@@ -17,13 +17,15 @@ interface NavigationState {
|
||||
userIndex: number;
|
||||
}
|
||||
|
||||
interface AgentSelectionStepProps {
|
||||
availableAgents: SubagentConfig[];
|
||||
onAgentSelect: (agentIndex: number) => void;
|
||||
}
|
||||
|
||||
export const AgentSelectionStep = ({
|
||||
state,
|
||||
dispatch,
|
||||
onNext,
|
||||
config,
|
||||
}: ManagementStepProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
availableAgents,
|
||||
onAgentSelect,
|
||||
}: AgentSelectionStepProps) => {
|
||||
const [navigation, setNavigation] = useState<NavigationState>({
|
||||
currentBlock: 'project',
|
||||
projectIndex: 0,
|
||||
@@ -31,60 +33,27 @@ export const AgentSelectionStep = ({
|
||||
});
|
||||
|
||||
// Group agents by level
|
||||
const projectAgents = state.availableAgents.filter(
|
||||
(agent) => agent.level === 'project',
|
||||
const projectAgents = useMemo(
|
||||
() => availableAgents.filter((agent) => agent.level === 'project'),
|
||||
[availableAgents],
|
||||
);
|
||||
const userAgents = state.availableAgents.filter(
|
||||
(agent) => agent.level === 'user',
|
||||
const userAgents = useMemo(
|
||||
() => 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(() => {
|
||||
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
|
||||
// Initialize navigation state when agents are loaded (only once)
|
||||
useEffect(() => {
|
||||
if (projectAgents.length > 0) {
|
||||
setNavigation((prev) => ({ ...prev, currentBlock: 'project' }));
|
||||
} else if (userAgents.length > 0) {
|
||||
setNavigation((prev) => ({ ...prev, currentBlock: 'user' }));
|
||||
}
|
||||
}, [projectAgents.length, userAgents.length]);
|
||||
}, [projectAgents, userAgents]);
|
||||
|
||||
// Custom keyboard navigation
|
||||
useKeypress(
|
||||
@@ -148,71 +117,24 @@ export const AgentSelectionStep = ({
|
||||
}
|
||||
});
|
||||
} else if (name === 'return' || name === 'space') {
|
||||
// Select current item
|
||||
const currentAgent =
|
||||
navigation.currentBlock === 'project'
|
||||
? projectAgents[navigation.projectIndex]
|
||||
: userAgents[navigation.userIndex];
|
||||
// Calculate global index and select current item
|
||||
let globalIndex: number;
|
||||
if (navigation.currentBlock === 'project') {
|
||||
globalIndex = navigation.projectIndex;
|
||||
} else {
|
||||
// User agents come after project agents in the availableAgents array
|
||||
globalIndex = projectAgents.length + navigation.userIndex;
|
||||
}
|
||||
|
||||
if (currentAgent) {
|
||||
const agentIndex = state.availableAgents.indexOf(currentAgent);
|
||||
handleAgentSelect(agentIndex);
|
||||
if (globalIndex >= 0 && globalIndex < availableAgents.length) {
|
||||
onAgentSelect(globalIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const handleAgentSelect = async (index: number) => {
|
||||
const selectedMetadata = state.availableAgents[index];
|
||||
if (!selectedMetadata) return;
|
||||
|
||||
try {
|
||||
if (!config) {
|
||||
throw new Error('Configuration not available');
|
||||
}
|
||||
const manager = config.getSubagentManager();
|
||||
const agent = await manager.loadSubagent(
|
||||
selectedMetadata.name,
|
||||
selectedMetadata.level,
|
||||
);
|
||||
|
||||
if (agent) {
|
||||
dispatch({ type: 'SELECT_AGENT', payload: { agent, index } });
|
||||
onNext();
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: `Failed to load agent: ${selectedMetadata.name}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: `Failed to load agent: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.secondary}>Loading agents...</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>{state.error}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.availableAgents.length === 0) {
|
||||
if (availableAgents.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>No subagents found.</Text>
|
||||
|
||||
@@ -4,20 +4,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { ManagementStepProps } from './types.js';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export const AgentViewerStep = ({ state, onPrevious }: ManagementStepProps) => {
|
||||
// Handle keyboard input
|
||||
useInput((input, key) => {
|
||||
if (key.escape || input === 'b') {
|
||||
onPrevious();
|
||||
}
|
||||
});
|
||||
interface AgentViewerStepProps {
|
||||
selectedAgent: SubagentConfig | null;
|
||||
}
|
||||
|
||||
if (!state.selectedAgent) {
|
||||
export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
|
||||
if (!selectedAgent) {
|
||||
return (
|
||||
<Box>
|
||||
<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(', ') : '*';
|
||||
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
* 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 { managementReducer, initialManagementState } from './reducers.js';
|
||||
import { AgentSelectionStep } from './AgentSelectionStep.js';
|
||||
import { ActionSelectionStep } from './ActionSelectionStep.js';
|
||||
import { AgentViewerStep } from './AgentViewerStep.js';
|
||||
import { ManagementStepProps, MANAGEMENT_STEPS } from './types.js';
|
||||
import { 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 { theme } from '../../semantic-colors.js';
|
||||
import { Config } from '@qwen-code/qwen-code-core';
|
||||
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
interface AgentsManagerDialogProps {
|
||||
onClose: () => void;
|
||||
@@ -27,67 +30,132 @@ export function AgentsManagerDialog({
|
||||
onClose,
|
||||
config,
|
||||
}: AgentsManagerDialogProps) {
|
||||
const [state, dispatch] = useReducer(
|
||||
managementReducer,
|
||||
initialManagementState,
|
||||
// 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],
|
||||
);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
dispatch({ type: 'GO_TO_NEXT_STEP' });
|
||||
// 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],
|
||||
);
|
||||
|
||||
const handleSelectAgent = useCallback((agentIndex: number) => {
|
||||
setSelectedAgentIndex(agentIndex);
|
||||
setNavigationStack((prev) => [...prev, MANAGEMENT_STEPS.ACTION_SELECTION]);
|
||||
}, []);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
dispatch({ type: 'GO_TO_PREVIOUS_STEP' });
|
||||
const handleNavigateToStep = useCallback((step: string) => {
|
||||
setNavigationStack((prev) => [...prev, step]);
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dispatch({ type: 'RESET_DIALOG' });
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
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],
|
||||
);
|
||||
|
||||
// Centralized ESC key handling for the entire dialog
|
||||
useInput((input, key) => {
|
||||
if (key.escape) {
|
||||
// Agent viewer step handles its own ESC logic
|
||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) {
|
||||
return; // Let AgentViewerStep handle it
|
||||
}
|
||||
|
||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
const currentStep = getCurrentStep();
|
||||
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
// On first step, ESC cancels the entire dialog
|
||||
handleCancel();
|
||||
onClose();
|
||||
} else {
|
||||
// On other steps, ESC goes back to previous step
|
||||
handlePrevious();
|
||||
// On other steps, ESC goes back to previous step in navigation stack
|
||||
handleNavigateBack();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const stepProps: ManagementStepProps = useMemo(
|
||||
// Props for child components - now using direct state and callbacks
|
||||
const commonProps = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
config,
|
||||
dispatch,
|
||||
onNext: handleNext,
|
||||
onPrevious: handlePrevious,
|
||||
onCancel: handleCancel,
|
||||
onNavigateToStep: handleNavigateToStep,
|
||||
onNavigateBack: handleNavigateBack,
|
||||
}),
|
||||
[state, dispatch, handleNext, handlePrevious, handleCancel, config],
|
||||
[handleNavigateToStep, handleNavigateBack],
|
||||
);
|
||||
|
||||
const renderStepHeader = useCallback(() => {
|
||||
const currentStep = getCurrentStep();
|
||||
const getStepHeaderText = () => {
|
||||
switch (state.currentStep) {
|
||||
switch (currentStep) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return 'Agents';
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return 'Choose Action';
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return state.selectedAgent?.name;
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return `Editing: ${state.selectedAgent?.name || 'Unknown'}`;
|
||||
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}`;
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return `Delete: ${state.selectedAgent?.name || 'Unknown'}`;
|
||||
return `Delete ${selectedAgent?.name}`;
|
||||
default:
|
||||
return 'Unknown Step';
|
||||
}
|
||||
@@ -98,22 +166,27 @@ export function AgentsManagerDialog({
|
||||
<Text bold>{getStepHeaderText()}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [state.currentStep, state.selectedAgent?.name]);
|
||||
}, [getCurrentStep, selectedAgent]);
|
||||
|
||||
const renderStepFooter = useCallback(() => {
|
||||
const currentStep = getCurrentStep();
|
||||
const getNavigationInstructions = () => {
|
||||
if (state.currentStep === MANAGEMENT_STEPS.ACTION_SELECTION) {
|
||||
return 'Enter to select, ↑↓ to navigate, Esc to go back';
|
||||
}
|
||||
|
||||
if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
if (state.availableAgents.length === 0) {
|
||||
if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) {
|
||||
if (availableAgents.length === 0) {
|
||||
return 'Esc to close';
|
||||
}
|
||||
return 'Enter to select, ↑↓ to navigate, Esc to close';
|
||||
}
|
||||
|
||||
return 'Esc to go back';
|
||||
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';
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -121,42 +194,110 @@ export function AgentsManagerDialog({
|
||||
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [state.currentStep, state.availableAgents.length]);
|
||||
}, [getCurrentStep, availableAgents]);
|
||||
|
||||
const renderStepContent = useCallback(() => {
|
||||
switch (state.currentStep) {
|
||||
const currentStep = getCurrentStep();
|
||||
switch (currentStep) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return <AgentSelectionStep {...stepProps} />;
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return <ActionSelectionStep {...stepProps} />;
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return <AgentViewerStep {...stepProps} />;
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.warning}>
|
||||
Agent editing not yet implemented
|
||||
</Text>
|
||||
<AgentSelectionStep
|
||||
availableAgents={availableAgents}
|
||||
onAgentSelect={handleSelectAgent}
|
||||
{...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>
|
||||
);
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.warning}>
|
||||
Agent deletion not yet implemented
|
||||
</Text>
|
||||
</Box>
|
||||
<AgentDeleteStep
|
||||
selectedAgent={selectedAgent}
|
||||
onDelete={handleDeleteAgent}
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.status.error}>
|
||||
Invalid step: {state.currentStep}
|
||||
</Text>
|
||||
<Text color={theme.status.error}>Invalid step: {currentStep}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}, [stepProps, state.currentStep]);
|
||||
}, [
|
||||
getCurrentStep,
|
||||
availableAgents,
|
||||
selectedAgent,
|
||||
commonProps,
|
||||
config,
|
||||
loadAgents,
|
||||
handleNavigateBack,
|
||||
handleSelectAgent,
|
||||
handleDeleteAgent,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
||||
@@ -4,25 +4,43 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { WizardStepProps, ColorOption } from './types.js';
|
||||
import { ColorOption } from './types.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { COLOR_OPTIONS } from './constants.js';
|
||||
|
||||
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({
|
||||
state,
|
||||
dispatch,
|
||||
onNext,
|
||||
onPrevious: _onPrevious,
|
||||
}: WizardStepProps) {
|
||||
const handleSelect = (_selectedValue: string) => {
|
||||
onNext();
|
||||
backgroundColor = 'auto',
|
||||
agentName = 'Agent',
|
||||
onSelect,
|
||||
}: ColorSelectorProps) {
|
||||
const [selectedColor, setSelectedColor] = useState<string>(backgroundColor);
|
||||
|
||||
// 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) => {
|
||||
@@ -30,12 +48,12 @@ export function ColorSelector({
|
||||
(option) => option.id === selectedValue,
|
||||
);
|
||||
if (colorOption) {
|
||||
dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.name });
|
||||
setSelectedColor(colorOption.name);
|
||||
}
|
||||
};
|
||||
|
||||
const currentColor =
|
||||
colorOptions.find((option) => option.name === state.backgroundColor) ||
|
||||
colorOptions.find((option) => option.name === selectedColor) ||
|
||||
colorOptions[0];
|
||||
|
||||
return (
|
||||
@@ -58,7 +76,7 @@ export function ColorSelector({
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.Gray}>Preview:</Text>
|
||||
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
||||
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||
<Text color="black">{` ${agentName} `}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -5,18 +5,12 @@
|
||||
*/
|
||||
|
||||
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 { validateSubagentConfig } from './validation.js';
|
||||
import {
|
||||
SubagentManager,
|
||||
SubagentConfig,
|
||||
EditorType,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { SubagentManager, SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||
import { useLaunchEditor } from './useLaunchEditor.js';
|
||||
|
||||
/**
|
||||
* Step 6: Final confirmation and actions.
|
||||
@@ -31,8 +25,7 @@ export function CreationSummary({
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [warnings, setWarnings] = useState<string[]>([]);
|
||||
|
||||
const settings = useSettings();
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const launchEditor = useLaunchEditor();
|
||||
|
||||
const truncateText = (text: string, maxLength: number): string => {
|
||||
if (text.length <= maxLength) return text;
|
||||
@@ -113,19 +106,6 @@ export function CreationSummary({
|
||||
|
||||
// Common method to save subagent configuration
|
||||
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
|
||||
if (!config) {
|
||||
throw new Error('Configuration not available');
|
||||
@@ -190,62 +170,11 @@ export function CreationSummary({
|
||||
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
|
||||
const wasRaw = stdin?.isRaw ?? false;
|
||||
try {
|
||||
setRawMode?.(false);
|
||||
await launchEditor(subagentFilePath);
|
||||
|
||||
// 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
|
||||
showSuccessAndClose();
|
||||
} finally {
|
||||
if (wasRaw) setRawMode?.(true);
|
||||
}
|
||||
// Show success UI and auto-close after successful edit
|
||||
showSuccessAndClose();
|
||||
} catch (error) {
|
||||
setSaveError(
|
||||
`Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
@@ -256,9 +185,7 @@ export function CreationSummary({
|
||||
showSuccessAndClose,
|
||||
state.generatedName,
|
||||
state.location,
|
||||
settings.merged.preferredEditor,
|
||||
stdin,
|
||||
setRawMode,
|
||||
launchEditor,
|
||||
]);
|
||||
|
||||
// Handle keyboard input
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
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 { useTextBuffer } from '../shared/text-buffer.js';
|
||||
import { useKeypress, Key } from '../../hooks/useKeypress.js';
|
||||
|
||||
@@ -194,9 +194,27 @@ export function SubagentCreationWizard({
|
||||
case WIZARD_STEPS.DESCRIPTION_INPUT:
|
||||
return <DescriptionInput {...stepProps} />;
|
||||
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:
|
||||
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:
|
||||
return <CreationSummary {...stepProps} />;
|
||||
default:
|
||||
@@ -208,7 +226,16 @@ export function SubagentCreationWizard({
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}, [stepProps, state.currentStep]);
|
||||
}, [
|
||||
stepProps,
|
||||
state.currentStep,
|
||||
state.selectedTools,
|
||||
state.backgroundColor,
|
||||
state.generatedName,
|
||||
config,
|
||||
handleNext,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { WizardStepProps, ToolCategory } from './types.js';
|
||||
import { Kind } from '@qwen-code/qwen-code-core';
|
||||
import { ToolCategory } from './types.js';
|
||||
import { Kind, Config } from '@qwen-code/qwen-code-core';
|
||||
import { Colors } from '../../colors.js';
|
||||
|
||||
interface ToolOption {
|
||||
@@ -17,20 +17,28 @@ interface ToolOption {
|
||||
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({
|
||||
state: _state,
|
||||
dispatch,
|
||||
onNext,
|
||||
onPrevious: _onPrevious,
|
||||
tools = [],
|
||||
onSelect,
|
||||
config,
|
||||
}: WizardStepProps) {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||
|
||||
}: ToolSelectorProps) {
|
||||
// Generate tool categories from actual tool registry
|
||||
const { toolCategories, readTools, editTools, executeTools } = useMemo(() => {
|
||||
const {
|
||||
toolCategories,
|
||||
readTools,
|
||||
editTools,
|
||||
executeTools,
|
||||
initialCategory,
|
||||
} = useMemo(() => {
|
||||
if (!config) {
|
||||
// Fallback categories if config not available
|
||||
return {
|
||||
@@ -44,6 +52,7 @@ export function ToolSelector({
|
||||
readTools: [],
|
||||
editTools: [],
|
||||
executeTools: [],
|
||||
initialCategory: 'all',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,8 +109,49 @@ export function ToolSelector({
|
||||
},
|
||||
].filter((category) => category.id === 'all' || category.tools.length > 0);
|
||||
|
||||
return { toolCategories, readTools, editTools, executeTools };
|
||||
}, [config]);
|
||||
// Determine initial category based on tools prop
|
||||
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) => ({
|
||||
label: category.name,
|
||||
@@ -117,11 +167,10 @@ export function ToolSelector({
|
||||
const category = toolCategories.find((cat) => cat.id === selectedValue);
|
||||
if (category) {
|
||||
if (category.id === 'all') {
|
||||
dispatch({ type: 'SET_TOOLS', tools: 'all' });
|
||||
onSelect([]); // Empty array for 'all'
|
||||
} 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 { ActionSelectionStep } from './ActionSelectionStep.js';
|
||||
export { AgentViewerStep } from './AgentViewerStep.js';
|
||||
export { AgentDeleteStep } from './AgentDeleteStep.js';
|
||||
|
||||
// Creation Wizard Types and State
|
||||
export type {
|
||||
@@ -30,16 +31,3 @@ export type {
|
||||
} from './types.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
|
||||
*/
|
||||
|
||||
import {
|
||||
CreationWizardState,
|
||||
WizardAction,
|
||||
ManagementDialogState,
|
||||
ManagementAction,
|
||||
MANAGEMENT_STEPS,
|
||||
} from './types.js';
|
||||
import { CreationWizardState, WizardAction } from './types.js';
|
||||
import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js';
|
||||
|
||||
export { MANAGEMENT_STEPS };
|
||||
|
||||
/**
|
||||
* Initial state for the creation wizard.
|
||||
*/
|
||||
@@ -26,7 +18,7 @@ export const initialWizardState: CreationWizardState = {
|
||||
generatedSystemPrompt: '',
|
||||
generatedDescription: '',
|
||||
generatedName: '',
|
||||
selectedTools: 'all',
|
||||
selectedTools: [],
|
||||
backgroundColor: 'auto',
|
||||
isGenerating: false,
|
||||
validationErrors: [],
|
||||
@@ -171,124 +163,3 @@ function validateStep(step: number, state: CreationWizardState): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state for the management dialog.
|
||||
*/
|
||||
export const initialManagementState: ManagementDialogState = {
|
||||
currentStep: MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||
availableAgents: [],
|
||||
selectedAgent: null,
|
||||
selectedAgentIndex: -1,
|
||||
selectedAction: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
canProceed: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for managing management dialog state transitions.
|
||||
*/
|
||||
export function managementReducer(
|
||||
state: ManagementDialogState,
|
||||
action: ManagementAction,
|
||||
): ManagementDialogState {
|
||||
switch (action.type) {
|
||||
case 'SET_AVAILABLE_AGENTS':
|
||||
return {
|
||||
...state,
|
||||
availableAgents: action.payload,
|
||||
canProceed: action.payload.length > 0,
|
||||
};
|
||||
|
||||
case 'SELECT_AGENT':
|
||||
return {
|
||||
...state,
|
||||
selectedAgent: action.payload.agent,
|
||||
selectedAgentIndex: action.payload.index,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'SELECT_ACTION':
|
||||
return {
|
||||
...state,
|
||||
selectedAction: action.payload,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
case 'GO_TO_NEXT_STEP': {
|
||||
const nextStep = state.currentStep + 1;
|
||||
return {
|
||||
...state,
|
||||
currentStep: nextStep,
|
||||
canProceed: getCanProceedForStep(nextStep, state),
|
||||
};
|
||||
}
|
||||
|
||||
case 'GO_TO_PREVIOUS_STEP': {
|
||||
const prevStep = Math.max(
|
||||
MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||
state.currentStep - 1,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
currentStep: prevStep,
|
||||
canProceed: getCanProceedForStep(prevStep, state),
|
||||
};
|
||||
}
|
||||
|
||||
case 'GO_TO_STEP':
|
||||
return {
|
||||
...state,
|
||||
currentStep: action.payload,
|
||||
canProceed: getCanProceedForStep(action.payload, state),
|
||||
};
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
};
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
};
|
||||
|
||||
case 'SET_CAN_PROCEED':
|
||||
return {
|
||||
...state,
|
||||
canProceed: action.payload,
|
||||
};
|
||||
|
||||
case 'RESET_DIALOG':
|
||||
return initialManagementState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a management step can proceed based on current state.
|
||||
*/
|
||||
function getCanProceedForStep(
|
||||
step: number,
|
||||
state: ManagementDialogState,
|
||||
): boolean {
|
||||
switch (step) {
|
||||
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||
return state.availableAgents.length > 0 && state.selectedAgent !== null;
|
||||
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||
return state.selectedAction !== null;
|
||||
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||
return true; // Can always go back from viewer
|
||||
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||
return true; // TODO: Add validation for editor
|
||||
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||
return true; // Can always proceed from confirmation
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
SubagentLevel,
|
||||
SubagentConfig,
|
||||
SubagentMetadata,
|
||||
Config,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { SubagentLevel, Config } from '@qwen-code/qwen-code-core';
|
||||
|
||||
/**
|
||||
* State management for the subagent creation wizard.
|
||||
@@ -37,7 +32,7 @@ export interface CreationWizardState {
|
||||
generatedName: string;
|
||||
|
||||
/** Selected tools for the subagent */
|
||||
selectedTools: string[] | 'all';
|
||||
selectedTools: string[];
|
||||
|
||||
/** Background color for runtime display */
|
||||
backgroundColor: string;
|
||||
@@ -84,7 +79,7 @@ export type WizardAction =
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
}
|
||||
| { type: 'SET_TOOLS'; tools: string[] | 'all' }
|
||||
| { type: 'SET_TOOLS'; tools: string[] }
|
||||
| { type: 'SET_BACKGROUND_COLOR'; color: string }
|
||||
| { type: 'SET_GENERATING'; isGenerating: boolean }
|
||||
| { type: 'SET_VALIDATION_ERRORS'; errors: string[] }
|
||||
@@ -116,54 +111,20 @@ export interface WizardResult {
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State management for the subagent management dialog.
|
||||
*/
|
||||
export interface ManagementDialogState {
|
||||
currentStep: number;
|
||||
availableAgents: SubagentMetadata[];
|
||||
selectedAgent: SubagentConfig | null;
|
||||
selectedAgentIndex: number;
|
||||
selectedAction: 'view' | 'edit' | 'delete' | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
canProceed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions that can be dispatched to update management dialog state.
|
||||
*/
|
||||
export type ManagementAction =
|
||||
| { type: 'SET_AVAILABLE_AGENTS'; payload: SubagentMetadata[] }
|
||||
| { type: 'SELECT_AGENT'; payload: { agent: SubagentConfig; index: number } }
|
||||
| { type: 'SELECT_ACTION'; payload: 'view' | 'edit' | 'delete' }
|
||||
| { type: 'GO_TO_NEXT_STEP' }
|
||||
| { type: 'GO_TO_PREVIOUS_STEP' }
|
||||
| { type: 'GO_TO_STEP'; payload: number }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_CAN_PROCEED'; payload: boolean }
|
||||
| { type: 'RESET_DIALOG' };
|
||||
|
||||
/**
|
||||
* Props for management dialog step components.
|
||||
*/
|
||||
export interface ManagementStepProps {
|
||||
state: ManagementDialogState;
|
||||
dispatch: React.Dispatch<ManagementAction>;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
onCancel: () => void;
|
||||
config: Config | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constants for management dialog steps.
|
||||
*/
|
||||
export const MANAGEMENT_STEPS = {
|
||||
AGENT_SELECTION: 1,
|
||||
ACTION_SELECTION: 2,
|
||||
AGENT_VIEWER: 3,
|
||||
AGENT_EDITOR: 4,
|
||||
DELETE_CONFIRMATION: 5,
|
||||
AGENT_SELECTION: 'agent-selection',
|
||||
ACTION_SELECTION: 'action-selection',
|
||||
AGENT_VIEWER: 'agent-viewer',
|
||||
EDIT_OPTIONS: 'edit-options',
|
||||
EDIT_TOOLS: 'edit-tools',
|
||||
EDIT_COLOR: 'edit-color',
|
||||
DELETE_CONFIRMATION: 'delete-confirmation',
|
||||
} 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'
|
||||
? undefined
|
||||
: 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
|
||||
export type {
|
||||
SubagentConfig,
|
||||
SubagentMetadata,
|
||||
SubagentLevel,
|
||||
SubagentRuntimeConfig,
|
||||
ValidationResult,
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import {
|
||||
SubagentConfig,
|
||||
SubagentRuntimeConfig,
|
||||
SubagentMetadata,
|
||||
SubagentLevel,
|
||||
ListSubagentsOptions,
|
||||
CreateSubagentOptions,
|
||||
@@ -235,8 +234,8 @@ export class SubagentManager {
|
||||
*/
|
||||
async listSubagents(
|
||||
options: ListSubagentsOptions = {},
|
||||
): Promise<SubagentMetadata[]> {
|
||||
const subagents: SubagentMetadata[] = [];
|
||||
): Promise<SubagentConfig[]> {
|
||||
const subagents: SubagentConfig[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
const levelsToCheck: SubagentLevel[] = options.level
|
||||
@@ -275,12 +274,6 @@ export class SubagentManager {
|
||||
case 'name':
|
||||
comparison = a.name.localeCompare(b.name);
|
||||
break;
|
||||
case 'lastModified': {
|
||||
const aTime = a.lastModified?.getTime() || 0;
|
||||
const bTime = b.lastModified?.getTime() || 0;
|
||||
comparison = aTime - bTime;
|
||||
break;
|
||||
}
|
||||
case 'level':
|
||||
// Project comes before user
|
||||
comparison =
|
||||
@@ -302,18 +295,18 @@ export class SubagentManager {
|
||||
* Finds a subagent by name and returns its metadata.
|
||||
*
|
||||
* @param name - Name of the subagent to find
|
||||
* @returns SubagentMetadata or null if not found
|
||||
* @returns SubagentConfig or null if not found
|
||||
*/
|
||||
async findSubagentByName(
|
||||
name: string,
|
||||
level?: SubagentLevel,
|
||||
): Promise<SubagentMetadata | null> {
|
||||
): Promise<SubagentConfig | null> {
|
||||
const config = await this.loadSubagent(name, level);
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.configToMetadata(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,7 +577,7 @@ export class SubagentManager {
|
||||
*/
|
||||
private async listSubagentsAtLevel(
|
||||
level: SubagentLevel,
|
||||
): Promise<SubagentMetadata[]> {
|
||||
): Promise<SubagentConfig[]> {
|
||||
const baseDir =
|
||||
level === 'project'
|
||||
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
||||
@@ -592,7 +585,7 @@ export class SubagentManager {
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(baseDir);
|
||||
const subagents: SubagentMetadata[] = [];
|
||||
const subagents: SubagentConfig[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
@@ -601,8 +594,7 @@ export class SubagentManager {
|
||||
|
||||
try {
|
||||
const config = await this.parseSubagentFile(filePath);
|
||||
const metadata = this.configToMetadata(config);
|
||||
subagents.push(metadata);
|
||||
subagents.push(config);
|
||||
} catch (error) {
|
||||
// Skip invalid files but log the error
|
||||
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).
|
||||
*
|
||||
|
||||
@@ -67,33 +67,6 @@ export interface SubagentConfig {
|
||||
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.
|
||||
* This interface maps SubagentConfig to the existing runtime interfaces.
|
||||
|
||||
Reference in New Issue
Block a user