From 9fcc7a4cbea39471aee7b19ad01c08b2e4bf4a13 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Thu, 4 Sep 2025 11:07:42 +0800 Subject: [PATCH] feat: subagent creation dialog - continued --- .../components/subagents/CreationSummary.tsx | 367 +++++++++++++----- .../components/subagents/DescriptionInput.tsx | 15 +- .../src/ui/components/subagents/constants.ts | 7 - .../core/src/subagents/subagent-manager.ts | 2 +- packages/core/src/tools/memoryTool.test.ts | 2 +- packages/core/src/tools/todoWrite.test.ts | 2 +- 6 files changed, 273 insertions(+), 122 deletions(-) diff --git a/packages/cli/src/ui/components/subagents/CreationSummary.tsx b/packages/cli/src/ui/components/subagents/CreationSummary.tsx index dd4cf146..5dadb7da 100644 --- a/packages/cli/src/ui/components/subagents/CreationSummary.tsx +++ b/packages/cli/src/ui/components/subagents/CreationSummary.tsx @@ -4,12 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useState } from 'react'; -import { Box, Text, useInput } from 'ink'; +import { useCallback, useState, useEffect } from 'react'; +import { Box, Text, useInput, useStdin } from 'ink'; import { WizardStepProps } from './types.js'; -import { UI } from './constants.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. @@ -18,87 +24,246 @@ export function CreationSummary({ state, onPrevious: _onPrevious, onCancel, + config, }: WizardStepProps) { - const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); + const [warnings, setWarnings] = useState([]); + + const settings = useSettings(); + const { stdin, setRawMode } = useStdin(); const truncateText = (text: string, maxLength: number): string => { if (text.length <= maxLength) return text; return text.substring(0, maxLength - 3) + '...'; }; + // Check for warnings + useEffect(() => { + const checkWarnings = async () => { + if (!config || !state.generatedName) return; + + const allWarnings: string[] = []; + + 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(', ') - : 'All available tools'; + : '*'; + + // Common method to save subagent configuration + const saveSubagent = useCallback(async (): Promise => { + // Validate configuration before saving + const configToValidate = { + name: state.generatedName, + description: state.generatedDescription, + systemPrompt: state.generatedSystemPrompt, + tools: state.selectedTools, + }; + + const validation = validateSubagentConfig(configToValidate); + if (!validation.isValid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + // Create SubagentManager instance + if (!config) { + throw new Error('Configuration not available'); + } + const projectRoot = config.getProjectRoot(); + const subagentManager = new SubagentManager(projectRoot); + + // Build subagent configuration + const subagentConfig: SubagentConfig = { + name: state.generatedName, + description: state.generatedDescription, + systemPrompt: state.generatedSystemPrompt, + level: state.location, + filePath: '', // Will be set by manager + tools: Array.isArray(state.selectedTools) + ? state.selectedTools + : undefined, + }; + + // Create the subagent + await subagentManager.createSubagent(subagentConfig, { + level: state.location, + overwrite: true, + }); + + return subagentManager; + }, [state, config]); + + // Common method to show success and auto-close + const showSuccessAndClose = useCallback(() => { + setSaveSuccess(true); + // Auto-close after successful save + setTimeout(() => { + onCancel(); + }, 2000); + }, [onCancel]); const handleSave = useCallback(async () => { - if (isSaving) return; - - setIsSaving(true); setSaveError(null); try { - // Validate configuration before saving - const configToValidate = { - name: state.generatedName, - description: state.generatedDescription, - systemPrompt: state.generatedSystemPrompt, - tools: state.selectedTools, - }; - - const validation = validateSubagentConfig(configToValidate); - if (!validation.isValid) { - throw new Error(`Validation failed: ${validation.errors.join(', ')}`); - } - - // Create SubagentManager instance - // TODO: Get project root from config or context - const projectRoot = process.cwd(); - const subagentManager = new SubagentManager(projectRoot); - - // Build subagent configuration - const config: SubagentConfig = { - name: state.generatedName, - description: state.generatedDescription, - systemPrompt: state.generatedSystemPrompt, - level: state.location, - filePath: '', // Will be set by manager - tools: Array.isArray(state.selectedTools) - ? state.selectedTools - : undefined, - // TODO: Add modelConfig and runConfig if needed - }; - - // Create the subagent - await subagentManager.createSubagent(config, { - level: state.location, - overwrite: false, - }); - - setSaveSuccess(true); - - // Auto-close after successful save - setTimeout(() => { - onCancel(); - }, UI.AUTO_CLOSE_DELAY_MS); + await saveSubagent(); + showSuccessAndClose(); } catch (error) { setSaveError( error instanceof Error ? error.message : 'Unknown error occurred', ); - } finally { - setIsSaving(false); } - }, [state, isSaving, onCancel]); + }, [saveSubagent, showSuccessAndClose]); - const handleEdit = useCallback(() => { - // TODO: Implement system editor integration - setSaveError('Edit functionality not yet implemented'); - }, []); + const handleEdit = useCallback(async () => { + // Clear any previous error messages + 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 useInput((input, key) => { - if (isSaving || saveSuccess) return; + if (saveSuccess) return; if (key.return || input === 's') { handleSave(); @@ -115,7 +280,7 @@ export function CreationSummary({ return ( - + ✅ Subagent Created Successfully! @@ -125,45 +290,14 @@ export function CreationSummary({ {state.location} level. - - Closing wizard... - - - ); - } - - if (isSaving) { - return ( - - - - 💾 Saving Subagent... - - - - Creating subagent "{state.generatedName}"... - ); } return ( - {saveError && ( - - - ❌ Error saving subagent: - - - {saveError} - - - )} - - - + + Name: {state.generatedName} @@ -185,30 +319,49 @@ export function CreationSummary({ Description: - + - {truncateText(state.generatedDescription, 200)} + {truncateText(state.generatedDescription, 250)} System Prompt: - + - {truncateText(state.generatedSystemPrompt, 200)} - - - - - Background Color: - - {state.backgroundColor === 'auto' - ? 'Automatic' - : state.backgroundColor} + {truncateText(state.generatedSystemPrompt, 250)} + + {saveError && ( + + + ❌ Error saving subagent: + + + + {saveError} + + + + )} + + {warnings.length > 0 && ( + + + Warnings: + + + {warnings.map((warning, index) => ( + + • {warning} + + ))} + + + )} ); } diff --git a/packages/cli/src/ui/components/subagents/DescriptionInput.tsx b/packages/cli/src/ui/components/subagents/DescriptionInput.tsx index c6cbd561..8c1c7b8e 100644 --- a/packages/cli/src/ui/components/subagents/DescriptionInput.tsx +++ b/packages/cli/src/ui/components/subagents/DescriptionInput.tsx @@ -30,17 +30,22 @@ export function DescriptionInput({ const [inputWidth] = useState(80); // Fixed width for now const abortControllerRef = useRef(null); - const buffer = useTextBuffer({ - initialText: state.userDescription || '', - viewport: { height: 10, width: inputWidth }, - isValidPath: () => false, // For subagent description, we don't need file path validation - onChange: (text) => { + const handleTextChange = useCallback( + (text: string) => { const sanitized = sanitizeInput(text); dispatch({ type: 'SET_USER_DESCRIPTION', 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( diff --git a/packages/cli/src/ui/components/subagents/constants.ts b/packages/cli/src/ui/components/subagents/constants.ts index 75bfbe2b..59c44243 100644 --- a/packages/cli/src/ui/components/subagents/constants.ts +++ b/packages/cli/src/ui/components/subagents/constants.ts @@ -30,10 +30,3 @@ export const STEP_NAMES: Record = { [WIZARD_STEPS.COLOR_SELECTION]: 'Color Selection', [WIZARD_STEPS.FINAL_CONFIRMATION]: 'Final Confirmation', }; - -// UI constants -export const UI = { - AUTO_CLOSE_DELAY_MS: 2000, - PROGRESS_BAR_FILLED: '█', - PROGRESS_BAR_EMPTY: '░', -} as const; diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index 75470939..e07995bf 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -539,7 +539,7 @@ export class SubagentManager { * @param level - Storage level * @returns Absolute file path */ - private getSubagentPath(name: string, level: SubagentLevel): string { + getSubagentPath(name: string, level: SubagentLevel): string { const baseDir = level === 'project' ? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR) diff --git a/packages/core/src/tools/memoryTool.test.ts b/packages/core/src/tools/memoryTool.test.ts index b66d60bf..2a8596eb 100644 --- a/packages/core/src/tools/memoryTool.test.ts +++ b/packages/core/src/tools/memoryTool.test.ts @@ -193,7 +193,7 @@ describe('MemoryTool', () => { it('should have correct name, displayName, description, and schema', () => { expect(memoryTool.name).toBe('save_memory'); - expect(memoryTool.displayName).toBe('Save Memory'); + expect(memoryTool.displayName).toBe('SaveMemory'); expect(memoryTool.description).toContain( 'Saves a specific piece of information', ); diff --git a/packages/core/src/tools/todoWrite.test.ts b/packages/core/src/tools/todoWrite.test.ts index 1bfc1516..7ff35f5f 100644 --- a/packages/core/src/tools/todoWrite.test.ts +++ b/packages/core/src/tools/todoWrite.test.ts @@ -242,7 +242,7 @@ describe('TodoWriteTool', () => { }); it('should have correct display name', () => { - expect(tool.displayName).toBe('Todo Write'); + expect(tool.displayName).toBe('TodoWrite'); }); it('should have correct kind', () => {