diff --git a/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx index d1161e10..1c23eaaf 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentSelectionStep.tsx @@ -227,7 +227,7 @@ export const AgentSelectionStep = ({ const textColor = isSelected ? theme.text.accent : theme.text.primary; return ( - + {isSelected ? '●' : ' '} diff --git a/packages/core/src/subagents/subagent-manager.test.ts b/packages/core/src/subagents/subagent-manager.test.ts index 2b199907..f997295b 100644 --- a/packages/core/src/subagents/subagent-manager.test.ts +++ b/packages/core/src/subagents/subagent-manager.test.ts @@ -185,6 +185,7 @@ You are a helpful assistant. const config = manager.parseSubagentContent( validMarkdown, validConfig.filePath, + 'project', ); expect(config.name).toBe('test-agent'); @@ -209,6 +210,7 @@ You are a helpful assistant. const config = manager.parseSubagentContent( markdownWithTools, validConfig.filePath, + 'project', ); expect(config.tools).toEqual(['read_file', 'write_file']); @@ -229,6 +231,7 @@ You are a helpful assistant. const config = manager.parseSubagentContent( markdownWithModel, validConfig.filePath, + 'project', ); expect(config.modelConfig).toEqual({ model: 'custom-model', temp: 0.5 }); @@ -249,6 +252,7 @@ You are a helpful assistant. const config = manager.parseSubagentContent( markdownWithRun, validConfig.filePath, + 'project', ); expect(config.runConfig).toEqual({ max_time_minutes: 5, max_turns: 10 }); @@ -266,6 +270,7 @@ You are a helpful assistant. const config = manager.parseSubagentContent( markdownWithNumeric, validConfig.filePath, + 'project', ); expect(config.name).toBe('11'); @@ -286,6 +291,7 @@ You are a helpful assistant. const config = manager.parseSubagentContent( markdownWithBoolean, validConfig.filePath, + 'project', ); expect(config.name).toBe('true'); @@ -301,8 +307,13 @@ You are a helpful assistant. const projectConfig = manager.parseSubagentContent( validMarkdown, projectPath, + 'project', + ); + const userConfig = manager.parseSubagentContent( + validMarkdown, + userPath, + 'user', ); - const userConfig = manager.parseSubagentContent(validMarkdown, userPath); expect(projectConfig.level).toBe('project'); expect(userConfig.level).toBe('user'); @@ -313,7 +324,11 @@ You are a helpful assistant. Just content`; expect(() => - manager.parseSubagentContent(invalidMarkdown, validConfig.filePath), + manager.parseSubagentContent( + invalidMarkdown, + validConfig.filePath, + 'project', + ), ).toThrow(SubagentError); }); @@ -326,7 +341,11 @@ You are a helpful assistant. `; expect(() => - manager.parseSubagentContent(markdownWithoutName, validConfig.filePath), + manager.parseSubagentContent( + markdownWithoutName, + validConfig.filePath, + 'project', + ), ).toThrow(SubagentError); }); @@ -342,39 +361,20 @@ You are a helpful assistant. manager.parseSubagentContent( markdownWithoutDescription, validConfig.filePath, + 'project', ), ).toThrow(SubagentError); }); - it('should warn when filename does not match subagent name', () => { - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const mismatchedPath = '/test/project/.qwen/agents/wrong-filename.md'; - - const config = manager.parseSubagentContent( - validMarkdown, - mismatchedPath, - ); - - expect(config.name).toBe('test-agent'); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining( - 'Warning: Subagent file "wrong-filename.md" contains name "test-agent"', - ), - ); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining( - 'Consider renaming the file to "test-agent.md"', - ), - ); - - consoleSpy.mockRestore(); - }); - it('should not warn when filename matches subagent name', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const matchingPath = '/test/project/.qwen/agents/test-agent.md'; - const config = manager.parseSubagentContent(validMarkdown, matchingPath); + const config = manager.parseSubagentContent( + validMarkdown, + matchingPath, + 'project', + ); expect(config.name).toBe('test-agent'); expect(consoleSpy).not.toHaveBeenCalled(); diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index c06081ae..c7b7740c 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -330,7 +330,10 @@ export class SubagentManager { * @returns SubagentConfig * @throws SubagentError if parsing fails */ - async parseSubagentFile(filePath: string): Promise { + async parseSubagentFile( + filePath: string, + level: SubagentLevel, + ): Promise { let content: string; try { @@ -342,7 +345,7 @@ export class SubagentManager { ); } - return this.parseSubagentContent(content, filePath); + return this.parseSubagentContent(content, filePath, level); } /** @@ -353,7 +356,11 @@ export class SubagentManager { * @returns SubagentConfig * @throws SubagentError if parsing fails */ - parseSubagentContent(content: string, filePath: string): SubagentConfig { + parseSubagentContent( + content: string, + filePath: string, + level: SubagentLevel, + ): SubagentConfig { try { // Split frontmatter and content const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; @@ -394,31 +401,16 @@ export class SubagentManager { | undefined; const color = frontmatter['color'] as string | undefined; - // Determine level from file path using robust, cross-platform check - // A project-level agent lives under /.qwen/agents - const projectAgentsDir = path.join( - this.config.getProjectRoot(), - QWEN_CONFIG_DIR, - AGENT_CONFIG_DIR, - ); - const rel = path.relative( - path.normalize(projectAgentsDir), - path.normalize(filePath), - ); - const isProjectLevel = - rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel); - const level: SubagentLevel = isProjectLevel ? 'project' : 'user'; - const config: SubagentConfig = { name, description, tools, systemPrompt: systemPrompt.trim(), - level, filePath, modelConfig: modelConfig as Partial, runConfig: runConfig as Partial, color, + level, }; // Validate the parsed configuration @@ -427,16 +419,6 @@ export class SubagentManager { throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } - // Warn if filename doesn't match subagent name (potential issue) - const expectedFilename = `${config.name}.md`; - const actualFilename = path.basename(filePath); - if (actualFilename !== expectedFilename) { - console.warn( - `Warning: Subagent file "${actualFilename}" contains name "${config.name}" but filename suggests "${path.basename(actualFilename, '.md')}". ` + - `Consider renaming the file to "${expectedFilename}" for consistency.`, - ); - } - return config; } catch (error) { throw new SubagentError( @@ -679,14 +661,18 @@ export class SubagentManager { return BuiltinAgentRegistry.getBuiltinAgents(); } - const baseDir = - level === 'project' - ? path.join( - this.config.getProjectRoot(), - QWEN_CONFIG_DIR, - AGENT_CONFIG_DIR, - ) - : path.join(os.homedir(), QWEN_CONFIG_DIR, AGENT_CONFIG_DIR); + const projectRoot = this.config.getProjectRoot(); + const homeDir = os.homedir(); + const isHomeDirectory = path.resolve(projectRoot) === path.resolve(homeDir); + + // If project level is requested but project root is same as home directory, + // return empty array to avoid conflicts between project and global agents + if (level === 'project' && isHomeDirectory) { + return []; + } + + let baseDir = level === 'project' ? projectRoot : homeDir; + baseDir = path.join(baseDir, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR); try { const files = await fs.readdir(baseDir); @@ -698,7 +684,7 @@ export class SubagentManager { const filePath = path.join(baseDir, file); try { - const config = await this.parseSubagentFile(filePath); + const config = await this.parseSubagentFile(filePath, level); subagents.push(config); } catch (_error) { // Ignore invalid files