From 8e2fc76c15b290aa69120025641aababd8b4d26d Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 16 Sep 2025 15:24:58 +0800 Subject: [PATCH 1/6] fix: Esc unable to cancel subagent dialog --- .../subagents/create/AgentCreationWizard.tsx | 16 +++++++++++----- .../subagents/manage/AgentsManagerDialog.tsx | 16 +++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx b/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx index 6756856b..11c47a45 100644 --- a/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx +++ b/packages/cli/src/ui/components/subagents/create/AgentCreationWizard.tsx @@ -5,7 +5,7 @@ */ import { useReducer, useCallback, useMemo } from 'react'; -import { Box, Text, useInput } from 'ink'; +import { Box, Text } from 'ink'; import { wizardReducer, initialWizardState } from '../reducers.js'; import { LocationSelector } from './LocationSelector.js'; import { GenerationMethodSelector } from './GenerationMethodSelector.js'; @@ -20,6 +20,7 @@ import { Config } from '@qwen-code/qwen-code-core'; import { Colors } from '../../../colors.js'; import { theme } from '../../../semantic-colors.js'; import { TextEntryStep } from './TextEntryStep.js'; +import { useKeypress } from '../../../hooks/useKeypress.js'; interface AgentCreationWizardProps { onClose: () => void; @@ -49,8 +50,12 @@ export function AgentCreationWizard({ }, [onClose]); // Centralized ESC key handling for the entire wizard - useInput((input, key) => { - if (key.escape) { + useKeypress( + (key) => { + if (key.name !== 'escape') { + return; + } + // LLM DescriptionInput handles its own ESC logic when generating const kind = getStepKind(state.generationMethod, state.currentStep); if (kind === 'LLM_DESC' && state.isGenerating) { @@ -64,8 +69,9 @@ export function AgentCreationWizard({ // On other steps, ESC goes back to previous step handlePrevious(); } - } - }); + }, + { isActive: true }, + ); const stepProps: WizardStepProps = useMemo( () => ({ diff --git a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx index e68167d4..d3be1a4b 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx @@ -5,7 +5,7 @@ */ import { useState, useCallback, useMemo, useEffect } from 'react'; -import { Box, Text, useInput } from 'ink'; +import { Box, Text } from 'ink'; import { AgentSelectionStep } from './AgentSelectionStep.js'; import { ActionSelectionStep } from './ActionSelectionStep.js'; import { AgentViewerStep } from './AgentViewerStep.js'; @@ -18,6 +18,7 @@ import { Colors } from '../../../colors.js'; import { theme } from '../../../semantic-colors.js'; import { getColorForDisplay, shouldShowColor } from '../utils.js'; import { Config, SubagentConfig } from '@qwen-code/qwen-code-core'; +import { useKeypress } from '../../../hooks/useKeypress.js'; interface AgentsManagerDialogProps { onClose: () => void; @@ -122,8 +123,12 @@ export function AgentsManagerDialog({ ); // Centralized ESC key handling for the entire dialog - useInput((input, key) => { - if (key.escape) { + useKeypress( + (key) => { + if (key.name !== 'escape') { + return; + } + const currentStep = getCurrentStep(); if (currentStep === MANAGEMENT_STEPS.AGENT_SELECTION) { // On first step, ESC cancels the entire dialog @@ -132,8 +137,9 @@ export function AgentsManagerDialog({ // On other steps, ESC goes back to previous step in navigation stack handleNavigateBack(); } - } - }); + }, + { isActive: true }, + ); // Props for child components - now using direct state and callbacks const commonProps = useMemo( From 19950e5b7c2986d4dbb3a1f04d9c032b029198d6 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 16 Sep 2025 16:03:35 +0800 Subject: [PATCH 2/6] chore: update subagent docs --- docs/subagents.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/subagents.md b/docs/subagents.md index 415df7ce..15b5e273 100644 --- a/docs/subagents.md +++ b/docs/subagents.md @@ -133,6 +133,28 @@ Focus on creating clear, comprehensive documentation that helps both new contributors and end users understand the project. ``` +## Using Subagents Effectively + +### Automatic Delegation + +Qwen Code proactively delegates tasks based on: + +- The task description in your request +- The description field in subagent configurations +- Current context and available tools + +To encourage more proactive subagent use, include phrases like "use PROACTIVELY" or "MUST BE USED" in your description field. + +### Explicit Invocation + +Request a specific subagent by mentioning it in your command: + +``` +> Let the testing-expert subagent create unit tests for the payment module +> Have the documentation-writer subagent update the API reference +> Get the react-specialist subagent to optimize this component's performance +``` + ## Examples ### Development Workflow Agents From 5f90472a7da558e6197be5a796e044aa6e2e5399 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 17 Sep 2025 11:32:52 +0800 Subject: [PATCH 3/6] fix: duplicate subagents config if qwen-code runs in home dir --- .../subagents/manage/AgentSelectionStep.tsx | 2 +- .../src/subagents/subagent-manager.test.ts | 56 ++++++++--------- .../core/src/subagents/subagent-manager.ts | 62 +++++++------------ 3 files changed, 53 insertions(+), 67 deletions(-) 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 From e552bc960929f5d717df24d76f82f0357dea8f1b Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 17 Sep 2025 17:01:06 +0800 Subject: [PATCH 4/6] fix: terminal flicker when subagent is executing --- packages/cli/src/ui/App.tsx | 15 ++-- .../src/ui/components/HistoryItemDisplay.tsx | 6 +- .../messages/ToolConfirmationMessage.tsx | 36 +++++++++ .../runtime/AgentExecutionDisplay.tsx | 79 +++++++++++++++---- packages/cli/src/ui/hooks/useGeminiStream.ts | 11 ++- 5 files changed, 120 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 5039a170..3494f3ce 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -723,8 +723,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { ); const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); - const pendingHistoryItems = [...pendingSlashCommandHistoryItems]; - pendingHistoryItems.push(...pendingGeminiHistoryItems); + const pendingHistoryItems = useMemo(() => { + const items = [...pendingSlashCommandHistoryItems]; + items.push(...pendingGeminiHistoryItems); + return items.map((item, i) => ({ ...item, id: i })); + }, [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems]); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); @@ -1070,16 +1073,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { - {pendingHistoryItems.map((item, i) => ( + {pendingHistoryItems.map((item) => ( = ({ +const HistoryItemDisplayComponent: React.FC = ({ item, availableTerminalHeight, terminalWidth, @@ -101,3 +101,7 @@ export const HistoryItemDisplay: React.FC = ({ {item.type === 'summary' && } ); + +HistoryItemDisplayComponent.displayName = 'HistoryItemDisplay'; + +export const HistoryItemDisplay = React.memo(HistoryItemDisplayComponent); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index f1113b62..434617bc 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -29,6 +29,7 @@ export interface ToolConfirmationMessageProps { isFocused?: boolean; availableTerminalHeight?: number; terminalWidth: number; + compactMode?: boolean; } export const ToolConfirmationMessage: React.FC< @@ -39,6 +40,7 @@ export const ToolConfirmationMessage: React.FC< isFocused = true, availableTerminalHeight, terminalWidth, + compactMode = false, }) => { const { onConfirm } = confirmationDetails; const childWidth = terminalWidth - 2; // 2 for padding @@ -70,6 +72,40 @@ export const ToolConfirmationMessage: React.FC< const handleSelect = (item: ToolConfirmationOutcome) => handleConfirm(item); + // Compact mode: return simple 3-option display + if (compactMode) { + const compactOptions: Array> = [ + { + label: 'Yes, allow once', + value: ToolConfirmationOutcome.ProceedOnce, + }, + { + label: 'Allow always', + value: ToolConfirmationOutcome.ProceedAlways, + }, + { + label: 'No', + value: ToolConfirmationOutcome.Cancel, + }, + ]; + + return ( + + + Do you want to proceed? + + + + + + ); + } + + // Original logic continues unchanged below let bodyContent: React.ReactNode | null = null; // Removed contextDisplay here let question: string; diff --git a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx index 355e7eae..a1beb922 100644 --- a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx +++ b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx @@ -17,12 +17,12 @@ import { COLOR_OPTIONS } from '../constants.js'; import { fmtDuration } from '../utils.js'; import { ToolConfirmationMessage } from '../../messages/ToolConfirmationMessage.js'; -export type DisplayMode = 'default' | 'verbose'; +export type DisplayMode = 'compact' | 'default' | 'verbose'; export interface AgentExecutionDisplayProps { data: TaskResultDisplay; availableHeight?: number; - childWidth?: number; + childWidth: number; } const getStatusColor = ( @@ -76,8 +76,8 @@ export const AgentExecutionDisplay: React.FC = ({ data, availableHeight, childWidth, -}) => { - const [displayMode, setDisplayMode] = React.useState('default'); +}: AgentExecutionDisplayProps) => { + const [displayMode, setDisplayMode] = React.useState('compact'); const agentColor = useMemo(() => { const colorOption = COLOR_OPTIONS.find( @@ -90,8 +90,6 @@ export const AgentExecutionDisplay: React.FC = ({ // This component only listens to keyboard shortcut events when the subagent is running if (data.status !== 'running') return ''; - if (displayMode === 'verbose') return 'Press ctrl+r to show less.'; - if (displayMode === 'default') { const hasMoreLines = data.taskPrompt.split('\n').length > MAX_TASK_PROMPT_LINES; @@ -99,17 +97,28 @@ export const AgentExecutionDisplay: React.FC = ({ data.toolCalls && data.toolCalls.length > MAX_TOOL_CALLS; if (hasMoreToolCalls || hasMoreLines) { - return 'Press ctrl+r to show more.'; + return 'Press ctrl+r to show less, ctrl+e to show more.'; } - return ''; + return 'Press ctrl+r to show less.'; } - return ''; - }, [displayMode, data.toolCalls, data.taskPrompt, data.status]); - // Handle ctrl+r keypresses to control display mode + if (displayMode === 'verbose') { + return 'Press ctrl+e to show less.'; + } + + return ''; + }, [displayMode, data]); + + // Handle keyboard shortcuts to control display mode useKeypress( (key) => { if (key.ctrl && key.name === 'r') { + // ctrl+r toggles between compact and default + setDisplayMode((current) => + current === 'compact' ? 'default' : 'compact', + ); + } else if (key.ctrl && key.name === 'e') { + // ctrl+e toggles between default and verbose setDisplayMode((current) => current === 'default' ? 'verbose' : 'default', ); @@ -118,6 +127,44 @@ export const AgentExecutionDisplay: React.FC = ({ { isActive: true }, ); + if (displayMode === 'compact') { + return ( + + {data.toolCalls && data.toolCalls.length > 0 && ( + + + {/* Show count of additional tool calls if there are more than 1 */} + {data.toolCalls.length > 1 && !data.pendingConfirmation && ( + + + +{data.toolCalls.length - 1} more tool calls{' '} + {data.status === 'running' ? '(ctrl+r to expand)' : ''} + + + )} + + )} + + {/* Inline approval prompt when awaiting confirmation */} + {data.pendingConfirmation && ( + + + + )} + + ); + } + + // Default and verbose modes use normal layout return ( {/* Header with subagent name and status */} @@ -154,7 +201,8 @@ export const AgentExecutionDisplay: React.FC = ({ confirmationDetails={data.pendingConfirmation} isFocused={true} availableTerminalHeight={availableHeight} - terminalWidth={childWidth ?? 80} + terminalWidth={childWidth} + compactMode={true} /> )} @@ -276,7 +324,8 @@ const ToolCallItem: React.FC<{ resultDisplay?: string; description?: string; }; -}> = ({ toolCall }) => { + compact?: boolean; +}> = ({ toolCall, compact = false }) => { const STATUS_INDICATOR_WIDTH = 3; // Map subagent status to ToolCallStatus-like display @@ -331,8 +380,8 @@ const ToolCallItem: React.FC<{ - {/* Second line: truncated returnDisplay output */} - {truncatedOutput && ( + {/* Second line: truncated returnDisplay output - hidden in compact mode */} + {!compact && truncatedOutput && ( {truncatedOutput} diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 282e8a1c..fd633ac0 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -901,10 +901,13 @@ export const useGeminiStream = ( ], ); - const pendingHistoryItems = [ - pendingHistoryItemRef.current, - pendingToolCallGroupDisplay, - ].filter((i) => i !== undefined && i !== null); + const pendingHistoryItems = useMemo( + () => + [pendingHistoryItemRef.current, pendingToolCallGroupDisplay].filter( + (i) => i !== undefined && i !== null, + ), + [pendingHistoryItemRef, pendingToolCallGroupDisplay], + ); useEffect(() => { const saveRestorableToolCalls = async () => { From 8803b2eb760a5ae25f74b22678cae29927c2aa68 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 17 Sep 2025 18:56:30 +0800 Subject: [PATCH 5/6] feat: add system-reminder to help model use subagent --- .../subagents/manage/AgentsManagerDialog.tsx | 13 +----- packages/core/src/core/client.test.ts | 4 ++ packages/core/src/core/client.ts | 22 +++++++++- .../core/src/subagents/subagent-manager.ts | 42 ++++++++++++++++++- packages/core/src/subagents/types.ts | 3 ++ 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx index d3be1a4b..3f45f0fc 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx @@ -53,18 +53,7 @@ export function AgentsManagerDialog({ const manager = config.getSubagentManager(); // Load agents from all levels separately to show all agents including conflicts - const [projectAgents, userAgents, builtinAgents] = await Promise.all([ - manager.listSubagents({ level: 'project' }), - manager.listSubagents({ level: 'user' }), - manager.listSubagents({ level: 'builtin' }), - ]); - - // Combine all agents (project, user, and builtin level) - const allAgents = [ - ...(projectAgents || []), - ...(userAgents || []), - ...(builtinAgents || []), - ]; + const allAgents = await manager.listSubagents(); setAvailableAgents(allAgents); }, [config]); diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 6999e441..2c1186bf 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -199,6 +199,9 @@ describe('Gemini Client (client.ts)', () => { vertexai: false, authType: AuthType.USE_GEMINI, }; + const mockSubagentManager = { + listSubagents: vi.fn().mockResolvedValue([]), + }; const mockConfigObject = { getContentGeneratorConfig: vi .fn() @@ -233,6 +236,7 @@ describe('Gemini Client (client.ts)', () => { getCliVersion: vi.fn().mockReturnValue('1.0.0'), getChatCompression: vi.fn().mockReturnValue(undefined), getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false), + getSubagentManager: vi.fn().mockReturnValue(mockSubagentManager), }; const MockedConfig = vi.mocked(Config, true); MockedConfig.mockImplementation( diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 359c7d60..576520e1 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -58,6 +58,7 @@ import { NextSpeakerCheckEvent, } from '../telemetry/types.js'; import { IdeContext, File } from '../ide/ideContext.js'; +import { TaskTool } from '../tools/task.js'; function isThinkingSupported(model: string) { if (model.startsWith('gemini-2.5')) return true; @@ -452,7 +453,8 @@ export class GeminiClient { turns: number = MAX_TURNS, originalModel?: string, ): AsyncGenerator { - if (this.lastPromptId !== prompt_id) { + const isNewPrompt = this.lastPromptId !== prompt_id; + if (isNewPrompt) { this.loopDetector.reset(prompt_id); this.lastPromptId = prompt_id; } @@ -549,6 +551,24 @@ export class GeminiClient { this.forceFullIdeContext = false; } + if (isNewPrompt) { + const taskTool = this.config.getToolRegistry().getTool(TaskTool.Name); + const subagents = ( + await this.config.getSubagentManager().listSubagents() + ).filter((subagent) => subagent.level !== 'builtin'); + + if (taskTool && subagents.length > 0) { + this.getChat().addHistory({ + role: 'user', + parts: [ + { + text: `You have powerful specialized agents at your disposal, available agent types are: ${subagents.map((subagent) => subagent.name).join(', ')}. PROACTIVELY use the ${TaskTool.Name} tool to delegate user's task to appropriate agent when user's task matches agent capabilities. Ignore this message if user's task is not relevant to any agent. This message is for internal use only. Do not mention this to user in your response.`, + }, + ], + }); + } + } + const turn = new Turn(this.getChat(), prompt_id); const loopDetected = await this.loopDetector.turnStarted(signal); diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index c7b7740c..c678c27f 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -40,6 +40,7 @@ const AGENT_CONFIG_DIR = 'agents'; */ export class SubagentManager { private readonly validator: SubagentValidator; + private subagentsCache: Map | null = null; constructor(private readonly config: Config) { this.validator = new SubagentValidator(); @@ -93,6 +94,8 @@ export class SubagentManager { try { await fs.writeFile(filePath, content, 'utf8'); + // Clear cache after successful creation + this.clearCache(); } catch (error) { throw new SubagentError( `Failed to write subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`, @@ -181,6 +184,8 @@ export class SubagentManager { try { await fs.writeFile(existing.filePath, content, 'utf8'); + // Clear cache after successful update + this.clearCache(); } catch (error) { throw new SubagentError( `Failed to update subagent file: ${error instanceof Error ? error.message : 'Unknown error'}`, @@ -237,6 +242,9 @@ export class SubagentManager { name, ); } + + // Clear cache after successful deletion + this.clearCache(); } /** @@ -255,9 +263,17 @@ export class SubagentManager { ? [options.level] : ['project', 'user', 'builtin']; + // Check if we should use cache or force refresh + const shouldUseCache = !options.force && this.subagentsCache !== null; + + // Initialize cache if it doesn't exist or we're forcing a refresh + if (!shouldUseCache) { + await this.refreshCache(); + } + // Collect subagents from each level (project takes precedence over user, user takes precedence over builtin) for (const level of levelsToCheck) { - const levelSubagents = await this.listSubagentsAtLevel(level); + const levelSubagents = this.subagentsCache?.get(level) || []; for (const subagent of levelSubagents) { // Skip if we've already seen this name (precedence: project > user > builtin) @@ -305,6 +321,30 @@ export class SubagentManager { return subagents; } + /** + * Refreshes the subagents cache by loading all subagents from disk. + * This method is called automatically when cache is null or when force=true. + * + * @private + */ + private async refreshCache(): Promise { + this.subagentsCache = new Map(); + + const levels: SubagentLevel[] = ['project', 'user', 'builtin']; + + for (const level of levels) { + const levelSubagents = await this.listSubagentsAtLevel(level); + this.subagentsCache.set(level, levelSubagents); + } + } + + /** + * Clears the subagents cache, forcing the next listSubagents call to reload from disk. + */ + clearCache(): void { + this.subagentsCache = null; + } + /** * Finds a subagent by name and returns its metadata. * diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts index b8146125..be7fc986 100644 --- a/packages/core/src/subagents/types.ts +++ b/packages/core/src/subagents/types.ts @@ -116,6 +116,9 @@ export interface ListSubagentsOptions { /** Sort direction */ sortOrder?: 'asc' | 'desc'; + + /** Force refresh from disk, bypassing cache. Defaults to false. */ + force?: boolean; } /** From de468f0525dd97bc808f3d4a1353f7d3fa3e03fa Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 17 Sep 2025 19:52:12 +0800 Subject: [PATCH 6/6] fix: merge issue --- .../runtime/AgentExecutionDisplay.tsx | 91 +++++++++++++------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx index b94a1c54..7d63320a 100644 --- a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx +++ b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx @@ -80,7 +80,7 @@ export const AgentExecutionDisplay: React.FC = ({ childWidth, config, }) => { - const [displayMode, setDisplayMode] = React.useState('default'); + const [displayMode, setDisplayMode] = React.useState('compact'); const agentColor = useMemo(() => { const colorOption = COLOR_OPTIONS.find( @@ -133,35 +133,72 @@ export const AgentExecutionDisplay: React.FC = ({ if (displayMode === 'compact') { return ( - {data.toolCalls && data.toolCalls.length > 0 && ( - - - {/* Show count of additional tool calls if there are more than 1 */} - {data.toolCalls.length > 1 && !data.pendingConfirmation && ( - - - +{data.toolCalls.length - 1} more tool calls{' '} - {data.status === 'running' ? '(ctrl+r to expand)' : ''} - - - )} + {/* Header: Agent name and status */} + {!data.pendingConfirmation && ( + + + {data.subagentName} + + + )} - {/* Inline approval prompt when awaiting confirmation */} - {data.pendingConfirmation && ( - - + {/* Running state: Show current tool call and progress */} + {data.status === 'running' && ( + <> + {/* Current tool call */} + {data.toolCalls && data.toolCalls.length > 0 && ( + + + {/* Show count of additional tool calls if there are more than 1 */} + {data.toolCalls.length > 1 && !data.pendingConfirmation && ( + + + +{data.toolCalls.length - 1} more tool calls (ctrl+r to + expand) + + + )} + + )} + + {/* Inline approval prompt when awaiting confirmation */} + {data.pendingConfirmation && ( + + + + )} + + )} + + {/* Completed state: Show summary line */} + {data.status === 'completed' && data.executionSummary && ( + + + Execution Summary: {data.executionSummary.totalToolCalls} tool + uses · {data.executionSummary.totalTokens.toLocaleString()} tokens + · {fmtDuration(data.executionSummary.totalDurationMs)} + + + )} + + {/* Failed/Cancelled state: Show error reason */} + {data.status === 'failed' && ( + + + Failed: {data.terminateReason} + )}