mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent list dialog - working
This commit is contained in:
@@ -25,6 +25,7 @@ import { useFolderTrust } from './hooks/useFolderTrust.js';
|
|||||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||||
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
||||||
import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js';
|
import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js';
|
||||||
|
import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js';
|
||||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||||
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
|
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
|
||||||
@@ -42,7 +43,10 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
|||||||
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
||||||
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
||||||
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.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 { Colors } from './colors.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
||||||
import { LoadedSettings, SettingScope } from '../config/settings.js';
|
import { LoadedSettings, SettingScope } from '../config/settings.js';
|
||||||
@@ -277,6 +281,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
closeSubagentCreateDialog,
|
closeSubagentCreateDialog,
|
||||||
} = useSubagentCreateDialog();
|
} = useSubagentCreateDialog();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isAgentsManagerDialogOpen,
|
||||||
|
openAgentsManagerDialog,
|
||||||
|
closeAgentsManagerDialog,
|
||||||
|
} = useAgentsManagerDialog();
|
||||||
|
|
||||||
const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust(
|
const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust(
|
||||||
settings,
|
settings,
|
||||||
setIsTrustedFolder,
|
setIsTrustedFolder,
|
||||||
@@ -574,6 +584,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
openPrivacyNotice,
|
openPrivacyNotice,
|
||||||
openSettingsDialog,
|
openSettingsDialog,
|
||||||
openSubagentCreateDialog,
|
openSubagentCreateDialog,
|
||||||
|
openAgentsManagerDialog,
|
||||||
toggleVimEnabled,
|
toggleVimEnabled,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
setGeminiMdFileCount,
|
setGeminiMdFileCount,
|
||||||
@@ -1087,6 +1098,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
config={config}
|
config={config}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
) : isAgentsManagerDialogOpen ? (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<AgentsManagerDialog
|
||||||
|
onClose={closeAgentsManagerDialog}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
) : isAuthenticating ? (
|
) : isAuthenticating ? (
|
||||||
<>
|
<>
|
||||||
{isQwenAuth && isQwenAuthenticating ? (
|
{isQwenAuth && isQwenAuthenticating ? (
|
||||||
|
|||||||
@@ -4,19 +4,22 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MessageType } from '../types.js';
|
import { CommandKind, SlashCommand, OpenDialogActionReturn } from './types.js';
|
||||||
import {
|
|
||||||
CommandKind,
|
|
||||||
SlashCommand,
|
|
||||||
SlashCommandActionReturn,
|
|
||||||
OpenDialogActionReturn,
|
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
export const agentsCommand: SlashCommand = {
|
export const agentsCommand: SlashCommand = {
|
||||||
name: 'agents',
|
name: 'agents',
|
||||||
description: 'Manage subagents for specialized task delegation.',
|
description: 'Manage subagents for specialized task delegation.',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
subCommands: [
|
subCommands: [
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
description: 'Manage existing subagents (view, edit, delete).',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
action: (): OpenDialogActionReturn => ({
|
||||||
|
type: 'dialog',
|
||||||
|
dialog: 'subagent_list',
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'create',
|
name: 'create',
|
||||||
description: 'Create a new subagent with guided setup.',
|
description: 'Create a new subagent with guided setup.',
|
||||||
@@ -26,94 +29,5 @@ export const agentsCommand: SlashCommand = {
|
|||||||
dialog: 'subagent_create',
|
dialog: 'subagent_create',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'list',
|
|
||||||
description: 'List all available subagents.',
|
|
||||||
kind: CommandKind.BUILT_IN,
|
|
||||||
action: async (context): Promise<SlashCommandActionReturn | void> => {
|
|
||||||
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<SlashCommandActionReturn | void> => {
|
|
||||||
if (!args || args.trim() === '') {
|
|
||||||
return {
|
|
||||||
type: 'message',
|
|
||||||
messageType: 'error',
|
|
||||||
content: 'Usage: /agents show <subagent-name>',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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<SlashCommandActionReturn | void> => {
|
|
||||||
if (!args || args.trim() === '') {
|
|
||||||
return {
|
|
||||||
type: 'message',
|
|
||||||
messageType: 'error',
|
|
||||||
content: 'Usage: /agents edit <subagent-name>',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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<SlashCommandActionReturn | void> => {
|
|
||||||
if (!args || args.trim() === '') {
|
|
||||||
return {
|
|
||||||
type: 'message',
|
|
||||||
messageType: 'error',
|
|
||||||
content: 'Usage: /agents delete <subagent-name>',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
context.ui.addItem(
|
|
||||||
{
|
|
||||||
type: MessageType.INFO,
|
|
||||||
text: `Deleting subagent: ${args.trim()} (not implemented yet)`,
|
|
||||||
},
|
|
||||||
Date.now(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ export interface OpenDialogActionReturn {
|
|||||||
| 'editor'
|
| 'editor'
|
||||||
| 'privacy'
|
| 'privacy'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
| 'subagent_create';
|
| 'subagent_create'
|
||||||
|
| 'subagent_list';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<RadioButtonSelect
|
||||||
|
items={actions}
|
||||||
|
initialIndex={selectedIndex}
|
||||||
|
onSelect={handleActionSelect}
|
||||||
|
showNumbers={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
305
packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx
Normal file
305
packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx
Normal file
@@ -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<NavigationState>({
|
||||||
|
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 (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.text.secondary}>Loading agents...</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.error) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.status.error}>{state.error}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.availableAgents.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color={theme.text.secondary}>No subagents found.</Text>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
Use '/agents create' to create your first subagent.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<Box key={agent.name} alignItems="center">
|
||||||
|
<Box minWidth={2} flexShrink={0}>
|
||||||
|
<Text color={isSelected ? theme.text.accent : theme.text.primary}>
|
||||||
|
{isSelected ? '●' : ' '}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text color={textColor} wrap="truncate">
|
||||||
|
{agent.name}
|
||||||
|
{agent.level === 'user' && projectNames.has(agent.name) && (
|
||||||
|
<Text color={isSelected ? theme.status.warning : Colors.Gray}>
|
||||||
|
{' '}
|
||||||
|
(overridden by project level agent)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate enabled agents count (excluding conflicted user-level agents)
|
||||||
|
const enabledAgentsCount =
|
||||||
|
projectAgents.length +
|
||||||
|
userAgents.filter((agent) => !projectNames.has(agent.name)).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{/* Project Level Agents */}
|
||||||
|
{projectAgents.length > 0 && (
|
||||||
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
|
<Text color={theme.text.primary} bold>
|
||||||
|
Project Level ({projectAgents[0].filePath.replace(/\/[^/]+$/, '')})
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={1} flexDirection="column">
|
||||||
|
{projectAgents.map((agent, index) => {
|
||||||
|
const isSelected =
|
||||||
|
navigation.currentBlock === 'project' &&
|
||||||
|
navigation.projectIndex === index;
|
||||||
|
return renderAgentItem(agent, index, isSelected);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* User Level Agents */}
|
||||||
|
{userAgents.length > 0 && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color={theme.text.primary} bold>
|
||||||
|
User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')})
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={1} flexDirection="column">
|
||||||
|
{userAgents.map((agent, index) => {
|
||||||
|
const isSelected =
|
||||||
|
navigation.currentBlock === 'user' &&
|
||||||
|
navigation.userIndex === index;
|
||||||
|
return renderAgentItem(agent, index, isSelected);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agent count summary */}
|
||||||
|
{(projectAgents.length > 0 || userAgents.length > 0) && (
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
Using: {enabledAgentsCount} agents
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
79
packages/cli/src/ui/components/subagents/AgentViewerStep.tsx
Normal file
79
packages/cli/src/ui/components/subagents/AgentViewerStep.tsx
Normal file
@@ -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 (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.status.error}>No agent selected</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = state.selectedAgent;
|
||||||
|
|
||||||
|
const toolsDisplay = agent.tools ? agent.tools.join(', ') : '*';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Box>
|
||||||
|
<Text bold>Location: </Text>
|
||||||
|
<Text>
|
||||||
|
{agent.level === 'project'
|
||||||
|
? 'Project Level (.qwen/agents/)'
|
||||||
|
: 'User Level (~/.qwen/agents/)'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text bold>File Path: </Text>
|
||||||
|
<Text>{agent.filePath}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text bold>Tools: </Text>
|
||||||
|
<Text>{toolsDisplay}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{shouldShowColor(agent.backgroundColor) && (
|
||||||
|
<Box>
|
||||||
|
<Text bold>Color: </Text>
|
||||||
|
<Box backgroundColor={getColorForDisplay(agent.backgroundColor)}>
|
||||||
|
<Text color="black">{` ${agent.name} `}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text bold>Description:</Text>
|
||||||
|
</Box>
|
||||||
|
<Box padding={1} paddingBottom={0}>
|
||||||
|
<Text wrap="wrap">{agent.description}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text bold>System Prompt:</Text>
|
||||||
|
</Box>
|
||||||
|
<Box padding={1} paddingBottom={0}>
|
||||||
|
<Text wrap="wrap">{agent.systemPrompt}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
178
packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx
Normal file
178
packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx
Normal file
@@ -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 (
|
||||||
|
<Box>
|
||||||
|
<Text bold>{getStepHeaderText()}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}, [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 (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.text.secondary}>{getNavigationInstructions()}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}, [state.currentStep, state.availableAgents.length]);
|
||||||
|
|
||||||
|
const renderStepContent = useCallback(() => {
|
||||||
|
switch (state.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>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.status.warning}>
|
||||||
|
Agent deletion not yet implemented
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text color={theme.status.error}>
|
||||||
|
Invalid step: {state.currentStep}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [stepProps, state.currentStep]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{/* Main content wrapped in bounding box */}
|
||||||
|
<Box
|
||||||
|
borderStyle="single"
|
||||||
|
borderColor={Colors.Gray}
|
||||||
|
flexDirection="column"
|
||||||
|
padding={1}
|
||||||
|
width="100%"
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
|
{renderStepHeader()}
|
||||||
|
{renderStepContent()}
|
||||||
|
{renderStepFooter()}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,44 +8,9 @@ import { Box, Text } from 'ink';
|
|||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||||
import { WizardStepProps, ColorOption } from './types.js';
|
import { WizardStepProps, ColorOption } from './types.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../colors.js';
|
||||||
|
import { COLOR_OPTIONS } from './constants.js';
|
||||||
|
|
||||||
const colorOptions: ColorOption[] = [
|
const colorOptions: ColorOption[] = 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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 5: Background color selection with preview.
|
* Step 5: Background color selection with preview.
|
||||||
@@ -65,12 +30,12 @@ export function ColorSelector({
|
|||||||
(option) => option.id === selectedValue,
|
(option) => option.id === selectedValue,
|
||||||
);
|
);
|
||||||
if (colorOption) {
|
if (colorOption) {
|
||||||
dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.value });
|
dispatch({ type: 'SET_BACKGROUND_COLOR', color: colorOption.name });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentColor =
|
const currentColor =
|
||||||
colorOptions.find((option) => option.value === state.backgroundColor) ||
|
colorOptions.find((option) => option.name === state.backgroundColor) ||
|
||||||
colorOptions[0];
|
colorOptions[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,8 +57,8 @@ export function ColorSelector({
|
|||||||
|
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Text color={Colors.Gray}>Preview:</Text>
|
<Text color={Colors.Gray}>Preview:</Text>
|
||||||
<Box marginLeft={2}>
|
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
||||||
<Text color={currentColor.value}>{state.generatedName}</Text>
|
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||||
import { spawnSync } from 'child_process';
|
import { spawnSync } from 'child_process';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 6: Final confirmation and actions.
|
* Step 6: Final confirmation and actions.
|
||||||
@@ -47,8 +48,7 @@ export function CreationSummary({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get project root from config
|
// Get project root from config
|
||||||
const projectRoot = config.getProjectRoot();
|
const subagentManager = config.getSubagentManager();
|
||||||
const subagentManager = new SubagentManager(projectRoot);
|
|
||||||
|
|
||||||
// Check for name conflicts
|
// Check for name conflicts
|
||||||
const isAvailable = await subagentManager.isNameAvailable(
|
const isAvailable = await subagentManager.isNameAvailable(
|
||||||
@@ -130,8 +130,7 @@ export function CreationSummary({
|
|||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('Configuration not available');
|
throw new Error('Configuration not available');
|
||||||
}
|
}
|
||||||
const projectRoot = config.getProjectRoot();
|
const subagentManager = config.getSubagentManager();
|
||||||
const subagentManager = new SubagentManager(projectRoot);
|
|
||||||
|
|
||||||
// Build subagent configuration
|
// Build subagent configuration
|
||||||
const subagentConfig: SubagentConfig = {
|
const subagentConfig: SubagentConfig = {
|
||||||
@@ -143,6 +142,7 @@ export function CreationSummary({
|
|||||||
tools: Array.isArray(state.selectedTools)
|
tools: Array.isArray(state.selectedTools)
|
||||||
? state.selectedTools
|
? state.selectedTools
|
||||||
: undefined,
|
: undefined,
|
||||||
|
backgroundColor: state.backgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the subagent
|
// Create the subagent
|
||||||
@@ -316,6 +316,15 @@ export function CreationSummary({
|
|||||||
<Text>{toolsDisplay}</Text>
|
<Text>{toolsDisplay}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{shouldShowColor(state.backgroundColor) && (
|
||||||
|
<Box>
|
||||||
|
<Text bold>Color: </Text>
|
||||||
|
<Box backgroundColor={getColorForDisplay(state.backgroundColor)}>
|
||||||
|
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text bold>Description:</Text>
|
<Text bold>Description:</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -337,11 +346,11 @@ export function CreationSummary({
|
|||||||
|
|
||||||
{saveError && (
|
{saveError && (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text bold color="red">
|
<Text bold color={theme.status.error}>
|
||||||
❌ Error saving subagent:
|
❌ Error saving subagent:
|
||||||
</Text>
|
</Text>
|
||||||
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||||
<Text color="red" wrap="wrap">
|
<Text color={theme.status.error} wrap="wrap">
|
||||||
{saveError}
|
{saveError}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export function DescriptionInput({
|
|||||||
{state.validationErrors.length > 0 && (
|
{state.validationErrors.length > 0 && (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{state.validationErrors.map((error, index) => (
|
{state.validationErrors.map((error, index) => (
|
||||||
<Text key={index} color="red">
|
<Text key={index} color={theme.status.error}>
|
||||||
⚠ {error}
|
⚠ {error}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Box flexDirection="column" gap={1}>
|
|
||||||
<Box>
|
|
||||||
<Text bold color="red">
|
|
||||||
❌ Subagent Wizard Error
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text>
|
|
||||||
An unexpected error occurred in the subagent creation wizard.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{this.state.error && (
|
|
||||||
<Box flexDirection="column" marginTop={1}>
|
|
||||||
<Text bold color="yellow">
|
|
||||||
Error Details:
|
|
||||||
</Text>
|
|
||||||
<Text color="gray" wrap="wrap">
|
|
||||||
{this.state.error.message}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box marginTop={1}>
|
|
||||||
<Text color="gray">
|
|
||||||
Press <Text color="cyan">Esc</Text> to close the wizard and try
|
|
||||||
again.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{process.env['NODE_ENV'] === 'development' &&
|
|
||||||
this.state.errorInfo && (
|
|
||||||
<Box flexDirection="column" marginTop={1}>
|
|
||||||
<Text bold color="yellow">
|
|
||||||
Stack Trace (Development):
|
|
||||||
</Text>
|
|
||||||
<Text color="gray" wrap="wrap">
|
|
||||||
{this.state.errorInfo.componentStack}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,13 @@
|
|||||||
|
|
||||||
import { useReducer, useCallback, useMemo } from 'react';
|
import { useReducer, useCallback, useMemo } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { wizardReducer, initialWizardState } from './wizardReducer.js';
|
import { wizardReducer, initialWizardState } from './reducers.js';
|
||||||
import { LocationSelector } from './LocationSelector.js';
|
import { LocationSelector } from './LocationSelector.js';
|
||||||
import { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
import { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
||||||
import { DescriptionInput } from './DescriptionInput.js';
|
import { DescriptionInput } from './DescriptionInput.js';
|
||||||
import { ToolSelector } from './ToolSelector.js';
|
import { ToolSelector } from './ToolSelector.js';
|
||||||
import { ColorSelector } from './ColorSelector.js';
|
import { ColorSelector } from './ColorSelector.js';
|
||||||
import { CreationSummary } from './CreationSummary.js';
|
import { CreationSummary } from './CreationSummary.js';
|
||||||
import { ErrorBoundary } from './ErrorBoundary.js';
|
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from './types.js';
|
||||||
import { WIZARD_STEPS } from './constants.js';
|
import { WIZARD_STEPS } from './constants.js';
|
||||||
import { Config } from '@qwen-code/qwen-code-core';
|
import { Config } from '@qwen-code/qwen-code-core';
|
||||||
@@ -113,9 +112,9 @@ export function SubagentCreationWizard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderStyle="single" borderColor="yellow" padding={1}>
|
<Box borderStyle="single" borderColor={theme.status.warning} padding={1}>
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text color="yellow" bold>
|
<Text color={theme.status.warning} bold>
|
||||||
Debug Info:
|
Debug Info:
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={Colors.Gray}>Step: {state.currentStep}</Text>
|
<Text color={Colors.Gray}>Step: {state.currentStep}</Text>
|
||||||
@@ -128,7 +127,9 @@ export function SubagentCreationWizard({
|
|||||||
<Text color={Colors.Gray}>Location: {state.location}</Text>
|
<Text color={Colors.Gray}>Location: {state.location}</Text>
|
||||||
<Text color={Colors.Gray}>Method: {state.generationMethod}</Text>
|
<Text color={Colors.Gray}>Method: {state.generationMethod}</Text>
|
||||||
{state.validationErrors.length > 0 && (
|
{state.validationErrors.length > 0 && (
|
||||||
<Text color="red">Errors: {state.validationErrors.join(', ')}</Text>
|
<Text color={theme.status.error}>
|
||||||
|
Errors: {state.validationErrors.join(', ')}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -201,19 +202,15 @@ export function SubagentCreationWizard({
|
|||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Text color="red">Invalid step: {state.currentStep}</Text>
|
<Text color={theme.status.error}>
|
||||||
|
Invalid step: {state.currentStep}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [stepProps, state.currentStep]);
|
}, [stepProps, state.currentStep]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
|
||||||
onError={(error, errorInfo) => {
|
|
||||||
// Additional error handling if needed
|
|
||||||
console.error('Subagent wizard error:', error, errorInfo);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{/* Main content wrapped in bounding box */}
|
{/* Main content wrapped in bounding box */}
|
||||||
<Box
|
<Box
|
||||||
@@ -230,6 +227,5 @@ export function SubagentCreationWizard({
|
|||||||
{renderStepFooter()}
|
{renderStepFooter()}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,3 +30,42 @@ export const STEP_NAMES: Record<number, string> = {
|
|||||||
[WIZARD_STEPS.COLOR_SELECTION]: 'Color Selection',
|
[WIZARD_STEPS.COLOR_SELECTION]: 'Color Selection',
|
||||||
[WIZARD_STEPS.FINAL_CONFIRMATION]: 'Final Confirmation',
|
[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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Creation Wizard Components
|
||||||
export { SubagentCreationWizard } from './SubagentCreationWizard.js';
|
export { SubagentCreationWizard } from './SubagentCreationWizard.js';
|
||||||
export { LocationSelector } from './LocationSelector.js';
|
export { LocationSelector } from './LocationSelector.js';
|
||||||
export { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
export { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
||||||
@@ -12,6 +13,13 @@ export { ToolSelector } from './ToolSelector.js';
|
|||||||
export { ColorSelector } from './ColorSelector.js';
|
export { ColorSelector } from './ColorSelector.js';
|
||||||
export { CreationSummary } from './CreationSummary.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 {
|
export type {
|
||||||
CreationWizardState,
|
CreationWizardState,
|
||||||
WizardAction,
|
WizardAction,
|
||||||
@@ -21,4 +29,17 @@ export type {
|
|||||||
ColorOption,
|
ColorOption,
|
||||||
} from './types.js';
|
} 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';
|
||||||
|
|||||||
294
packages/cli/src/ui/components/subagents/reducers.tsx
Normal file
294
packages/cli/src/ui/components/subagents/reducers.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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.
|
* State management for the subagent creation wizard.
|
||||||
@@ -110,3 +115,55 @@ export interface WizardResult {
|
|||||||
tools?: string[];
|
tools?: string[];
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State management for the subagent management dialog.
|
||||||
|
*/
|
||||||
|
export interface ManagementDialogState {
|
||||||
|
currentStep: number;
|
||||||
|
availableAgents: SubagentMetadata[];
|
||||||
|
selectedAgent: SubagentConfig | null;
|
||||||
|
selectedAgentIndex: number;
|
||||||
|
selectedAction: 'view' | 'edit' | 'delete' | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
canProceed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions that can be dispatched to update management dialog state.
|
||||||
|
*/
|
||||||
|
export type ManagementAction =
|
||||||
|
| { type: 'SET_AVAILABLE_AGENTS'; payload: SubagentMetadata[] }
|
||||||
|
| { type: 'SELECT_AGENT'; payload: { agent: SubagentConfig; index: number } }
|
||||||
|
| { type: 'SELECT_ACTION'; payload: 'view' | 'edit' | 'delete' }
|
||||||
|
| { type: 'GO_TO_NEXT_STEP' }
|
||||||
|
| { type: 'GO_TO_PREVIOUS_STEP' }
|
||||||
|
| { type: 'GO_TO_STEP'; payload: number }
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'SET_CAN_PROCEED'; payload: boolean }
|
||||||
|
| { type: 'RESET_DIALOG' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for management dialog step components.
|
||||||
|
*/
|
||||||
|
export interface ManagementStepProps {
|
||||||
|
state: ManagementDialogState;
|
||||||
|
dispatch: React.Dispatch<ManagementAction>;
|
||||||
|
onNext: () => void;
|
||||||
|
onPrevious: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
config: Config | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for management dialog steps.
|
||||||
|
*/
|
||||||
|
export const MANAGEMENT_STEPS = {
|
||||||
|
AGENT_SELECTION: 1,
|
||||||
|
ACTION_SELECTION: 2,
|
||||||
|
AGENT_VIEWER: 3,
|
||||||
|
AGENT_EDITOR: 4,
|
||||||
|
DELETE_CONFIRMATION: 5,
|
||||||
|
} as const;
|
||||||
|
|||||||
9
packages/cli/src/ui/components/subagents/utils.ts
Normal file
9
packages/cli/src/ui/components/subagents/utils.ts
Normal file
@@ -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;
|
||||||
@@ -4,9 +4,17 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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';
|
import { WIZARD_STEPS, TOTAL_WIZARD_STEPS } from './constants.js';
|
||||||
|
|
||||||
|
export { MANAGEMENT_STEPS };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial state for the creation wizard.
|
* Initial state for the creation wizard.
|
||||||
*/
|
*/
|
||||||
@@ -163,3 +171,124 @@ function validateStep(step: number, state: CreationWizardState): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial state for the management dialog.
|
||||||
|
*/
|
||||||
|
export const initialManagementState: ManagementDialogState = {
|
||||||
|
currentStep: MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||||
|
availableAgents: [],
|
||||||
|
selectedAgent: null,
|
||||||
|
selectedAgentIndex: -1,
|
||||||
|
selectedAction: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
canProceed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer for managing management dialog state transitions.
|
||||||
|
*/
|
||||||
|
export function managementReducer(
|
||||||
|
state: ManagementDialogState,
|
||||||
|
action: ManagementAction,
|
||||||
|
): ManagementDialogState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_AVAILABLE_AGENTS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
availableAgents: action.payload,
|
||||||
|
canProceed: action.payload.length > 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SELECT_AGENT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedAgent: action.payload.agent,
|
||||||
|
selectedAgentIndex: action.payload.index,
|
||||||
|
canProceed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SELECT_ACTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedAction: action.payload,
|
||||||
|
canProceed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'GO_TO_NEXT_STEP': {
|
||||||
|
const nextStep = state.currentStep + 1;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentStep: nextStep,
|
||||||
|
canProceed: getCanProceedForStep(nextStep, state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'GO_TO_PREVIOUS_STEP': {
|
||||||
|
const prevStep = Math.max(
|
||||||
|
MANAGEMENT_STEPS.AGENT_SELECTION,
|
||||||
|
state.currentStep - 1,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentStep: prevStep,
|
||||||
|
canProceed: getCanProceedForStep(prevStep, state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'GO_TO_STEP':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentStep: action.payload,
|
||||||
|
canProceed: getCanProceedForStep(action.payload, state),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_CAN_PROCEED':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
canProceed: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'RESET_DIALOG':
|
||||||
|
return initialManagementState;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether a management step can proceed based on current state.
|
||||||
|
*/
|
||||||
|
function getCanProceedForStep(
|
||||||
|
step: number,
|
||||||
|
state: ManagementDialogState,
|
||||||
|
): boolean {
|
||||||
|
switch (step) {
|
||||||
|
case MANAGEMENT_STEPS.AGENT_SELECTION:
|
||||||
|
return state.availableAgents.length > 0 && state.selectedAgent !== null;
|
||||||
|
case MANAGEMENT_STEPS.ACTION_SELECTION:
|
||||||
|
return state.selectedAction !== null;
|
||||||
|
case MANAGEMENT_STEPS.AGENT_VIEWER:
|
||||||
|
return true; // Can always go back from viewer
|
||||||
|
case MANAGEMENT_STEPS.AGENT_EDITOR:
|
||||||
|
return true; // TODO: Add validation for editor
|
||||||
|
case MANAGEMENT_STEPS.DELETE_CONFIRMATION:
|
||||||
|
return true; // Can always proceed from confirmation
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
vi.fn(), // openPrivacyNotice
|
vi.fn(), // openPrivacyNotice
|
||||||
vi.fn(), // openSettingsDialog
|
vi.fn(), // openSettingsDialog
|
||||||
vi.fn(), // openSubagentCreateDialog
|
vi.fn(), // openSubagentCreateDialog
|
||||||
|
vi.fn(), // openAgentsManagerDialog
|
||||||
vi.fn(), // toggleVimEnabled
|
vi.fn(), // toggleVimEnabled
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
vi.fn(), // setGeminiMdFileCount
|
vi.fn(), // setGeminiMdFileCount
|
||||||
@@ -898,6 +899,7 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
vi.fn(), // openPrivacyNotice
|
vi.fn(), // openPrivacyNotice
|
||||||
vi.fn(), // openSettingsDialog
|
vi.fn(), // openSettingsDialog
|
||||||
vi.fn(), // openSubagentCreateDialog
|
vi.fn(), // openSubagentCreateDialog
|
||||||
|
vi.fn(), // openAgentsManagerDialog
|
||||||
vi.fn(), // toggleVimEnabled
|
vi.fn(), // toggleVimEnabled
|
||||||
vi.fn(), // setIsProcessing
|
vi.fn(), // setIsProcessing
|
||||||
vi.fn(), // setGeminiMdFileCount
|
vi.fn(), // setGeminiMdFileCount
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const useSlashCommandProcessor = (
|
|||||||
openPrivacyNotice: () => void,
|
openPrivacyNotice: () => void,
|
||||||
openSettingsDialog: () => void,
|
openSettingsDialog: () => void,
|
||||||
openSubagentCreateDialog: () => void,
|
openSubagentCreateDialog: () => void,
|
||||||
|
openAgentsManagerDialog: () => void,
|
||||||
toggleVimEnabled: () => Promise<boolean>,
|
toggleVimEnabled: () => Promise<boolean>,
|
||||||
setIsProcessing: (isProcessing: boolean) => void,
|
setIsProcessing: (isProcessing: boolean) => void,
|
||||||
setGeminiMdFileCount: (count: number) => void,
|
setGeminiMdFileCount: (count: number) => void,
|
||||||
@@ -352,16 +353,19 @@ export const useSlashCommandProcessor = (
|
|||||||
toolArgs: result.toolArgs,
|
toolArgs: result.toolArgs,
|
||||||
};
|
};
|
||||||
case 'message':
|
case 'message':
|
||||||
addItem(
|
if (result.messageType === 'info') {
|
||||||
{
|
addMessage({
|
||||||
type:
|
type: MessageType.INFO,
|
||||||
result.messageType === 'error'
|
content: result.content,
|
||||||
? MessageType.ERROR
|
timestamp: new Date(),
|
||||||
: MessageType.INFO,
|
});
|
||||||
text: result.content,
|
} else {
|
||||||
},
|
addMessage({
|
||||||
Date.now(),
|
type: MessageType.ERROR,
|
||||||
);
|
content: result.content,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
case 'dialog':
|
case 'dialog':
|
||||||
switch (result.dialog) {
|
switch (result.dialog) {
|
||||||
@@ -383,6 +387,9 @@ export const useSlashCommandProcessor = (
|
|||||||
case 'subagent_create':
|
case 'subagent_create':
|
||||||
openSubagentCreateDialog();
|
openSubagentCreateDialog();
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
|
case 'subagent_list':
|
||||||
|
openAgentsManagerDialog();
|
||||||
|
return { type: 'handled' };
|
||||||
case 'help':
|
case 'help':
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
default: {
|
default: {
|
||||||
@@ -558,6 +565,7 @@ export const useSlashCommandProcessor = (
|
|||||||
setQuittingMessages,
|
setQuittingMessages,
|
||||||
openSettingsDialog,
|
openSettingsDialog,
|
||||||
openSubagentCreateDialog,
|
openSubagentCreateDialog,
|
||||||
|
openAgentsManagerDialog,
|
||||||
setShellConfirmationRequest,
|
setShellConfirmationRequest,
|
||||||
setSessionShellAllowlist,
|
setSessionShellAllowlist,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
|
|||||||
32
packages/cli/src/ui/hooks/useAgentsManagerDialog.ts
Normal file
32
packages/cli/src/ui/hooks/useAgentsManagerDialog.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
} from '../services/fileSystemService.js';
|
} from '../services/fileSystemService.js';
|
||||||
import { logCliConfiguration, logIdeConnection } from '../telemetry/loggers.js';
|
import { logCliConfiguration, logIdeConnection } from '../telemetry/loggers.js';
|
||||||
import { IdeConnectionEvent, IdeConnectionType } from '../telemetry/types.js';
|
import { IdeConnectionEvent, IdeConnectionType } from '../telemetry/types.js';
|
||||||
|
import { SubagentManager } from '../subagents/subagent-manager.js';
|
||||||
|
|
||||||
// Re-export OAuth config type
|
// Re-export OAuth config type
|
||||||
export type { MCPOAuthConfig };
|
export type { MCPOAuthConfig };
|
||||||
@@ -316,6 +317,7 @@ export class Config {
|
|||||||
private readonly shouldUseNodePtyShell: boolean;
|
private readonly shouldUseNodePtyShell: boolean;
|
||||||
private readonly skipNextSpeakerCheck: boolean;
|
private readonly skipNextSpeakerCheck: boolean;
|
||||||
private initialized: boolean = false;
|
private initialized: boolean = false;
|
||||||
|
private subagentManager: SubagentManager | null = null;
|
||||||
|
|
||||||
constructor(params: ConfigParameters) {
|
constructor(params: ConfigParameters) {
|
||||||
this.sessionId = params.sessionId;
|
this.sessionId = params.sessionId;
|
||||||
@@ -865,6 +867,13 @@ export class Config {
|
|||||||
return this.gitService;
|
return this.gitService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSubagentManager(): SubagentManager {
|
||||||
|
if (!this.subagentManager) {
|
||||||
|
this.subagentManager = new SubagentManager(this.targetDir);
|
||||||
|
}
|
||||||
|
return this.subagentManager;
|
||||||
|
}
|
||||||
|
|
||||||
async createToolRegistry(): Promise<ToolRegistry> {
|
async createToolRegistry(): Promise<ToolRegistry> {
|
||||||
const registry = new ToolRegistry(this);
|
const registry = new ToolRegistry(this);
|
||||||
|
|
||||||
|
|||||||
@@ -111,12 +111,28 @@ export class SubagentManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a subagent configuration by name.
|
* 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 name - Name of the subagent to load
|
||||||
|
* @param level - Optional level to limit search to specific level
|
||||||
* @returns SubagentConfig or null if not found
|
* @returns SubagentConfig or null if not found
|
||||||
*/
|
*/
|
||||||
async loadSubagent(name: string): Promise<SubagentConfig | null> {
|
async loadSubagent(
|
||||||
|
name: string,
|
||||||
|
level?: SubagentLevel,
|
||||||
|
): Promise<SubagentConfig | null> {
|
||||||
|
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
|
// Try project level first
|
||||||
const projectPath = this.getSubagentPath(name, 'project');
|
const projectPath = this.getSubagentPath(name, 'project');
|
||||||
try {
|
try {
|
||||||
@@ -147,8 +163,9 @@ export class SubagentManager {
|
|||||||
async updateSubagent(
|
async updateSubagent(
|
||||||
name: string,
|
name: string,
|
||||||
updates: Partial<SubagentConfig>,
|
updates: Partial<SubagentConfig>,
|
||||||
|
level?: SubagentLevel,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const existing = await this.loadSubagent(name);
|
const existing = await this.loadSubagent(name, level);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new SubagentError(
|
throw new SubagentError(
|
||||||
`Subagent "${name}" not found`,
|
`Subagent "${name}" not found`,
|
||||||
@@ -287,8 +304,11 @@ export class SubagentManager {
|
|||||||
* @param name - Name of the subagent to find
|
* @param name - Name of the subagent to find
|
||||||
* @returns SubagentMetadata or null if not found
|
* @returns SubagentMetadata or null if not found
|
||||||
*/
|
*/
|
||||||
async findSubagentByName(name: string): Promise<SubagentMetadata | null> {
|
async findSubagentByName(
|
||||||
const config = await this.loadSubagent(name);
|
name: string,
|
||||||
|
level?: SubagentLevel,
|
||||||
|
): Promise<SubagentMetadata | null> {
|
||||||
|
const config = await this.loadSubagent(name, level);
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -361,6 +381,9 @@ export class SubagentManager {
|
|||||||
const runConfig = frontmatter['runConfig'] as
|
const runConfig = frontmatter['runConfig'] as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
const backgroundColor = frontmatter['backgroundColor'] as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
// Determine level from file path
|
// Determine level from file path
|
||||||
// Project level paths contain the project root, user level paths are in home directory
|
// 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<
|
runConfig: runConfig as Partial<
|
||||||
import('../core/subagent.js').RunConfig
|
import('../core/subagent.js').RunConfig
|
||||||
>,
|
>,
|
||||||
|
backgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate the parsed configuration
|
// Validate the parsed configuration
|
||||||
@@ -424,6 +448,10 @@ export class SubagentManager {
|
|||||||
frontmatter['runConfig'] = config.runConfig;
|
frontmatter['runConfig'] = config.runConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.backgroundColor && config.backgroundColor !== 'auto') {
|
||||||
|
frontmatter['backgroundColor'] = config.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize to YAML
|
// Serialize to YAML
|
||||||
const yamlContent = stringifyYaml(frontmatter, {
|
const yamlContent = stringifyYaml(frontmatter, {
|
||||||
lineWidth: 0, // Disable line wrapping
|
lineWidth: 0, // Disable line wrapping
|
||||||
@@ -616,7 +644,7 @@ export class SubagentManager {
|
|||||||
* @returns True if name is available
|
* @returns True if name is available
|
||||||
*/
|
*/
|
||||||
async isNameAvailable(name: string, level?: SubagentLevel): Promise<boolean> {
|
async isNameAvailable(name: string, level?: SubagentLevel): Promise<boolean> {
|
||||||
const existing = await this.loadSubagent(name);
|
const existing = await this.loadSubagent(name, level);
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return true; // Name is available
|
return true; // Name is available
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ export interface SubagentConfig {
|
|||||||
* Can specify max_time_minutes and max_turns.
|
* Can specify max_time_minutes and max_turns.
|
||||||
*/
|
*/
|
||||||
runConfig?: Partial<RunConfig>;
|
runConfig?: Partial<RunConfig>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional background color for runtime display.
|
||||||
|
* If 'auto' or omitted, uses automatic color assignment.
|
||||||
|
*/
|
||||||
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user