Improve user-facing error messages for IDE mode (#5522)

This commit is contained in:
Shreya Keshive 2025-08-04 17:06:17 -04:00 committed by GitHub
parent 11808ef7ed
commit 2180dd13dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 43 additions and 54 deletions

View File

@ -65,6 +65,7 @@ describe('ideCommand', () => {
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true); vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
vi.mocked(mockConfig.getIdeClient).mockReturnValue({ vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getCurrentIde: () => DetectedIde.VSCode, getCurrentIde: () => DetectedIde.VSCode,
getDetectedIdeDisplayName: () => 'VS Code',
} as ReturnType<Config['getIdeClient']>); } as ReturnType<Config['getIdeClient']>);
const command = ideCommand(mockConfig); const command = ideCommand(mockConfig);
expect(command).not.toBeNull(); expect(command).not.toBeNull();
@ -82,6 +83,7 @@ describe('ideCommand', () => {
vi.mocked(mockConfig.getIdeClient).mockReturnValue({ vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getConnectionStatus: mockGetConnectionStatus, getConnectionStatus: mockGetConnectionStatus,
getCurrentIde: () => DetectedIde.VSCode, getCurrentIde: () => DetectedIde.VSCode,
getDetectedIdeDisplayName: () => 'VS Code',
} as unknown as ReturnType<Config['getIdeClient']>); } as unknown as ReturnType<Config['getIdeClient']>);
}); });
@ -96,7 +98,7 @@ describe('ideCommand', () => {
expect(result).toEqual({ expect(result).toEqual({
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: '🟢 Connected', content: '🟢 Connected to VS Code',
}); });
}); });
@ -155,6 +157,7 @@ describe('ideCommand', () => {
vi.mocked(mockConfig.getIdeClient).mockReturnValue({ vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getCurrentIde: () => DetectedIde.VSCode, getCurrentIde: () => DetectedIde.VSCode,
getConnectionStatus: vi.fn(), getConnectionStatus: vi.fn(),
getDetectedIdeDisplayName: () => 'VS Code',
} as unknown as ReturnType<Config['getIdeClient']>); } as unknown as ReturnType<Config['getIdeClient']>);
vi.mocked(core.getIdeInstaller).mockReturnValue({ vi.mocked(core.getIdeInstaller).mockReturnValue({
install: mockInstall, install: mockInstall,
@ -180,7 +183,7 @@ describe('ideCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
type: 'info', type: 'info',
text: `Installing IDE companion extension...`, text: `Installing IDE companion...`,
}), }),
expect.any(Number), expect.any(Number),
); );
@ -210,7 +213,7 @@ describe('ideCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
type: 'info', type: 'info',
text: `Installing IDE companion extension...`, text: `Installing IDE companion...`,
}), }),
expect.any(Number), expect.any(Number),
); );

View File

