diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index b2fe4ef2e..14d5e14a7 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -207,6 +207,33 @@ describe('BuiltinCommandLoader', () => { expect(modelCmd?.name).toBe('model'); }); + it('should still load all other commands when ideCommand() throws', async () => { + // Simulate ideCommand() failure (e.g., platform-specific process detection fails) + const { ideCommand: ideCommandMock } = await import( + '../ui/commands/ideCommand.js' + ); + (ideCommandMock as Mock).mockRejectedValueOnce( + new Error('PowerShell not available'), + ); + + const loader = new BuiltinCommandLoader(mockConfig); + const commands = await loader.loadCommands(new AbortController().signal); + + // IDE command should NOT be present + const ideCmd = commands.find((c) => c.name === 'ide'); + expect(ideCmd).toBeUndefined(); + + // But all other built-in commands should still be loaded + const modelCmd = commands.find((c) => c.name === 'model'); + expect(modelCmd).toBeDefined(); + + const statusCmd = commands.find((c) => c.name === 'status'); + expect(statusCmd).toBeDefined(); + + const mcpCmd = commands.find((c) => c.name === 'mcp'); + expect(mcpCmd).toBeDefined(); + }); + it('should always include hooks command regardless of disableAllHooks', async () => { // When disableAllHooks is false const loader1 = new BuiltinCommandLoader(mockConfig); diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index df4555d73..5db6c965a 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -26,6 +26,7 @@ import { extensionsCommand } from '../ui/commands/extensionsCommand.js'; import { helpCommand } from '../ui/commands/helpCommand.js'; import { hooksCommand } from '../ui/commands/hooksCommand.js'; import { ideCommand } from '../ui/commands/ideCommand.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { initCommand } from '../ui/commands/initCommand.js'; import { languageCommand } from '../ui/commands/languageCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js'; @@ -47,6 +48,8 @@ import { vimCommand } from '../ui/commands/vimCommand.js'; import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js'; import { insightCommand } from '../ui/commands/insightCommand.js'; +const builtinDebugLogger = createDebugLogger('BUILTIN_COMMAND_LOADER'); + /** * Loads the core, hard-coded slash commands that are an integral part * of the Qwen Code application. @@ -62,6 +65,19 @@ export class BuiltinCommandLoader implements ICommandLoader { * @returns A promise that resolves to an array of `SlashCommand` objects. */ async loadCommands(_signal: AbortSignal): Promise { + // Load ideCommand separately with error handling so that a failure + // (e.g., platform-specific process detection on Windows) does not + // prevent ALL built-in commands from loading. + let resolvedIdeCommand: SlashCommand | null = null; + try { + resolvedIdeCommand = await ideCommand(); + } catch (error) { + builtinDebugLogger.warn( + 'Failed to load IDE command:', + error instanceof Error ? error.message : String(error), + ); + } + const allDefinitions: Array = [ aboutCommand, agentsCommand, @@ -81,7 +97,7 @@ export class BuiltinCommandLoader implements ICommandLoader { extensionsCommand, helpCommand, hooksCommand, - await ideCommand(), + resolvedIdeCommand, initCommand, languageCommand, mcpCommand, diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index b0d7806e7..c2124dd1b 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -333,17 +333,24 @@ export const useSlashCommandProcessor = ( useEffect(() => { const controller = new AbortController(); const load = async () => { - const loaders = [ - new McpPromptLoader(config), - new BuiltinCommandLoader(config), - new BundledSkillLoader(config), - new FileCommandLoader(config), - ]; - const commandService = await CommandService.create( - loaders, - controller.signal, - ); - setCommands(commandService.getCommands()); + try { + const loaders = [ + new McpPromptLoader(config), + new BuiltinCommandLoader(config), + new BundledSkillLoader(config), + new FileCommandLoader(config), + ]; + const commandService = await CommandService.create( + loaders, + controller.signal, + ); + // Avoid overwriting newer results from a subsequent effect run + if (!controller.signal.aborted) { + setCommands(commandService.getCommands()); + } + } catch (error) { + debugLogger.error('Failed to load slash commands:', error); + } }; load();