mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent creation dialog - continued
This commit is contained in:
@@ -4,12 +4,18 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput, useStdin } from 'ink';
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from './types.js';
|
||||||
import { UI } from './constants.js';
|
|
||||||
import { validateSubagentConfig } from './validation.js';
|
import { validateSubagentConfig } from './validation.js';
|
||||||
import { SubagentManager, SubagentConfig } from '@qwen-code/qwen-code-core';
|
import {
|
||||||
|
SubagentManager,
|
||||||
|
SubagentConfig,
|
||||||
|
EditorType,
|
||||||
|
} from '@qwen-code/qwen-code-core';
|
||||||
|
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 6: Final confirmation and actions.
|
* Step 6: Final confirmation and actions.
|
||||||
@@ -18,27 +24,95 @@ export function CreationSummary({
|
|||||||
state,
|
state,
|
||||||
onPrevious: _onPrevious,
|
onPrevious: _onPrevious,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
config,
|
||||||
}: WizardStepProps) {
|
}: WizardStepProps) {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
const [warnings, setWarnings] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const settings = useSettings();
|
||||||
|
const { stdin, setRawMode } = useStdin();
|
||||||
|
|
||||||
const truncateText = (text: string, maxLength: number): string => {
|
const truncateText = (text: string, maxLength: number): string => {
|
||||||
if (text.length <= maxLength) return text;
|
if (text.length <= maxLength) return text;
|
||||||
return text.substring(0, maxLength - 3) + '...';
|
return text.substring(0, maxLength - 3) + '...';
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolsDisplay = Array.isArray(state.selectedTools)
|
// Check for warnings
|
||||||
? state.selectedTools.join(', ')
|
useEffect(() => {
|
||||||
: 'All available tools';
|
const checkWarnings = async () => {
|
||||||
|
if (!config || !state.generatedName) return;
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const allWarnings: string[] = [];
|
||||||
if (isSaving) return;
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
setSaveError(null);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get project root from config
|
||||||
|
const projectRoot = config.getProjectRoot();
|
||||||
|
const subagentManager = new SubagentManager(projectRoot);
|
||||||
|
|
||||||
|
// Check for name conflicts
|
||||||
|
const isAvailable = await subagentManager.isNameAvailable(
|
||||||
|
state.generatedName,
|
||||||
|
);
|
||||||
|
if (!isAvailable) {
|
||||||
|
const existing = await subagentManager.loadSubagent(
|
||||||
|
state.generatedName,
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
const conflictLevel =
|
||||||
|
existing.level === 'project' ? 'project' : 'user';
|
||||||
|
const targetLevel = state.location;
|
||||||
|
|
||||||
|
if (conflictLevel === targetLevel) {
|
||||||
|
allWarnings.push(
|
||||||
|
`Name "${state.generatedName}" already exists at ${conflictLevel} level - will overwrite existing subagent`,
|
||||||
|
);
|
||||||
|
} else if (targetLevel === 'project') {
|
||||||
|
allWarnings.push(
|
||||||
|
`Name "${state.generatedName}" exists at user level - project level will take precedence`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
allWarnings.push(
|
||||||
|
`Name "${state.generatedName}" exists at project level - existing subagent will take precedence`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently handle errors in warning checks
|
||||||
|
console.warn('Error checking subagent name availability:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check length warnings
|
||||||
|
if (state.generatedDescription.length > 300) {
|
||||||
|
allWarnings.push(
|
||||||
|
`Description is over ${state.generatedDescription.length} characters`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state.generatedSystemPrompt.length > 10000) {
|
||||||
|
allWarnings.push(
|
||||||
|
`System prompt is over ${state.generatedSystemPrompt.length} characters`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setWarnings(allWarnings);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkWarnings();
|
||||||
|
}, [
|
||||||
|
config,
|
||||||
|
state.generatedName,
|
||||||
|
state.generatedDescription,
|
||||||
|
state.generatedSystemPrompt,
|
||||||
|
state.location,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toolsDisplay = Array.isArray(state.selectedTools)
|
||||||
|
? state.selectedTools.join(', ')
|
||||||
|
: '*';
|
||||||
|
|
||||||
|
// Common method to save subagent configuration
|
||||||
|
const saveSubagent = useCallback(async (): Promise<SubagentManager> => {
|
||||||
// Validate configuration before saving
|
// Validate configuration before saving
|
||||||
const configToValidate = {
|
const configToValidate = {
|
||||||
name: state.generatedName,
|
name: state.generatedName,
|
||||||
@@ -53,12 +127,14 @@ export function CreationSummary({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create SubagentManager instance
|
// Create SubagentManager instance
|
||||||
// TODO: Get project root from config or context
|
if (!config) {
|
||||||
const projectRoot = process.cwd();
|
throw new Error('Configuration not available');
|
||||||
|
}
|
||||||
|
const projectRoot = config.getProjectRoot();
|
||||||
const subagentManager = new SubagentManager(projectRoot);
|
const subagentManager = new SubagentManager(projectRoot);
|
||||||
|
|
||||||
// Build subagent configuration
|
// Build subagent configuration
|
||||||
const config: SubagentConfig = {
|
const subagentConfig: SubagentConfig = {
|
||||||
name: state.generatedName,
|
name: state.generatedName,
|
||||||
description: state.generatedDescription,
|
description: state.generatedDescription,
|
||||||
systemPrompt: state.generatedSystemPrompt,
|
systemPrompt: state.generatedSystemPrompt,
|
||||||
@@ -67,38 +143,127 @@ export function CreationSummary({
|
|||||||
tools: Array.isArray(state.selectedTools)
|
tools: Array.isArray(state.selectedTools)
|
||||||
? state.selectedTools
|
? state.selectedTools
|
||||||
: undefined,
|
: undefined,
|
||||||
// TODO: Add modelConfig and runConfig if needed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the subagent
|
// Create the subagent
|
||||||
await subagentManager.createSubagent(config, {
|
await subagentManager.createSubagent(subagentConfig, {
|
||||||
level: state.location,
|
level: state.location,
|
||||||
overwrite: false,
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
setSaveSuccess(true);
|
return subagentManager;
|
||||||
|
}, [state, config]);
|
||||||
|
|
||||||
|
// Common method to show success and auto-close
|
||||||
|
const showSuccessAndClose = useCallback(() => {
|
||||||
|
setSaveSuccess(true);
|
||||||
// Auto-close after successful save
|
// Auto-close after successful save
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onCancel();
|
onCancel();
|
||||||
}, UI.AUTO_CLOSE_DELAY_MS);
|
}, 2000);
|
||||||
|
}, [onCancel]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
setSaveError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveSubagent();
|
||||||
|
showSuccessAndClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveError(
|
setSaveError(
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
}, [state, isSaving, onCancel]);
|
}, [saveSubagent, showSuccessAndClose]);
|
||||||
|
|
||||||
const handleEdit = useCallback(() => {
|
const handleEdit = useCallback(async () => {
|
||||||
// TODO: Implement system editor integration
|
// Clear any previous error messages
|
||||||
setSaveError('Edit functionality not yet implemented');
|
setSaveError(null);
|
||||||
}, []);
|
|
||||||
|
try {
|
||||||
|
// Save the subagent to file first using shared logic
|
||||||
|
const subagentManager = await saveSubagent();
|
||||||
|
|
||||||
|
// Get the file path of the created subagent
|
||||||
|
const subagentFilePath = subagentManager.getSubagentPath(
|
||||||
|
state.generatedName,
|
||||||
|
state.location,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine editor to use
|
||||||
|
const preferredEditor = settings.merged.preferredEditor as
|
||||||
|
| EditorType
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
let editor: string;
|
||||||
|
if (preferredEditor) {
|
||||||
|
editor = preferredEditor;
|
||||||
|
} else {
|
||||||
|
// Platform-specific defaults with UI preference for macOS
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
editor = 'open -t'; // TextEdit in plain text mode
|
||||||
|
break;
|
||||||
|
case 'win32':
|
||||||
|
editor = 'notepad';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
editor = process.env['VISUAL'] || process.env['EDITOR'] || 'vi';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch editor with the actual subagent file
|
||||||
|
const wasRaw = stdin?.isRaw ?? false;
|
||||||
|
try {
|
||||||
|
setRawMode?.(false);
|
||||||
|
|
||||||
|
// Handle different editor command formats
|
||||||
|
let editorCommand: string;
|
||||||
|
let editorArgs: string[];
|
||||||
|
|
||||||
|
if (editor === 'open -t') {
|
||||||
|
// macOS TextEdit in plain text mode
|
||||||
|
editorCommand = 'open';
|
||||||
|
editorArgs = ['-t', subagentFilePath];
|
||||||
|
} else {
|
||||||
|
// Standard editor command
|
||||||
|
editorCommand = editor;
|
||||||
|
editorArgs = [subagentFilePath];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, error } = spawnSync(editorCommand, editorArgs, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
if (typeof status === 'number' && status !== 0) {
|
||||||
|
throw new Error(`Editor exited with status ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success UI and auto-close after successful edit
|
||||||
|
showSuccessAndClose();
|
||||||
|
} finally {
|
||||||
|
if (wasRaw) setRawMode?.(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSaveError(
|
||||||
|
`Failed to save and edit subagent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
saveSubagent,
|
||||||
|
showSuccessAndClose,
|
||||||
|
state.generatedName,
|
||||||
|
state.location,
|
||||||
|
settings.merged.preferredEditor,
|
||||||
|
stdin,
|
||||||
|
setRawMode,
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle keyboard input
|
// Handle keyboard input
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
if (isSaving || saveSuccess) return;
|
if (saveSuccess) return;
|
||||||
|
|
||||||
if (key.return || input === 's') {
|
if (key.return || input === 's') {
|
||||||
handleSave();
|
handleSave();
|
||||||
@@ -115,7 +280,7 @@ export function CreationSummary({
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
<Box>
|
<Box>
|
||||||
<Text bold color="green">
|
<Text bold color={theme.status.success}>
|
||||||
✅ Subagent Created Successfully!
|
✅ Subagent Created Successfully!
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -125,44 +290,13 @@ export function CreationSummary({
|
|||||||
{state.location} level.
|
{state.location} level.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
<Text color="gray">Closing wizard...</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSaving) {
|
|
||||||
return (
|
|
||||||
<Box flexDirection="column" gap={1}>
|
|
||||||
<Box>
|
|
||||||
<Text bold color="cyan">
|
|
||||||
💾 Saving Subagent...
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text>Creating subagent "{state.generatedName}"...</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
{saveError && (
|
<Box flexDirection="column">
|
||||||
<Box flexDirection="column" marginTop={1}>
|
|
||||||
<Text bold color="red">
|
|
||||||
❌ Error saving subagent:
|
|
||||||
</Text>
|
|
||||||
<Text color="red" wrap="wrap">
|
|
||||||
{saveError}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box
|
|
||||||
flexDirection="column"
|
|
||||||
>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text bold>Name: </Text>
|
<Text bold>Name: </Text>
|
||||||
<Text>{state.generatedName}</Text>
|
<Text>{state.generatedName}</Text>
|
||||||
@@ -185,30 +319,49 @@ export function CreationSummary({
|
|||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text bold>Description:</Text>
|
<Text bold>Description:</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box padding={1} paddingBottom={0}>
|
||||||
<Text wrap="wrap">
|
<Text wrap="wrap">
|
||||||
{truncateText(state.generatedDescription, 200)}
|
{truncateText(state.generatedDescription, 250)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text bold>System Prompt:</Text>
|
<Text bold>System Prompt:</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box padding={1} paddingBottom={0}>
|
||||||
<Text wrap="wrap">
|
<Text wrap="wrap">
|
||||||
{truncateText(state.generatedSystemPrompt, 200)}
|
{truncateText(state.generatedSystemPrompt, 250)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1}>
|
{saveError && (
|
||||||
<Text bold>Background Color: </Text>
|
<Box flexDirection="column">
|
||||||
<Text>
|
<Text bold color="red">
|
||||||
{state.backgroundColor === 'auto'
|
❌ Error saving subagent:
|
||||||
? 'Automatic'
|
</Text>
|
||||||
: state.backgroundColor}
|
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||||
|
<Text color="red" wrap="wrap">
|
||||||
|
{saveError}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{warnings.length > 0 && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text bold color={theme.status.warning}>
|
||||||
|
Warnings:
|
||||||
|
</Text>
|
||||||
|
<Box flexDirection="column" padding={1} paddingBottom={0}>
|
||||||
|
{warnings.map((warning, index) => (
|
||||||
|
<Text key={index} color={theme.status.warning} wrap="wrap">
|
||||||
|
• {warning}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,22 @@ export function DescriptionInput({
|
|||||||
const [inputWidth] = useState(80); // Fixed width for now
|
const [inputWidth] = useState(80); // Fixed width for now
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const buffer = useTextBuffer({
|
const handleTextChange = useCallback(
|
||||||
initialText: state.userDescription || '',
|
(text: string) => {
|
||||||
viewport: { height: 10, width: inputWidth },
|
|
||||||
isValidPath: () => false, // For subagent description, we don't need file path validation
|
|
||||||
onChange: (text) => {
|
|
||||||
const sanitized = sanitizeInput(text);
|
const sanitized = sanitizeInput(text);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_USER_DESCRIPTION',
|
type: 'SET_USER_DESCRIPTION',
|
||||||
description: sanitized,
|
description: sanitized,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buffer = useTextBuffer({
|
||||||
|
initialText: state.userDescription || '',
|
||||||
|
viewport: { height: 10, width: inputWidth },
|
||||||
|
isValidPath: () => false, // For subagent description, we don't need file path validation
|
||||||
|
onChange: handleTextChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleGenerate = useCallback(
|
const handleGenerate = useCallback(
|
||||||
|
|||||||
@@ -30,10 +30,3 @@ 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',
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI constants
|
|
||||||
export const UI = {
|
|
||||||
AUTO_CLOSE_DELAY_MS: 2000,
|
|
||||||
PROGRESS_BAR_FILLED: '█',
|
|
||||||
PROGRESS_BAR_EMPTY: '░',
|
|
||||||
} as const;
|
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ export class SubagentManager {
|
|||||||
* @param level - Storage level
|
* @param level - Storage level
|
||||||
* @returns Absolute file path
|
* @returns Absolute file path
|
||||||
*/
|
*/
|
||||||
private getSubagentPath(name: string, level: SubagentLevel): string {
|
getSubagentPath(name: string, level: SubagentLevel): string {
|
||||||
const baseDir =
|
const baseDir =
|
||||||
level === 'project'
|
level === 'project'
|
||||||
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
||||||
|
|||||||
Reference in New Issue
Block a user