From 35feea8868d5bb4f082d32cbb0662a4251abee28 Mon Sep 17 00:00:00 2001 From: Sandy Tao Date: Thu, 22 Jan 2026 15:22:56 -0800 Subject: [PATCH] feat(cli): add /agents config command and improve agent discovery (#17342) --- .../cli/src/test-utils/mockCommandContext.ts | 2 + .../cli/src/ui/commands/agentsCommand.test.ts | 141 ++++++++++++++++++ packages/cli/src/ui/commands/agentsCommand.ts | 82 +++++++++- 3 files changed, 223 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 63328b2a21..928d04c7a1 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -61,6 +61,8 @@ export const createMockCommandContext = ( loadHistory: vi.fn(), toggleCorgiMode: vi.fn(), toggleVimEnabled: vi.fn(), + openAgentConfigDialog: vi.fn(), + closeAgentConfigDialog: vi.fn(), extensionsUpdateState: new Map(), setExtensionsUpdateState: vi.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/cli/src/ui/commands/agentsCommand.test.ts b/packages/cli/src/ui/commands/agentsCommand.test.ts index 3070e4d779..a750888fb2 100644 --- a/packages/cli/src/ui/commands/agentsCommand.test.ts +++ b/packages/cli/src/ui/commands/agentsCommand.test.ts @@ -218,6 +218,21 @@ describe('agentsCommand', () => { }); }); + it('should show an error if config is not available for enable', async () => { + const contextWithoutConfig = createMockCommandContext({ + services: { config: null }, + }); + const enableCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'enable', + ); + const result = await enableCommand!.action!(contextWithoutConfig, 'test'); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }); + }); + it('should disable an agent successfully', async () => { const reloadSpy = vi.fn().mockResolvedValue(undefined); mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ @@ -308,4 +323,130 @@ describe('agentsCommand', () => { content: 'Usage: /agents disable ', }); }); + + it('should show an error if config is not available for disable', async () => { + const contextWithoutConfig = createMockCommandContext({ + services: { config: null }, + }); + const disableCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'disable', + ); + const result = await disableCommand!.action!(contextWithoutConfig, 'test'); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }); + }); + + describe('config sub-command', () => { + it('should open agent config dialog for a valid agent', async () => { + const mockDefinition = { + name: 'test-agent', + displayName: 'Test Agent', + description: 'test desc', + }; + mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ + getDiscoveredDefinition: vi.fn().mockReturnValue(mockDefinition), + }); + + const configCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'config', + ); + expect(configCommand).toBeDefined(); + + const result = await configCommand!.action!(mockContext, 'test-agent'); + + expect(mockContext.ui.openAgentConfigDialog).not.toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: + "Configuration for 'test-agent' will be available in the next update.", + }); + }); + + it('should use name if displayName is missing', async () => { + const mockDefinition = { + name: 'test-agent', + description: 'test desc', + }; + mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ + getDiscoveredDefinition: vi.fn().mockReturnValue(mockDefinition), + }); + + const configCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'config', + ); + const result = await configCommand!.action!(mockContext, 'test-agent'); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: + "Configuration for 'test-agent' will be available in the next update.", + }); + }); + + it('should show error if agent is not found', async () => { + mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ + getDiscoveredDefinition: vi.fn().mockReturnValue(undefined), + }); + + const configCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'config', + ); + const result = await configCommand!.action!(mockContext, 'non-existent'); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: "Agent 'non-existent' not found.", + }); + }); + + it('should show usage error if no agent name provided', async () => { + const configCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'config', + ); + const result = await configCommand!.action!(mockContext, ' '); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Usage: /agents config ', + }); + }); + + it('should show an error if config is not available', async () => { + const contextWithoutConfig = createMockCommandContext({ + services: { config: null }, + }); + const configCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'config', + ); + const result = await configCommand!.action!(contextWithoutConfig, 'test'); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }); + }); + + it('should provide completions for discovered agents', async () => { + mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ + getAllDiscoveredAgentNames: vi + .fn() + .mockReturnValue(['agent1', 'agent2', 'other']), + }); + + const configCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'config', + ); + expect(configCommand?.completion).toBeDefined(); + + const completions = await configCommand!.completion!(mockContext, 'age'); + expect(completions).toEqual(['agent1', 'agent2']); + }); + }); }); diff --git a/packages/cli/src/ui/commands/agentsCommand.ts b/packages/cli/src/ui/commands/agentsCommand.ts index cd1f7eb78c..fdfb329c21 100644 --- a/packages/cli/src/ui/commands/agentsCommand.ts +++ b/packages/cli/src/ui/commands/agentsCommand.ts @@ -62,7 +62,13 @@ async function enableAction( args: string, ): Promise { const { config, settings } = context.services; - if (!config) return; + if (!config) { + return { + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }; + } const agentName = args.trim(); if (!agentName) { @@ -132,7 +138,13 @@ async function disableAction( args: string, ): Promise { const { config, settings } = context.services; - if (!config) return; + if (!config) { + return { + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }; + } const agentName = args.trim(); if (!agentName) { @@ -200,6 +212,53 @@ async function disableAction( }; } +async function configAction( + context: CommandContext, + args: string, +): Promise { + const { config } = context.services; + if (!config) { + return { + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }; + } + + const agentName = args.trim(); + if (!agentName) { + return { + type: 'message', + messageType: 'error', + content: 'Usage: /agents config ', + }; + } + + const agentRegistry = config.getAgentRegistry(); + if (!agentRegistry) { + return { + type: 'message', + messageType: 'error', + content: 'Agent registry not found.', + }; + } + + const definition = agentRegistry.getDiscoveredDefinition(agentName); + if (!definition) { + return { + type: 'message', + messageType: 'error', + content: `Agent '${agentName}' not found.`, + }; + } + + return { + type: 'message', + messageType: 'info', + content: `Configuration for '${agentName}' will be available in the next update.`, + }; +} + function completeAgentsToEnable(context: CommandContext, partialArg: string) { const { config, settings } = context.services; if (!config) return []; @@ -221,6 +280,15 @@ function completeAgentsToDisable(context: CommandContext, partialArg: string) { return allAgents.filter((name: string) => name.startsWith(partialArg)); } +function completeAllAgents(context: CommandContext, partialArg: string) { + const { config } = context.services; + if (!config) return []; + + const agentRegistry = config.getAgentRegistry(); + const allAgents = agentRegistry?.getAllDiscoveredAgentNames() ?? []; + return allAgents.filter((name: string) => name.startsWith(partialArg)); +} + const enableCommand: SlashCommand = { name: 'enable', description: 'Enable a disabled agent', @@ -239,6 +307,15 @@ const disableCommand: SlashCommand = { completion: completeAgentsToDisable, }; +const configCommand: SlashCommand = { + name: 'config', + description: 'Configure an agent', + kind: CommandKind.BUILT_IN, + autoExecute: false, + action: configAction, + completion: completeAllAgents, +}; + const agentsRefreshCommand: SlashCommand = { name: 'refresh', description: 'Reload the agent registry', @@ -278,6 +355,7 @@ export const agentsCommand: SlashCommand = { agentsRefreshCommand, enableCommand, disableCommand, + configCommand, ], action: async (context: CommandContext, args) => // Default to list if no subcommand is provided