@ -6,6 +6,7 @@
import { import {
Config, Config,
DetectedIde,
IDEConnectionStatus, IDEConnectionStatus,
getIdeDisplayName, getIdeDisplayName,
getIdeInstaller, getIdeInstaller,
@ -19,12 +20,27 @@ import {
import { SettingScope } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js';
export const ideCommand = (config: Config | null): SlashCommand | null => { export const ideCommand = (config: Config | null): SlashCommand | null => {
if (!config?.getIdeModeFeature()) { if (!config || !config.getIdeModeFeature()) {
return null; return null;
} }
const currentIDE = config.getIdeClient().getCurrentIde(); const ideClient = config.getIdeClient();
if (!currentIDE) { const currentIDE = ideClient.getCurrentIde();
return null; 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 = { const ideSlashCommand: SlashCommand = {
@ -39,13 +55,13 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
description: 'check status of IDE integration', description: 'check status of IDE integration',
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: (_context: CommandContext): SlashCommandActionReturn => { action: (_context: CommandContext): SlashCommandActionReturn => {
const connection = config.getIdeClient().getConnectionStatus(); const connection = ideClient.getConnectionStatus();
switch (connection?.status) { switch (connection.status) {
case IDEConnectionStatus.Connected: case IDEConnectionStatus.Connected:
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
content: `🟢 Connected`, content: `🟢 Connected to ${ideClient.getDetectedIdeDisplayName()}`,
} as const; } as const;
case IDEConnectionStatus.Connecting: case IDEConnectionStatus.Connecting:
return { return {
@ -70,7 +86,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
const installCommand: SlashCommand = { const installCommand: SlashCommand = {
name: 'install', name: 'install',
description: `install required IDE companion ${getIdeDisplayName(currentIDE)} extension `, description: `install required IDE companion for ${ideClient.getDetectedIdeDisplayName()}`,
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
action: async (context) => { action: async (context) => {
const installer = getIdeInstaller(currentIDE); const installer = getIdeInstaller(currentIDE);
@ -78,7 +94,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
context.ui.addItem( context.ui.addItem(
{ {
type: 'error', 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(), Date.now(),
); );
@ -88,7 +104,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
context.ui.addItem( context.ui.addItem(
{ {
type: 'info', type: 'info',
text: `Installing IDE companion extension...`, text: `Installing IDE companion...`,
}, },
Date.now(), Date.now(),
); );

View File

@ -11,9 +11,12 @@ export enum DetectedIde {
export function getIdeDisplayName(ide: DetectedIde): string { export function getIdeDisplayName(ide: DetectedIde): string {
switch (ide) { switch (ide) {
case DetectedIde.VSCode: case DetectedIde.VSCode:
return 'VSCode'; return 'VS Code';
default: default: {
throw new Error(`Unsupported IDE: ${ide}`); // This ensures that if a new IDE is added to the enum, we get a compile-time error.
const exhaustiveCheck: never = ide;
return exhaustiveCheck;
}
} }
} }

View File

@ -45,32 +45,6 @@ describe('ide-installer', () => {
vi.restoreAllMocks(); 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', () => { describe('install', () => {
it('should return a failure message if VS Code is not installed', async () => { it('should return a failure message if VS Code is not installed', async () => {
vi.spyOn(child_process, 'execSync').mockImplementation(() => { vi.spyOn(child_process, 'execSync').mockImplementation(() => {
@ -81,9 +55,7 @@ describe('ide-installer', () => {
installer = getIdeInstaller(DetectedIde.VSCode)!; installer = getIdeInstaller(DetectedIde.VSCode)!;
const result = await installer.install(); const result = await installer.install();
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.message).toContain( expect(result.message).toContain('VS Code CLI not found');
'not found in your PATH or common installation locations',
);
}); });
}); });
}); });

View File

@ -18,7 +18,6 @@ const VSCODE_COMPANION_EXTENSION_FOLDER = 'vscode-ide-companion';
export interface IdeInstaller { export interface IdeInstaller {
install(): Promise<InstallResult>; install(): Promise<InstallResult>;
isInstalled(): Promise<boolean>;
} }
export interface InstallResult { export interface InstallResult {
@ -95,16 +94,12 @@ class VsCodeInstaller implements IdeInstaller {
this.vsCodeCommand = findVsCodeCommand(); this.vsCodeCommand = findVsCodeCommand();
} }
async isInstalled(): Promise<boolean> {
return (await this.vsCodeCommand) !== null;
}
async install(): Promise<InstallResult> { async install(): Promise<InstallResult> {
const commandPath = await this.vsCodeCommand; const commandPath = await this.vsCodeCommand;
if (!commandPath) { if (!commandPath) {
return { return {
success: false, 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 { return {
success: true, success: true,
message: 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) { } catch (_error) {
return { return {
success: false, 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 { export function getIdeInstaller(ide: DetectedIde): IdeInstaller | null {
switch (ide) { switch (ide) {
case 'vscode': case DetectedIde.VSCode:
return new VsCodeInstaller(); return new VsCodeInstaller();
default: default:
return null; return null;