From 2180dd13dc580db4cef77b39aa69eaa8017530ea Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Mon, 4 Aug 2025 17:06:17 -0400 Subject: [PATCH] Improve user-facing error messages for IDE mode (#5522) --- .../cli/src/ui/commands/ideCommand.test.ts | 9 +++-- packages/cli/src/ui/commands/ideCommand.ts | 36 +++++++++++++------ packages/core/src/ide/detect-ide.ts | 9 +++-- packages/core/src/ide/ide-installer.test.ts | 30 +--------------- packages/core/src/ide/ide-installer.ts | 13 +++---- 5 files changed, 43 insertions(+), 54 deletions(-) diff --git a/packages/cli/src/ui/commands/ideCommand.test.ts b/packages/cli/src/ui/commands/ideCommand.test.ts index 3c73549c..4f2b7af2 100644 --- a/packages/cli/src/ui/commands/ideCommand.test.ts +++ b/packages/cli/src/ui/commands/ideCommand.test.ts @@ -65,6 +65,7 @@ describe('ideCommand', () => { vi.mocked(mockConfig.getIdeMode).mockReturnValue(true); vi.mocked(mockConfig.getIdeClient).mockReturnValue({ getCurrentIde: () => DetectedIde.VSCode, + getDetectedIdeDisplayName: () => 'VS Code', } as ReturnType); const command = ideCommand(mockConfig); expect(command).not.toBeNull(); @@ -82,6 +83,7 @@ describe('ideCommand', () => { vi.mocked(mockConfig.getIdeClient).mockReturnValue({ getConnectionStatus: mockGetConnectionStatus, getCurrentIde: () => DetectedIde.VSCode, + getDetectedIdeDisplayName: () => 'VS Code', } as unknown as ReturnType); }); @@ -96,7 +98,7 @@ describe('ideCommand', () => { expect(result).toEqual({ type: 'message', messageType: 'info', - content: '🟢 Connected', + content: '🟢 Connected to VS Code', }); }); @@ -155,6 +157,7 @@ describe('ideCommand', () => { vi.mocked(mockConfig.getIdeClient).mockReturnValue({ getCurrentIde: () => DetectedIde.VSCode, getConnectionStatus: vi.fn(), + getDetectedIdeDisplayName: () => 'VS Code', } as unknown as ReturnType); vi.mocked(core.getIdeInstaller).mockReturnValue({ install: mockInstall, @@ -180,7 +183,7 @@ describe('ideCommand', () => { expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: 'info', - text: `Installing IDE companion extension...`, + text: `Installing IDE companion...`, }), expect.any(Number), ); @@ -210,7 +213,7 @@ describe('ideCommand', () => { expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: 'info', - text: `Installing IDE companion extension...`, + text: `Installing IDE companion...`, }), expect.any(Number), ); diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts index 1da7d6b0..c6d65264 100644 --- a/packages/cli/src/ui/commands/ideCommand.ts +++ b/packages/cli/src/ui/commands/ideCommand.ts @@ -6,6 +6,7 @@ import { Config, + DetectedIde, IDEConnectionStatus, getIdeDisplayName, getIdeInstaller, @@ -19,12 +20,27 @@ import { import { SettingScope } from '../../config/settings.js'; export const ideCommand = (config: Config | null): SlashCommand | null => { - if (!config?.getIdeModeFeature()) { + if (!config || !config.getIdeModeFeature()) { return null; } - const currentIDE = config.getIdeClient().getCurrentIde(); - if (!currentIDE) { - return null; + const ideClient = config.getIdeClient(); + const currentIDE = ideClient.getCurrentIde(); + if (!currentIDE || !ideClient.getDetectedIdeDisplayName()) { + return { + name: 'ide', + description: 'manage IDE integration', + kind: CommandKind.BUILT_IN, + action: (): SlashCommandActionReturn => + ({ + type: 'message', + messageType: 'error', + content: `IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: ${Object.values( + DetectedIde, + ) + .map((ide) => getIdeDisplayName(ide)) + .join(', ')}`, + }) as const, + }; } const ideSlashCommand: SlashCommand = { @@ -39,13 +55,13 @@ export const ideCommand = (config: Config | null): SlashCommand | null => { description: 'check status of IDE integration', kind: CommandKind.BUILT_IN, action: (_context: CommandContext): SlashCommandActionReturn => { - const connection = config.getIdeClient().getConnectionStatus(); - switch (connection?.status) { + const connection = ideClient.getConnectionStatus(); + switch (connection.status) { case IDEConnectionStatus.Connected: return { type: 'message', messageType: 'info', - content: `🟢 Connected`, + content: `🟢 Connected to ${ideClient.getDetectedIdeDisplayName()}`, } as const; case IDEConnectionStatus.Connecting: return { @@ -70,7 +86,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => { const installCommand: SlashCommand = { name: 'install', - description: `install required IDE companion ${getIdeDisplayName(currentIDE)} extension `, + description: `install required IDE companion for ${ideClient.getDetectedIdeDisplayName()}`, kind: CommandKind.BUILT_IN, action: async (context) => { const installer = getIdeInstaller(currentIDE); @@ -78,7 +94,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => { context.ui.addItem( { type: 'error', - text: 'No installer available for your configured IDE.', + text: `No installer is available for ${ideClient.getDetectedIdeDisplayName()}. Please install the IDE companion manually from its marketplace.`, }, Date.now(), ); @@ -88,7 +104,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => { context.ui.addItem( { type: 'info', - text: `Installing IDE companion extension...`, + text: `Installing IDE companion...`, }, Date.now(), ); diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index ae46789e..f3d8cc63 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -11,9 +11,12 @@ export enum DetectedIde { export function getIdeDisplayName(ide: DetectedIde): string { switch (ide) { case DetectedIde.VSCode: - return 'VSCode'; - default: - throw new Error(`Unsupported IDE: ${ide}`); + return 'VS Code'; + default: { + // This ensures that if a new IDE is added to the enum, we get a compile-time error. + const exhaustiveCheck: never = ide; + return exhaustiveCheck; + } } } diff --git a/packages/core/src/ide/ide-installer.test.ts b/packages/core/src/ide/ide-installer.test.ts index 83459d6b..698c3173 100644 --- a/packages/core/src/ide/ide-installer.test.ts +++ b/packages/core/src/ide/ide-installer.test.ts @@ -45,32 +45,6 @@ describe('ide-installer', () => { vi.restoreAllMocks(); }); - describe('isInstalled', () => { - it('should return true if command is in PATH', async () => { - expect(await installer.isInstalled()).toBe(true); - }); - - it('should return true if command is in a known location', async () => { - vi.spyOn(child_process, 'execSync').mockImplementation(() => { - throw new Error('Command not found'); - }); - vi.spyOn(fs, 'existsSync').mockReturnValue(true); - // Re-create the installer so it re-runs findVsCodeCommand - installer = getIdeInstaller(DetectedIde.VSCode)!; - expect(await installer.isInstalled()).toBe(true); - }); - - it('should return false if command is not found', async () => { - vi.spyOn(child_process, 'execSync').mockImplementation(() => { - throw new Error('Command not found'); - }); - vi.spyOn(fs, 'existsSync').mockReturnValue(false); - // Re-create the installer so it re-runs findVsCodeCommand - installer = getIdeInstaller(DetectedIde.VSCode)!; - expect(await installer.isInstalled()).toBe(false); - }); - }); - describe('install', () => { it('should return a failure message if VS Code is not installed', async () => { vi.spyOn(child_process, 'execSync').mockImplementation(() => { @@ -81,9 +55,7 @@ describe('ide-installer', () => { installer = getIdeInstaller(DetectedIde.VSCode)!; const result = await installer.install(); expect(result.success).toBe(false); - expect(result.message).toContain( - 'not found in your PATH or common installation locations', - ); + expect(result.message).toContain('VS Code CLI not found'); }); }); }); diff --git a/packages/core/src/ide/ide-installer.ts b/packages/core/src/ide/ide-installer.ts index 725f4f7c..7db8e2d2 100644 --- a/packages/core/src/ide/ide-installer.ts +++ b/packages/core/src/ide/ide-installer.ts @@ -18,7 +18,6 @@ const VSCODE_COMPANION_EXTENSION_FOLDER = 'vscode-ide-companion'; export interface IdeInstaller { install(): Promise; - isInstalled(): Promise; } export interface InstallResult { @@ -95,16 +94,12 @@ class VsCodeInstaller implements IdeInstaller { this.vsCodeCommand = findVsCodeCommand(); } - async isInstalled(): Promise { - return (await this.vsCodeCommand) !== null; - } - async install(): Promise { const commandPath = await this.vsCodeCommand; if (!commandPath) { return { success: false, - message: `VS Code command-line tool not found in your PATH or common installation locations.`, + message: `VS Code CLI not found. Please ensure 'code' is in your system's PATH. For help, see https://code.visualstudio.com/docs/configure/command-line#_code-is-not-recognized-as-an-internal-or-external-command. You can also install the companion extension manually from the VS Code marketplace.`, }; } @@ -141,12 +136,12 @@ class VsCodeInstaller implements IdeInstaller { return { success: true, message: - 'VS Code companion extension installed successfully. Restart gemini-cli in a fresh terminal window.', + 'VS Code companion extension was installed successfully. Please restart your terminal to complete the setup.', }; } catch (_error) { return { success: false, - message: 'Failed to install VS Code companion extension.', + message: `Failed to install VS Code companion extension. Please try installing it manually from the VS Code marketplace.`, }; } } @@ -154,7 +149,7 @@ class VsCodeInstaller implements IdeInstaller { export function getIdeInstaller(ide: DetectedIde): IdeInstaller | null { switch (ide) { - case 'vscode': + case DetectedIde.VSCode: return new VsCodeInstaller(); default: return null;