diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index b040c363..b5abf4cd 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -25,6 +25,7 @@ import { useFolderTrust } from './hooks/useFolderTrust.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js'; +import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js'; import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js'; import { useMessageQueue } from './hooks/useMessageQueue.js'; import { useConsoleMessages } from './hooks/useConsoleMessages.js'; @@ -42,7 +43,10 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js'; import { FolderTrustDialog } from './components/FolderTrustDialog.js'; import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js'; import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js'; -import { SubagentCreationWizard } from './components/subagents/SubagentCreationWizard.js'; +import { + SubagentCreationWizard, + AgentsManagerDialog, +} from './components/subagents/index.js'; import { Colors } from './colors.js'; import { loadHierarchicalGeminiMemory } from '../config/config.js'; import { LoadedSettings, SettingScope } from '../config/settings.js'; @@ -277,6 +281,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { closeSubagentCreateDialog, } = useSubagentCreateDialog(); + const { + isAgentsManagerDialogOpen, + openAgentsManagerDialog, + closeAgentsManagerDialog, + } = useAgentsManagerDialog(); + const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust( settings, setIsTrustedFolder, @@ -574,6 +584,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { openPrivacyNotice, openSettingsDialog, openSubagentCreateDialog, + openAgentsManagerDialog, toggleVimEnabled, setIsProcessing, setGeminiMdFileCount, @@ -1087,6 +1098,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { config={config} /> + ) : isAgentsManagerDialogOpen ? ( + + + ) : isAuthenticating ? ( <> {isQwenAuth && isQwenAuthenticating ? ( diff --git a/packages/cli/src/ui/commands/agentsCommand.ts b/packages/cli/src/ui/commands/agentsCommand.ts index 307a1cdf..b0781c3c 100644 --- a/packages/cli/src/ui/commands/agentsCommand.ts +++ b/packages/cli/src/ui/commands/agentsCommand.ts @@ -4,19 +4,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MessageType } from '../types.js'; -import { - CommandKind, - SlashCommand, - SlashCommandActionReturn, - OpenDialogActionReturn, -} from './types.js'; +import { CommandKind, SlashCommand, OpenDialogActionReturn } from './types.js'; export const agentsCommand: SlashCommand = { name: 'agents', description: 'Manage subagents for specialized task delegation.', kind: CommandKind.BUILT_IN, subCommands: [ + { + name: 'list', + description: 'Manage existing subagents (view, edit, delete).', + kind: CommandKind.BUILT_IN, + action: (): OpenDialogActionReturn => ({ + type: 'dialog', + dialog: 'subagent_list', + }), + }, { name: 'create', description: 'Create a new subagent with guided setup.', @@ -26,94 +29,5 @@ export const agentsCommand: SlashCommand = { dialog: 'subagent_create', }), }, - { - name: 'list', - description: 'List all available subagents.', - kind: CommandKind.BUILT_IN, - action: async (context): Promise => { - context.ui.addItem( - { - type: MessageType.INFO, - text: 'Listing subagents... (not implemented yet)', - }, - Date.now(), - ); - }, - }, - { - name: 'show', - description: 'Show detailed information about a subagent.', - kind: CommandKind.BUILT_IN, - action: async ( - context, - args, - ): Promise => { - if (!args || args.trim() === '') { - return { - type: 'message', - messageType: 'error', - content: 'Usage: /agents show ', - }; - } - - context.ui.addItem( - { - type: MessageType.INFO, - text: `Showing details for subagent: ${args.trim()} (not implemented yet)`, - }, - Date.now(), - ); - }, - }, - { - name: 'edit', - description: 'Edit an existing subagent configuration.', - kind: CommandKind.BUILT_IN, - action: async ( - context, - args, - ): Promise => { - if (!args || args.trim() === '') { - return { - type: 'message', - messageType: 'error', - content: 'Usage: /agents edit ', - }; - } - - context.ui.addItem( - { - type: MessageType.INFO, - text: `Editing subagent: ${args.trim()} (not implemented yet)`, - }, - Date.now(), - ); - }, - }, - { - name: 'delete', - description: 'Delete a subagent configuration.', - kind: CommandKind.BUILT_IN, - action: async ( - context, - args, - ): Promise => { - if (!args || args.trim() === '') { - return { - type: 'message', - messageType: 'error', - content: 'Usage: /agents delete ', - }; - } - - context.ui.addItem( - { - type: MessageType.INFO, - text: `Deleting subagent: ${args.trim()} (not implemented yet)`, - }, - Date.now(), - ); - }, - }, ], }; diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts index 91f42a2e..f0411e03 100644 --- a/packages/cli/src/ui/commands/types.ts +++ b/packages/cli/src/ui/commands/types.ts @@ -111,7 +111,8 @@ export interface OpenDialogActionReturn { | 'editor' | 'privacy' | 'settings' - | 'subagent_create'; + | 'subagent_create' + | 'subagent_list'; } /** diff --git a/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx b/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx new file mode 100644 index 00000000..f3e14637 --- /dev/null +++ b/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box } from 'ink'; +import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; +import { ManagementStepProps } from './types.js'; + +export const ActionSelectionStep = ({ + state, + dispatch, + onNext, + onPrevious, +}: ManagementStepProps) => { + const actions = [ + { label: 'View Agent', value: 'view' as const }, + { label: 'Edit Agent', value: 'edit' as const }, + { label: 'Delete Agent', value: 'delete' as const }, + { label: 'Back', value: 'back' as const }, + ]; + + const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => { + if (value === 'back') { + onPrevious(); + return; + } + + dispatch({ type: 'SELECT_ACTION', payload: value }); + onNext(); + }; + + const selectedIndex = state.selectedAction + ? actions.findIndex((action) => action.value === state.selectedAction) + : 0; + + return ( + + + + ); +}; diff --git a/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx b/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx new file mode 100644 index 00000000..3e68a0d9 --- /dev/null +++ b/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx @@ -0,0 +1,305 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; +import { Box, Text } from 'ink'; +import { ManagementStepProps } from './types.js'; +import { theme } from '../../semantic-colors.js'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; + +interface NavigationState { + currentBlock: 'project' | 'user'; + projectIndex: number; + userIndex: number; +} + +export const AgentSelectionStep = ({ + state, + dispatch, + onNext, + config, +}: ManagementStepProps) => { + const [isLoading, setIsLoading] = useState(false); + const [navigation, setNavigation] = useState({ + currentBlock: 'project', + projectIndex: 0, + userIndex: 0, + }); + + // Group agents by level + const projectAgents = state.availableAgents.filter( + (agent) => agent.level === 'project', + ); + const userAgents = state.availableAgents.filter( + (agent) => agent.level === 'user', + ); + const projectNames = new Set(projectAgents.map((agent) => agent.name)); + + useEffect(() => { + const loadAgents = async () => { + setIsLoading(true); + dispatch({ type: 'SET_LOADING', payload: true }); + + try { + if (!config) { + throw new Error('Configuration not available'); + } + const manager = config.getSubagentManager(); + + // Load agents from both levels separately to show all agents including conflicts + const [projectAgents, userAgents] = await Promise.all([ + manager.listSubagents({ level: 'project' }), + manager.listSubagents({ level: 'user' }), + ]); + + // Combine all agents (project and user level) + const allAgents = [...projectAgents, ...userAgents]; + + dispatch({ type: 'SET_AVAILABLE_AGENTS', payload: allAgents }); + dispatch({ type: 'SET_ERROR', payload: null }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + dispatch({ + type: 'SET_ERROR', + payload: `Failed to load agents: ${errorMessage}`, + }); + } finally { + setIsLoading(false); + dispatch({ type: 'SET_LOADING', payload: false }); + } + }; + + loadAgents(); + }, [dispatch, config]); + + // Initialize navigation state when agents are loaded + useEffect(() => { + if (projectAgents.length > 0) { + setNavigation((prev) => ({ ...prev, currentBlock: 'project' })); + } else if (userAgents.length > 0) { + setNavigation((prev) => ({ ...prev, currentBlock: 'user' })); + } + }, [projectAgents.length, userAgents.length]); + + // Custom keyboard navigation + useKeypress( + (key) => { + const { name } = key; + + if (name === 'up' || name === 'k') { + setNavigation((prev) => { + if (prev.currentBlock === 'project') { + if (prev.projectIndex > 0) { + return { ...prev, projectIndex: prev.projectIndex - 1 }; + } else if (userAgents.length > 0) { + // Move to last item in user block + return { + ...prev, + currentBlock: 'user', + userIndex: userAgents.length - 1, + }; + } else { + // Wrap to last item in project block + return { ...prev, projectIndex: projectAgents.length - 1 }; + } + } else { + if (prev.userIndex > 0) { + return { ...prev, userIndex: prev.userIndex - 1 }; + } else if (projectAgents.length > 0) { + // Move to last item in project block + return { + ...prev, + currentBlock: 'project', + projectIndex: projectAgents.length - 1, + }; + } else { + // Wrap to last item in user block + return { ...prev, userIndex: userAgents.length - 1 }; + } + } + }); + } else if (name === 'down' || name === 'j') { + setNavigation((prev) => { + if (prev.currentBlock === 'project') { + if (prev.projectIndex < projectAgents.length - 1) { + return { ...prev, projectIndex: prev.projectIndex + 1 }; + } else if (userAgents.length > 0) { + // Move to first item in user block + return { ...prev, currentBlock: 'user', userIndex: 0 }; + } else { + // Wrap to first item in project block + return { ...prev, projectIndex: 0 }; + } + } else { + if (prev.userIndex < userAgents.length - 1) { + return { ...prev, userIndex: prev.userIndex + 1 }; + } else if (projectAgents.length > 0) { + // Move to first item in project block + return { ...prev, currentBlock: 'project', projectIndex: 0 }; + } else { + // Wrap to first item in user block + return { ...prev, userIndex: 0 }; + } + } + }); + } else if (name === 'return' || name === 'space') { + // Select current item + const currentAgent = + navigation.currentBlock === 'project' + ? projectAgents[navigation.projectIndex] + : userAgents[navigation.userIndex]; + + if (currentAgent) { + const agentIndex = state.availableAgents.indexOf(currentAgent); + handleAgentSelect(agentIndex); + } + } + }, + { isActive: true }, + ); + + const handleAgentSelect = async (index: number) => { + const selectedMetadata = state.availableAgents[index]; + if (!selectedMetadata) return; + + try { + if (!config) { + throw new Error('Configuration not available'); + } + const manager = config.getSubagentManager(); + const agent = await manager.loadSubagent( + selectedMetadata.name, + selectedMetadata.level, + ); + + if (agent) { + dispatch({ type: 'SELECT_AGENT', payload: { agent, index } }); + onNext(); + } else { + dispatch({ + type: 'SET_ERROR', + payload: `Failed to load agent: ${selectedMetadata.name}`, + }); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + dispatch({ + type: 'SET_ERROR', + payload: `Failed to load agent: ${errorMessage}`, + }); + } + }; + + if (isLoading) { + return ( + + Loading agents... + + ); + } + + if (state.error) { + return ( + + {state.error} + + ); + } + + if (state.availableAgents.length === 0) { + return ( + + No subagents found. + + Use '/agents create' to create your first subagent. + + + ); + } + + // Render custom radio button items + const renderAgentItem = ( + agent: { name: string; level: 'project' | 'user' }, + index: number, + isSelected: boolean, + ) => { + const textColor = isSelected ? theme.text.accent : theme.text.primary; + + return ( + + + + {isSelected ? '●' : ' '} + + + + {agent.name} + {agent.level === 'user' && projectNames.has(agent.name) && ( + + {' '} + (overridden by project level agent) + + )} + + + ); + }; + + // Calculate enabled agents count (excluding conflicted user-level agents) + const enabledAgentsCount = + projectAgents.length + + userAgents.filter((agent) => !projectNames.has(agent.name)).length; + + return ( + + {/* Project Level Agents */} + {projectAgents.length > 0 && ( + + + Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')}) + + + {projectAgents.map((agent, index) => { + const isSelected = + navigation.currentBlock === 'project' && + navigation.projectIndex === index; + return renderAgentItem(agent, index, isSelected); + })} + + + )} + + {/* User Level Agents */} + {userAgents.length > 0 && ( + + + User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')}) + + + {userAgents.map((agent, index) => { + const isSelected = + navigation.currentBlock === 'user' && + navigation.userIndex === index; + return renderAgentItem(agent, index, isSelected); + })} + + + )} + + {/* Agent count summary */} + {(projectAgents.length > 0 || userAgents.length > 0) && ( + + + Using: {enabledAgentsCount} agents + + + )} + + ); +}; diff --git a/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx b/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx new file mode 100644 index 00000000..3aa48f8e --- /dev/null +++ b/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box, Text, useInput } from 'ink'; +import { ManagementStepProps } from './types.js'; +import { theme } from '../../semantic-colors.js'; +import { shouldShowColor, getColorForDisplay } from './utils.js'; + +export const AgentViewerStep = ({ state, onPrevious }: ManagementStepProps) => { + // Handle keyboard input + useInput((input, key) => { + if (key.escape || input === 'b') { + onPrevious(); + } + }); + + if (!state.selectedAgent) { + return ( + + No agent selected + + ); + } + + const agent = state.selectedAgent; + + const toolsDisplay = agent.tools ? agent.tools.join(', ') : '*'; + + return ( + + + + Location: + + {agent.level === 'project' + ? 'Project Level (.qwen/agents/)' + : 'User Level (~/.qwen/agents/)'} + + + + + File Path: + {agent.filePath} + + + + Tools: + {toolsDisplay} + + + {shouldShowColor(agent.backgroundColor) && ( + + Color: + + {` ${agent.name} `} + + + )} + + + Description: + + + {agent.description} + + + + System Prompt: + + + {agent.systemPrompt} + + + + ); +}; diff --git a/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx new file mode 100644 index 00000000..699e80e7 --- /dev/null +++ b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx @@ -0,0 +1,178 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useReducer, useCallback, useMemo } from 'react'; +import { Box, Text, useInput } from 'ink'; +import { managementReducer, initialManagementState } from './reducers.js'; +import { AgentSelectionStep } from './AgentSelectionStep.js'; +import { ActionSelectionStep } from './ActionSelectionStep.js'; +import { AgentViewerStep } from './AgentViewerStep.js'; +import { ManagementStepProps, MANAGEMENT_STEPS } from './types.js'; +import { Colors } from '../../colors.js'; +import { theme } from '../../semantic-colors.js'; +import { Config } from '@qwen-code/qwen-code-core'; + +interface AgentsManagerDialogProps { + onClose: () => void; + config: Config | null; +} + +/** + * Main orchestrator component for the agents management dialog. + */ +export function AgentsManagerDialog({ + onClose, + config, +}: AgentsManagerDialogProps) { + const [state, dispatch] = useReducer( + managementReducer, + initialManagementState, + ); + + const handleNext = useCallback(() => { + dispatch({ type: 'GO_TO_NEXT_STEP' }); + }, []); + + const handlePrevious = useCallback(() => { + dispatch({ type: 'GO_TO_PREVIOUS_STEP' }); + }, []); + + const handleCancel = useCallback(() => { + dispatch({ type: 'RESET_DIALOG' }); + onClose(); + }, [onClose]); + + // Centralized ESC key handling for the entire dialog + useInput((input, key) => { + if (key.escape) { + // Agent viewer step handles its own ESC logic + if (state.currentStep === MANAGEMENT_STEPS.AGENT_VIEWER) { + return; // Let AgentViewerStep handle it + } + + if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) { + // On first step, ESC cancels the entire dialog + handleCancel(); + } else { + // On other steps, ESC goes back to previous step + handlePrevious(); + } + } + }); + + const stepProps: ManagementStepProps = useMemo( + () => ({ + state, + config, + dispatch, + onNext: handleNext, + onPrevious: handlePrevious, + onCancel: handleCancel, + }), + [state, dispatch, handleNext, handlePrevious, handleCancel, config], + ); + + const renderStepHeader = useCallback(() => { + const getStepHeaderText = () => { + switch (state.currentStep) { + case MANAGEMENT_STEPS.AGENT_SELECTION: + return 'Agents'; + case MANAGEMENT_STEPS.ACTION_SELECTION: + return 'Choose Action'; + case MANAGEMENT_STEPS.AGENT_VIEWER: + return state.selectedAgent?.name; + case MANAGEMENT_STEPS.AGENT_EDITOR: + return `Editing: ${state.selectedAgent?.name || 'Unknown'}`; + case MANAGEMENT_STEPS.DELETE_CONFIRMATION: + return `Delete: ${state.selectedAgent?.name || 'Unknown'}`; + default: + return 'Unknown Step'; + } + }; + + return ( + + {getStepHeaderText()} + + ); + }, [state.currentStep, state.selectedAgent?.name]); + + const renderStepFooter = useCallback(() => { + const getNavigationInstructions = () => { + if (state.currentStep === MANAGEMENT_STEPS.ACTION_SELECTION) { + return 'Enter to select, ↑↓ to navigate, Esc to go back'; + } + + if (state.currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) { + if (state.availableAgents.length === 0) { + return 'Esc to close'; + } + return 'Enter to select, ↑↓ to navigate, Esc to close'; + } + + return 'Esc to go back'; + }; + + return ( + + {getNavigationInstructions()} + + ); + }, [state.currentStep, state.availableAgents.length]); + + const renderStepContent = useCallback(() => { + switch (state.currentStep) { + case MANAGEMENT_STEPS.AGENT_SELECTION: + return ; + case MANAGEMENT_STEPS.ACTION_SELECTION: + return ; + case MANAGEMENT_STEPS.AGENT_VIEWER: + return ; + case MANAGEMENT_STEPS.AGENT_EDITOR: + return ( + + + Agent editing not yet implemented + + + ); + case MANAGEMENT_STEPS.DELETE_CONFIRMATION: + return ( + + + Agent deletion not yet implemented + + + ); + default: + return ( + + + Invalid step: {state.currentStep} + + + ); + } + }, [stepProps, state.currentStep]); + + return ( + + {/* Main content wrapped in bounding box */} + + {renderStepHeader()} + {renderStepContent()} + {renderStepFooter()} + + + ); +} diff --git a/packages/cli/src/ui/components/subagents/ColorSelector.tsx b/packages/cli/src/ui/components/subagents/ColorSelector.tsx index 451216ad..7e318803 100644 --- a/packages/cli/src/ui/components/subagents/ColorSelector.tsx +++ b/packages/cli/src/ui/components/subagents/ColorSelector.tsx @@ -8,44 +8,9 @@ import { Box, Text } from 'ink'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; import { WizardStepProps, ColorOption } from './types.js'; import { Colors } from '../../colors.js'; +import { COLOR_OPTIONS } from './constants.js'; -const colorOptions: ColorOption[] = [ - { - id: 'auto', - name: 'Automatic Color', - value: 'auto', - }, - { - id: 'blue', - name: 'Blue', - value: '#3b82f6', - }, - { - id: 'green', - name: 'Green', - value: '#10b981', - }, - { - id: 'purple', - name: 'Purple', - value: '#8b5cf6', - }, - { - id: 'orange', - name: 'Orange', - value: '#f59e0b', - }, - { - id: 'red', - name: 'Red', - value: '#ef4444', - }, - { - id: 'cyan', - name: 'Cyan', - value: '#06b6d4', - }, -]; +const colorOptions: ColorOption[] = COLOR_OPTIONS; /** * Step 5: Background color selection with preview. @@ -65,12 +30,12 @@ export function ColorSelector({ (option) => option.id === selectedValue, ); if (colorOption) { - dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.value }); + dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.name }); } }; const currentColor = - colorOptions.find((option) => option.value === state.backgroundColor) || + colorOptions.find((option) => option.name === state.backgroundColor) || colorOptions[0]; return ( @@ -92,8 +57,8 @@ export function ColorSelector({ Preview: - - {state.generatedName} + + {` ${state.generatedName} `} diff --git a/packages/cli/src/ui/components/subagents/CreationSummary.tsx b/packages/cli/src/ui/components/subagents/CreationSummary.tsx index 5dadb7da..10af42f4 100644 --- a/packages/cli/src/ui/components/subagents/CreationSummary.tsx +++ b/packages/cli/src/ui/components/subagents/CreationSummary.tsx @@ -16,6 +16,7 @@ import { import { useSettings } from '../../contexts/SettingsContext.js'; import { spawnSync } from 'child_process'; import { theme } from '../../semantic-colors.js'; +import { shouldShowColor, getColorForDisplay } from './utils.js'; /** * Step 6: Final confirmation and actions. @@ -47,8 +48,7 @@ export function CreationSummary({ try { // Get project root from config - const projectRoot = config.getProjectRoot(); - const subagentManager = new SubagentManager(projectRoot); + const subagentManager = config.getSubagentManager(); // Check for name conflicts const isAvailable = await subagentManager.isNameAvailable( @@ -130,8 +130,7 @@ export function CreationSummary({ if (!config) { throw new Error('Configuration not available'); } - const projectRoot = config.getProjectRoot(); - const subagentManager = new SubagentManager(projectRoot); + const subagentManager = config.getSubagentManager(); // Build subagent configuration const subagentConfig: SubagentConfig = { @@ -143,6 +142,7 @@ export function CreationSummary({ tools: Array.isArray(state.selectedTools) ? state.selectedTools : undefined, + backgroundColor: state.backgroundColor, }; // Create the subagent @@ -316,6 +316,15 @@ export function CreationSummary({ {toolsDisplay} + {shouldShowColor(state.backgroundColor) && ( + + Color: + + {` ${state.generatedName} `} + + + )} + Description: @@ -337,11 +346,11 @@ export function CreationSummary({ {saveError && ( - + ❌ Error saving subagent: - + {saveError} diff --git a/packages/cli/src/ui/components/subagents/DescriptionInput.tsx b/packages/cli/src/ui/components/subagents/DescriptionInput.tsx index 8c1c7b8e..1d1d7f72 100644 --- a/packages/cli/src/ui/components/subagents/DescriptionInput.tsx +++ b/packages/cli/src/ui/components/subagents/DescriptionInput.tsx @@ -302,7 +302,7 @@ export function DescriptionInput({ {state.validationErrors.length > 0 && ( {state.validationErrors.map((error, index) => ( - + ⚠ {error} ))} diff --git a/packages/cli/src/ui/components/subagents/ErrorBoundary.tsx b/packages/cli/src/ui/components/subagents/ErrorBoundary.tsx deleted file mode 100644 index e5716383..00000000 --- a/packages/cli/src/ui/components/subagents/ErrorBoundary.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { Component, ReactNode } from 'react'; -import { Box, Text } from 'ink'; - -interface ErrorBoundaryState { - hasError: boolean; - error?: Error; - errorInfo?: React.ErrorInfo; -} - -interface ErrorBoundaryProps { - children: ReactNode; - fallback?: ReactNode; - onError?: (error: Error, errorInfo: React.ErrorInfo) => void; -} - -/** - * Error boundary component for graceful error handling in the subagent wizard. - */ -export class ErrorBoundary extends Component< - ErrorBoundaryProps, - ErrorBoundaryState -> { - constructor(props: ErrorBoundaryProps) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { - hasError: true, - error, - }; - } - - override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ - error, - errorInfo, - }); - - // Call optional error handler - this.props.onError?.(error, errorInfo); - - // Log error for debugging - console.error( - 'SubagentWizard Error Boundary caught an error:', - error, - errorInfo, - ); - } - - override render() { - if (this.state.hasError) { - // Custom fallback UI or default error display - if (this.props.fallback) { - return this.props.fallback; - } - - return ( - - - - ❌ Subagent Wizard Error - - - - - - An unexpected error occurred in the subagent creation wizard. - - - - {this.state.error && ( - - - Error Details: - - - {this.state.error.message} - - - )} - - - - Press Esc to close the wizard and try - again. - - - - {process.env['NODE_ENV'] === 'development' && - this.state.errorInfo && ( - - - Stack Trace (Development): - - - {this.state.errorInfo.componentStack} - - - )} - - ); - } - - return this.props.children; - } -} diff --git a/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx b/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx index 23934f93..3d660506 100644 --- a/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx +++ b/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx @@ -6,14 +6,13 @@ import { useReducer, useCallback, useMemo } from 'react'; import { Box, Text, useInput } from 'ink'; -import { wizardReducer, initialWizardState } from './wizardReducer.js'; +import { wizardReducer, initialWizardState } from './reducers.js'; import { LocationSelector } from './LocationSelector.js'; import { GenerationMethodSelector } from './GenerationMethodSelector.js'; import { DescriptionInput } from './DescriptionInput.js'; import { ToolSelector } from './ToolSelector.js'; import { ColorSelector } from './ColorSelector.js'; import { CreationSummary } from './CreationSummary.js'; -import { ErrorBoundary } from './ErrorBoundary.js'; import { WizardStepProps } from './types.js'; import { WIZARD_STEPS } from './constants.js'; import { Config } from '@qwen-code/qwen-code-core'; @@ -113,9 +112,9 @@ export function SubagentCreationWizard({ } return ( - + - + Debug Info: Step: {state.currentStep} @@ -128,7 +127,9 @@ export function SubagentCreationWizard({ Location: {state.location} Method: {state.generationMethod} {state.validationErrors.length > 0 && ( - Errors: {state.validationErrors.join(', ')} + + Errors: {state.validationErrors.join(', ')} + )} @@ -201,35 +202,30 @@ export function SubagentCreationWizard({ default: return ( - Invalid step: {state.currentStep} + + Invalid step: {state.currentStep} + ); } }, [stepProps, state.currentStep]); return ( - { - // Additional error handling if needed - console.error('Subagent wizard error:', error, errorInfo); - }} - > - - {/* Main content wrapped in bounding box */} - - {renderStepHeader()} - {renderStepContent()} - {renderDebugContent()} - {renderStepFooter()} - + + {/* Main content wrapped in bounding box */} + + {renderStepHeader()} + {renderStepContent()} + {renderDebugContent()} + {renderStepFooter()} - + ); } diff --git a/packages/cli/src/ui/components/subagents/constants.ts b/packages/cli/src/ui/components/subagents/constants.ts index 59c44243..0aecfefe 100644 --- a/packages/cli/src/ui/components/subagents/constants.ts +++ b/packages/cli/src/ui/components/subagents/constants.ts @@ -30,3 +30,42 @@ export const STEP_NAMES: Record = { [WIZARD_STEPS.COLOR_SELECTION]: 'Color Selection', [WIZARD_STEPS.FINAL_CONFIRMATION]: 'Final Confirmation', }; + +// Color options for subagent display +export const COLOR_OPTIONS = [ + { + id: 'auto', + name: 'Automatic Color', + value: 'auto', + }, + { + id: 'blue', + name: 'Blue', + value: '#3b82f6', + }, + { + id: 'green', + name: 'Green', + value: '#10b981', + }, + { + id: 'purple', + name: 'Purple', + value: '#8b5cf6', + }, + { + id: 'orange', + name: 'Orange', + value: '#f59e0b', + }, + { + id: 'red', + name: 'Red', + value: '#ef4444', + }, + { + id: 'cyan', + name: 'Cyan', + value: '#06b6d4', + }, +]; diff --git a/packages/cli/src/ui/components/subagents/index.ts b/packages/cli/src/ui/components/subagents/index.ts index eec7e0d6..eb4aa00d 100644 --- a/packages/cli/src/ui/components/subagents/index.ts +++ b/packages/cli/src/ui/components/subagents/index.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +// Creation Wizard Components export { SubagentCreationWizard } from './SubagentCreationWizard.js'; export { LocationSelector } from './LocationSelector.js'; export { GenerationMethodSelector } from './GenerationMethodSelector.js'; @@ -12,6 +13,13 @@ export { ToolSelector } from './ToolSelector.js'; export { ColorSelector } from './ColorSelector.js'; export { CreationSummary } from './CreationSummary.js'; +// Management Dialog Components +export { AgentsManagerDialog } from './AgentsManagerDialog.js'; +export { AgentSelectionStep } from './AgentSelectionStep.js'; +export { ActionSelectionStep } from './ActionSelectionStep.js'; +export { AgentViewerStep } from './AgentViewerStep.js'; + +// Creation Wizard Types and State export type { CreationWizardState, WizardAction, @@ -21,4 +29,17 @@ export type { ColorOption, } from './types.js'; -export { wizardReducer, initialWizardState } from './wizardReducer.js'; +export { wizardReducer, initialWizardState } from './reducers.js'; + +// Management Dialog Types and State +export type { + ManagementDialogState, + ManagementAction, + ManagementStepProps, +} from './types.js'; + +export { + managementReducer, + initialManagementState, + MANAGEMENT_STEPS, +} from './reducers.js'; diff --git a/packages/cli/src/ui/components/subagents/reducers.tsx b/packages/cli/src/ui/components/subagents/reducers.tsx new file mode 100644 index 00000000..a7f57a3f --- /dev/null +++ b/packages/cli/src/ui/components/subagents/reducers.tsx @@ -0,0 +1,294 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CreationWizardState, + WizardAction, + ManagementDialogState, + ManagementAction, + MANAGEMENT_STEPS, +} from './types.js'; +import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js'; + +export { MANAGEMENT_STEPS }; + +/** + * Initial state for the creation wizard. + */ +export const initialWizardState: CreationWizardState = { + currentStep: WIZARD_STEPS.LOCATION_SELECTION, + location: 'project', + generationMethod: 'qwen', + userDescription: '', + generatedSystemPrompt: '', + generatedDescription: '', + generatedName: '', + selectedTools: 'all', + backgroundColor: 'auto', + isGenerating: false, + validationErrors: [], + canProceed: false, +}; + +/** + * Reducer for managing wizard state transitions. + */ +export function wizardReducer( + state: CreationWizardState, + action: WizardAction, +): CreationWizardState { + switch (action.type) { + case 'SET_STEP': + return { + ...state, + currentStep: Math.max( + WIZARD_STEPS.LOCATION_SELECTION, + Math.min(TOTAL_WIZARD_STEPS, action.step), + ), + validationErrors: [], + }; + + case 'SET_LOCATION': + return { + ...state, + location: action.location, + canProceed: true, + }; + + case 'SET_GENERATION_METHOD': + return { + ...state, + generationMethod: action.method, + canProceed: true, + }; + + case 'SET_USER_DESCRIPTION': + return { + ...state, + userDescription: action.description, + canProceed: action.description.trim().length >= 0, + }; + + case 'SET_GENERATED_CONTENT': + return { + ...state, + generatedName: action.name, + generatedDescription: action.description, + generatedSystemPrompt: action.systemPrompt, + isGenerating: false, + canProceed: true, + }; + + case 'SET_TOOLS': + return { + ...state, + selectedTools: action.tools, + canProceed: true, + }; + + case 'SET_BACKGROUND_COLOR': + return { + ...state, + backgroundColor: action.color, + canProceed: true, + }; + + case 'SET_GENERATING': + return { + ...state, + isGenerating: action.isGenerating, + canProceed: !action.isGenerating, + }; + + case 'SET_VALIDATION_ERRORS': + return { + ...state, + validationErrors: action.errors, + canProceed: action.errors.length === 0, + }; + + case 'GO_TO_NEXT_STEP': + if (state.canProceed && state.currentStep < TOTAL_WIZARD_STEPS) { + return { + ...state, + currentStep: state.currentStep + 1, + validationErrors: [], + canProceed: validateStep(state.currentStep + 1, state), + }; + } + return state; + + case 'GO_TO_PREVIOUS_STEP': + if (state.currentStep > WIZARD_STEPS.LOCATION_SELECTION) { + return { + ...state, + currentStep: state.currentStep - 1, + validationErrors: [], + canProceed: validateStep(state.currentStep - 1, state), + }; + } + return state; + + case 'RESET_WIZARD': + return initialWizardState; + + default: + return state; + } +} + +/** + * Validates whether a step can proceed based on current state. + */ +function validateStep(step: number, state: CreationWizardState): boolean { + switch (step) { + case WIZARD_STEPS.LOCATION_SELECTION: // Location selection + return true; // Always can proceed from location selection + + case WIZARD_STEPS.GENERATION_METHOD: // Generation method + return true; // Always can proceed from method selection + + case WIZARD_STEPS.DESCRIPTION_INPUT: // Description input + return state.userDescription.trim().length >= 0; + + case WIZARD_STEPS.TOOL_SELECTION: // Tool selection + return ( + state.generatedName.length > 0 && + state.generatedDescription.length > 0 && + state.generatedSystemPrompt.length > 0 + ); + + case WIZARD_STEPS.COLOR_SELECTION: // Color selection + return true; // Always can proceed from tool selection + + case WIZARD_STEPS.FINAL_CONFIRMATION: // Final confirmation + return state.backgroundColor.length > 0; + + default: + return false; + } +} + +/** + * Initial state for the management dialog. + */ +export const initialManagementState: ManagementDialogState = { + currentStep: MANAGEMENT_STEPS.AGENT_SELECTION, + availableAgents: [], + selectedAgent: null, + selectedAgentIndex: -1, + selectedAction: null, + isLoading: false, + error: null, + canProceed: false, +}; + +/** + * Reducer for managing management dialog state transitions. + */ +export function managementReducer( + state: ManagementDialogState, + action: ManagementAction, +): ManagementDialogState { + switch (action.type) { + case 'SET_AVAILABLE_AGENTS': + return { + ...state, + availableAgents: action.payload, + canProceed: action.payload.length > 0, + }; + + case 'SELECT_AGENT': + return { + ...state, + selectedAgent: action.payload.agent, + selectedAgentIndex: action.payload.index, + canProceed: true, + }; + + case 'SELECT_ACTION': + return { + ...state, + selectedAction: action.payload, + canProceed: true, + }; + + case 'GO_TO_NEXT_STEP': { + const nextStep = state.currentStep + 1; + return { + ...state, + currentStep: nextStep, + canProceed: getCanProceedForStep(nextStep, state), + }; + } + + case 'GO_TO_PREVIOUS_STEP': { + const prevStep = Math.max( + MANAGEMENT_STEPS.AGENT_SELECTION, + state.currentStep - 1, + ); + return { + ...state, + currentStep: prevStep, + canProceed: getCanProceedForStep(prevStep, state), + }; + } + + case 'GO_TO_STEP': + return { + ...state, + currentStep: action.payload, + canProceed: getCanProceedForStep(action.payload, state), + }; + + case 'SET_LOADING': + return { + ...state, + isLoading: action.payload, + }; + + case 'SET_ERROR': + return { + ...state, + error: action.payload, + }; + + case 'SET_CAN_PROCEED': + return { + ...state, + canProceed: action.payload, + }; + + case 'RESET_DIALOG': + return initialManagementState; + + default: + return state; + } +} + +/** + * Validates whether a management step can proceed based on current state. + */ +function getCanProceedForStep( + step: number, + state: ManagementDialogState, +): boolean { + switch (step) { + case MANAGEMENT_STEPS.AGENT_SELECTION: + return state.availableAgents.length > 0 && state.selectedAgent !== null; + case MANAGEMENT_STEPS.ACTION_SELECTION: + return state.selectedAction !== null; + case MANAGEMENT_STEPS.AGENT_VIEWER: + return true; // Can always go back from viewer + case MANAGEMENT_STEPS.AGENT_EDITOR: + return true; // TODO: Add validation for editor + case MANAGEMENT_STEPS.DELETE_CONFIRMATION: + return true; // Can always proceed from confirmation + default: + return false; + } +} diff --git a/packages/cli/src/ui/components/subagents/types.ts b/packages/cli/src/ui/components/subagents/types.ts index 444b228e..d3393b98 100644 --- a/packages/cli/src/ui/components/subagents/types.ts +++ b/packages/cli/src/ui/components/subagents/types.ts @@ -4,7 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SubagentLevel, Config } from '@qwen-code/qwen-code-core'; +import { + SubagentLevel, + SubagentConfig, + SubagentMetadata, + Config, +} from '@qwen-code/qwen-code-core'; /** * State management for the subagent creation wizard. @@ -110,3 +115,55 @@ export interface WizardResult { tools?: string[]; backgroundColor: string; } + +/** + * State management for the subagent management dialog. + */ +export interface ManagementDialogState { + currentStep: number; + availableAgents: SubagentMetadata[]; + selectedAgent: SubagentConfig | null; + selectedAgentIndex: number; + selectedAction: 'view' | 'edit' | 'delete' | null; + isLoading: boolean; + error: string | null; + canProceed: boolean; +} + +/** + * Actions that can be dispatched to update management dialog state. + */ +export type ManagementAction = + | { type: 'SET_AVAILABLE_AGENTS'; payload: SubagentMetadata[] } + | { type: 'SELECT_AGENT'; payload: { agent: SubagentConfig; index: number } } + | { type: 'SELECT_ACTION'; payload: 'view' | 'edit' | 'delete' } + | { type: 'GO_TO_NEXT_STEP' } + | { type: 'GO_TO_PREVIOUS_STEP' } + | { type: 'GO_TO_STEP'; payload: number } + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'SET_ERROR'; payload: string | null } + | { type: 'SET_CAN_PROCEED'; payload: boolean } + | { type: 'RESET_DIALOG' }; + +/** + * Props for management dialog step components. + */ +export interface ManagementStepProps { + state: ManagementDialogState; + dispatch: React.Dispatch; + onNext: () => void; + onPrevious: () => void; + onCancel: () => void; + config: Config | null; +} + +/** + * Constants for management dialog steps. + */ +export const MANAGEMENT_STEPS = { + AGENT_SELECTION: 1, + ACTION_SELECTION: 2, + AGENT_VIEWER: 3, + AGENT_EDITOR: 4, + DELETE_CONFIRMATION: 5, +} as const; diff --git a/packages/cli/src/ui/components/subagents/utils.ts b/packages/cli/src/ui/components/subagents/utils.ts new file mode 100644 index 00000000..5f777bf9 --- /dev/null +++ b/packages/cli/src/ui/components/subagents/utils.ts @@ -0,0 +1,9 @@ +import { COLOR_OPTIONS } from './constants.js'; + +export const shouldShowColor = (backgroundColor?: string): boolean => + backgroundColor !== undefined && backgroundColor !== 'auto'; + +export const getColorForDisplay = (colorName?: string): string | undefined => + !colorName || colorName === 'auto' + ? undefined + : COLOR_OPTIONS.find((color) => color.name === colorName)?.value; diff --git a/packages/cli/src/ui/components/subagents/wizardReducer.ts b/packages/cli/src/ui/components/subagents/wizardReducer.ts index 8fb21bed..a7f57a3f 100644 --- a/packages/cli/src/ui/components/subagents/wizardReducer.ts +++ b/packages/cli/src/ui/components/subagents/wizardReducer.ts @@ -4,9 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CreationWizardState, WizardAction } from './types.js'; +import { + CreationWizardState, + WizardAction, + ManagementDialogState, + ManagementAction, + MANAGEMENT_STEPS, +} from './types.js'; import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js'; +export { MANAGEMENT_STEPS }; + /** * Initial state for the creation wizard. */ @@ -163,3 +171,124 @@ function validateStep(step: number, state: CreationWizardState): boolean { return false; } } + +/** + * Initial state for the management dialog. + */ +export const initialManagementState: ManagementDialogState = { + currentStep: MANAGEMENT_STEPS.AGENT_SELECTION, + availableAgents: [], + selectedAgent: null, + selectedAgentIndex: -1, + selectedAction: null, + isLoading: false, + error: null, + canProceed: false, +}; + +/** + * Reducer for managing management dialog state transitions. + */ +export function managementReducer( + state: ManagementDialogState, + action: ManagementAction, +): ManagementDialogState { + switch (action.type) { + case 'SET_AVAILABLE_AGENTS': + return { + ...state, + availableAgents: action.payload, + canProceed: action.payload.length > 0, + }; + + case 'SELECT_AGENT': + return { + ...state, + selectedAgent: action.payload.agent, + selectedAgentIndex: action.payload.index, + canProceed: true, + }; + + case 'SELECT_ACTION': + return { + ...state, + selectedAction: action.payload, + canProceed: true, + }; + + case 'GO_TO_NEXT_STEP': { + const nextStep = state.currentStep + 1; + return { + ...state, + currentStep: nextStep, + canProceed: getCanProceedForStep(nextStep, state), + }; + } + + case 'GO_TO_PREVIOUS_STEP': { + const prevStep = Math.max( + MANAGEMENT_STEPS.AGENT_SELECTION, + state.currentStep - 1, + ); + return { + ...state, + currentStep: prevStep, + canProceed: getCanProceedForStep(prevStep, state), + }; + } + + case 'GO_TO_STEP': + return { + ...state, + currentStep: action.payload, + canProceed: getCanProceedForStep(action.payload, state), + }; + + case 'SET_LOADING': + return { + ...state, + isLoading: action.payload, + }; + + case 'SET_ERROR': + return { + ...state, + error: action.payload, + }; + + case 'SET_CAN_PROCEED': + return { + ...state, + canProceed: action.payload, + }; + + case 'RESET_DIALOG': + return initialManagementState; + + default: + return state; + } +} + +/** + * Validates whether a management step can proceed based on current state. + */ +function getCanProceedForStep( + step: number, + state: ManagementDialogState, +): boolean { + switch (step) { + case MANAGEMENT_STEPS.AGENT_SELECTION: + return state.availableAgents.length > 0 && state.selectedAgent !== null; + case MANAGEMENT_STEPS.ACTION_SELECTION: + return state.selectedAction !== null; + case MANAGEMENT_STEPS.AGENT_VIEWER: + return true; // Can always go back from viewer + case MANAGEMENT_STEPS.AGENT_EDITOR: + return true; // TODO: Add validation for editor + case MANAGEMENT_STEPS.DELETE_CONFIRMATION: + return true; // Can always proceed from confirmation + default: + return false; + } +} diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 6ed2d5f3..b94c95cd 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -145,6 +145,7 @@ describe('useSlashCommandProcessor', () => { vi.fn(), // openPrivacyNotice vi.fn(), // openSettingsDialog vi.fn(), // openSubagentCreateDialog + vi.fn(), // openAgentsManagerDialog vi.fn(), // toggleVimEnabled setIsProcessing, vi.fn(), // setGeminiMdFileCount @@ -898,6 +899,7 @@ describe('useSlashCommandProcessor', () => { vi.fn(), // openPrivacyNotice vi.fn(), // openSettingsDialog vi.fn(), // openSubagentCreateDialog + vi.fn(), // openAgentsManagerDialog vi.fn(), // toggleVimEnabled vi.fn(), // setIsProcessing vi.fn(), // setGeminiMdFileCount diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index c96f6d97..c9cb8dd1 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -52,6 +52,7 @@ export const useSlashCommandProcessor = ( openPrivacyNotice: () => void, openSettingsDialog: () => void, openSubagentCreateDialog: () => void, + openAgentsManagerDialog: () => void, toggleVimEnabled: () => Promise, setIsProcessing: (isProcessing: boolean) => void, setGeminiMdFileCount: (count: number) => void, @@ -352,16 +353,19 @@ export const useSlashCommandProcessor = ( toolArgs: result.toolArgs, }; case 'message': - addItem( - { - type: - result.messageType === 'error' - ? MessageType.ERROR - : MessageType.INFO, - text: result.content, - }, - Date.now(), - ); + if (result.messageType === 'info') { + addMessage({ + type: MessageType.INFO, + content: result.content, + timestamp: new Date(), + }); + } else { + addMessage({ + type: MessageType.ERROR, + content: result.content, + timestamp: new Date(), + }); + } return { type: 'handled' }; case 'dialog': switch (result.dialog) { @@ -383,6 +387,9 @@ export const useSlashCommandProcessor = ( case 'subagent_create': openSubagentCreateDialog(); return { type: 'handled' }; + case 'subagent_list': + openAgentsManagerDialog(); + return { type: 'handled' }; case 'help': return { type: 'handled' }; default: { @@ -558,6 +565,7 @@ export const useSlashCommandProcessor = ( setQuittingMessages, openSettingsDialog, openSubagentCreateDialog, + openAgentsManagerDialog, setShellConfirmationRequest, setSessionShellAllowlist, setIsProcessing, diff --git a/packages/cli/src/ui/hooks/useAgentsManagerDialog.ts b/packages/cli/src/ui/hooks/useAgentsManagerDialog.ts new file mode 100644 index 00000000..b5904af6 --- /dev/null +++ b/packages/cli/src/ui/hooks/useAgentsManagerDialog.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useCallback } from 'react'; + +export interface UseAgentsManagerDialogReturn { + isAgentsManagerDialogOpen: boolean; + openAgentsManagerDialog: () => void; + closeAgentsManagerDialog: () => void; +} + +export const useAgentsManagerDialog = (): UseAgentsManagerDialogReturn => { + const [isAgentsManagerDialogOpen, setIsAgentsManagerDialogOpen] = + useState(false); + + const openAgentsManagerDialog = useCallback(() => { + setIsAgentsManagerDialogOpen(true); + }, []); + + const closeAgentsManagerDialog = useCallback(() => { + setIsAgentsManagerDialogOpen(false); + }, []); + + return { + isAgentsManagerDialogOpen, + openAgentsManagerDialog, + closeAgentsManagerDialog, + }; +}; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 0b04c875..b0db59c8 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -54,6 +54,7 @@ import { } from '../services/fileSystemService.js'; import { logCliConfiguration, logIdeConnection } from '../telemetry/loggers.js'; import { IdeConnectionEvent, IdeConnectionType } from '../telemetry/types.js'; +import { SubagentManager } from '../subagents/subagent-manager.js'; // Re-export OAuth config type export type { MCPOAuthConfig }; @@ -316,6 +317,7 @@ export class Config { private readonly shouldUseNodePtyShell: boolean; private readonly skipNextSpeakerCheck: boolean; private initialized: boolean = false; + private subagentManager: SubagentManager | null = null; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -865,6 +867,13 @@ export class Config { return this.gitService; } + getSubagentManager(): SubagentManager { + if (!this.subagentManager) { + this.subagentManager = new SubagentManager(this.targetDir); + } + return this.subagentManager; + } + async createToolRegistry(): Promise { const registry = new ToolRegistry(this); diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index e07995bf..a2d94b96 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -111,12 +111,28 @@ export class SubagentManager { /** * Loads a subagent configuration by name. - * Searches project-level first, then user-level. + * If level is specified, only searches that level. + * If level is omitted, searches project-level first, then user-level. * * @param name - Name of the subagent to load + * @param level - Optional level to limit search to specific level * @returns SubagentConfig or null if not found */ - async loadSubagent(name: string): Promise { + async loadSubagent( + name: string, + level?: SubagentLevel, + ): Promise { + if (level) { + // Search only the specified level + const path = this.getSubagentPath(name, level); + try { + const config = await this.parseSubagentFile(path); + return config; + } catch (_error) { + return null; + } + } + // Try project level first const projectPath = this.getSubagentPath(name, 'project'); try { @@ -147,8 +163,9 @@ export class SubagentManager { async updateSubagent( name: string, updates: Partial, + level?: SubagentLevel, ): Promise { - const existing = await this.loadSubagent(name); + const existing = await this.loadSubagent(name, level); if (!existing) { throw new SubagentError( `Subagent "${name}" not found`, @@ -287,8 +304,11 @@ export class SubagentManager { * @param name - Name of the subagent to find * @returns SubagentMetadata or null if not found */ - async findSubagentByName(name: string): Promise { - const config = await this.loadSubagent(name); + async findSubagentByName( + name: string, + level?: SubagentLevel, + ): Promise { + const config = await this.loadSubagent(name, level); if (!config) { return null; } @@ -361,6 +381,9 @@ export class SubagentManager { const runConfig = frontmatter['runConfig'] as | Record | undefined; + const backgroundColor = frontmatter['backgroundColor'] as + | string + | undefined; // Determine level from file path // Project level paths contain the project root, user level paths are in home directory @@ -382,6 +405,7 @@ export class SubagentManager { runConfig: runConfig as Partial< import('../core/subagent.js').RunConfig >, + backgroundColor, }; // Validate the parsed configuration @@ -424,6 +448,10 @@ export class SubagentManager { frontmatter['runConfig'] = config.runConfig; } + if (config.backgroundColor && config.backgroundColor !== 'auto') { + frontmatter['backgroundColor'] = config.backgroundColor; + } + // Serialize to YAML const yamlContent = stringifyYaml(frontmatter, { lineWidth: 0, // Disable line wrapping @@ -616,7 +644,7 @@ export class SubagentManager { * @returns True if name is available */ async isNameAvailable(name: string, level?: SubagentLevel): Promise { - const existing = await this.loadSubagent(name); + const existing = await this.loadSubagent(name, level); if (!existing) { return true; // Name is available diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts index 5e61fccf..955112f6 100644 --- a/packages/core/src/subagents/types.ts +++ b/packages/core/src/subagents/types.ts @@ -59,6 +59,12 @@ export interface SubagentConfig { * Can specify max_time_minutes and max_turns. */ runConfig?: Partial; + + /** + * Optional background color for runtime display. + * If 'auto' or omitted, uses automatic color assignment. + */ + backgroundColor?: string; } /**