[ide-mode] Update installation logic and nudge (#6068)

This commit is contained in:
christine betts 2025-08-12 20:08:47 +00:00 committed by GitHub
parent 8524cce7b9
commit 74fd0841d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 78 additions and 113 deletions

View File

@ -576,14 +576,18 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const handleIdePromptComplete = useCallback( const handleIdePromptComplete = useCallback(
(result: IdeIntegrationNudgeResult) => { (result: IdeIntegrationNudgeResult) => {
if (result === 'yes') { if (result.userSelection === 'yes') {
if (result.isExtensionPreInstalled) {
handleSlashCommand('/ide enable');
} else {
handleSlashCommand('/ide install'); handleSlashCommand('/ide install');
}
settings.setValue( settings.setValue(
SettingScope.User, SettingScope.User,
'hasSeenIdeIntegrationNudge', 'hasSeenIdeIntegrationNudge',
true, true,
); );
} else if (result === 'dismiss') { } else if (result.userSelection === 'dismiss') {
settings.setValue( settings.setValue(
SettingScope.User, SettingScope.User,
'hasSeenIdeIntegrationNudge', 'hasSeenIdeIntegrationNudge',
@ -942,9 +946,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Box> </Box>
)} )}
{shouldShowIdePrompt ? ( {shouldShowIdePrompt && currentIDE ? (
<IdeIntegrationNudge <IdeIntegrationNudge
ideName={config.getIdeClient().getDetectedIdeDisplayName()} ide={currentIDE}
onComplete={handleIdePromptComplete} onComplete={handleIdePromptComplete}
/> />
) : isFolderTrustDialogOpen ? ( ) : isFolderTrustDialogOpen ? (

View File

@ -4,44 +4,74 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { DetectedIde, getIdeInfo } from '@google/gemini-cli-core';
import { Box, Text, useInput } from 'ink'; import { Box, Text, useInput } from 'ink';
import { import {
RadioButtonSelect, RadioButtonSelect,
RadioSelectItem, RadioSelectItem,
} from './components/shared/RadioButtonSelect.js'; } from './components/shared/RadioButtonSelect.js';
export type IdeIntegrationNudgeResult = 'yes' | 'no' | 'dismiss'; export type IdeIntegrationNudgeResult = {
userSelection: 'yes' | 'no' | 'dismiss';
isExtensionPreInstalled: boolean;
};
interface IdeIntegrationNudgeProps { interface IdeIntegrationNudgeProps {
ideName?: string; ide: DetectedIde;
onComplete: (result: IdeIntegrationNudgeResult) => void; onComplete: (result: IdeIntegrationNudgeResult) => void;
} }
export function IdeIntegrationNudge({ export function IdeIntegrationNudge({
ideName, ide,
onComplete, onComplete,
}: IdeIntegrationNudgeProps) { }: IdeIntegrationNudgeProps) {
useInput((_input, key) => { useInput((_input, key) => {
if (key.escape) { if (key.escape) {
onComplete('no'); onComplete({
userSelection: 'no',
isExtensionPreInstalled: false,
});
} }
}); });
const { displayName: ideName } = getIdeInfo(ide);
// Assume extension is already installed if the env variables are set.
const isExtensionPreInstalled =
!!process.env.GEMINI_CLI_IDE_SERVER_PORT &&
!!process.env.GEMINI_CLI_IDE_WORKSPACE_PATH;
const OPTIONS: Array<RadioSelectItem<IdeIntegrationNudgeResult>> = [ const OPTIONS: Array<RadioSelectItem<IdeIntegrationNudgeResult>> = [
{ {
label: 'Yes', label: 'Yes',
value: 'yes', value: {
userSelection: 'yes',
isExtensionPreInstalled,
},
}, },
{ {
label: 'No (esc)', label: 'No (esc)',
value: 'no', value: {
userSelection: 'no',
isExtensionPreInstalled,
},
}, },
{ {
label: "No, don't ask again", label: "No, don't ask again",
value: 'dismiss', value: {
userSelection: 'dismiss',
isExtensionPreInstalled,
},
}, },
]; ];
const installText = isExtensionPreInstalled
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`;
return ( return (
<Box <Box
flexDirection="column" flexDirection="column"
@ -54,11 +84,9 @@ export function IdeIntegrationNudge({
<Box marginBottom={1} flexDirection="column"> <Box marginBottom={1} flexDirection="column">
<Text> <Text>
<Text color="yellow">{'> '}</Text> <Text color="yellow">{'> '}</Text>
{`Do you want to connect your ${ideName ?? 'your'} editor to Gemini CLI?`} {`Do you want to connect ${ideName ?? 'your'} editor to Gemini CLI?`}
</Text> </Text>
<Text <Text dimColor>{installText}</Text>
dimColor
>{`If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${ideName ?? 'your editor'}.`}</Text>
</Box> </Box>
<RadioButtonSelect <RadioButtonSelect
items={OPTIONS} items={OPTIONS}

View File

@ -8,7 +8,7 @@ import {
Config, Config,
DetectedIde, DetectedIde,
IDEConnectionStatus, IDEConnectionStatus,
getIdeDisplayName, getIdeInfo,
getIdeInstaller, getIdeInstaller,
IdeClient, IdeClient,
type File, type File,
@ -132,7 +132,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
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( 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, DetectedIde,
) )
.map((ide) => getIdeDisplayName(ide)) .map((ide) => getIdeInfo(ide).displayName)
.join(', ')}`, .join(', ')}`,
}) as const, }) as const,
}; };

View File

@ -6,33 +6,43 @@
export enum DetectedIde { export enum DetectedIde {
VSCode = 'vscode', VSCode = 'vscode',
VSCodium = 'vscodium',
Cursor = 'cursor', Cursor = 'cursor',
CloudShell = 'cloudshell', CloudShell = 'cloudshell',
Codespaces = 'codespaces', Codespaces = 'codespaces',
Windsurf = 'windsurf',
FirebaseStudio = 'firebasestudio', FirebaseStudio = 'firebasestudio',
Trae = 'trae', Trae = 'trae',
} }
export function getIdeDisplayName(ide: DetectedIde): string { export interface IdeInfo {
displayName: string;
}
export function getIdeInfo(ide: DetectedIde): IdeInfo {
switch (ide) { switch (ide) {
case DetectedIde.VSCode: case DetectedIde.VSCode:
return 'VS Code'; return {
case DetectedIde.VSCodium: displayName: 'VS Code',
return 'VSCodium'; };
case DetectedIde.Cursor: case DetectedIde.Cursor:
return 'Cursor'; return {
displayName: 'Cursor',
};
case DetectedIde.CloudShell: case DetectedIde.CloudShell:
return 'Cloud Shell'; return {
displayName: 'Cloud Shell',
};
case DetectedIde.Codespaces: case DetectedIde.Codespaces:
return 'GitHub Codespaces'; return {
case DetectedIde.Windsurf: displayName: 'GitHub Codespaces',
return 'Windsurf'; };
case DetectedIde.FirebaseStudio: case DetectedIde.FirebaseStudio:
return 'Firebase Studio'; return {
displayName: 'Firebase Studio',
};
case DetectedIde.Trae: case DetectedIde.Trae:
return 'Trae'; return {
displayName: 'Trae',
};
default: { default: {
// This ensures that if a new IDE is added to the enum, we get a compile-time error. // This ensures that if a new IDE is added to the enum, we get a compile-time error.
const exhaustiveCheck: never = ide; const exhaustiveCheck: never = ide;

View File

@ -6,11 +6,7 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { import { detectIde, DetectedIde, getIdeInfo } from '../ide/detect-ide.js';
detectIde,
DetectedIde,
getIdeDisplayName,
} from '../ide/detect-ide.js';
import { import {
ideContext, ideContext,
IdeContextNotificationSchema, IdeContextNotificationSchema,
@ -68,7 +64,7 @@ export class IdeClient {
private constructor() { private constructor() {
this.currentIde = detectIde(); this.currentIde = detectIde();
if (this.currentIde) { if (this.currentIde) {
this.currentIdeDisplayName = getIdeDisplayName(this.currentIde); this.currentIdeDisplayName = getIdeInfo(this.currentIde).displayName;
} }
} }
@ -86,7 +82,7 @@ export class IdeClient {
`IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: ${Object.values( `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, DetectedIde,
) )
.map((ide) => getIdeDisplayName(ide)) .map((ide) => getIdeInfo(ide).displayName)
.join(', ')}`, .join(', ')}`,
false, false,
); );

View File

@ -23,19 +23,6 @@ describe('ide-installer', () => {
// A more specific check might be needed if we export the class // A more specific check might be needed if we export the class
expect(installer).toBeInstanceOf(Object); expect(installer).toBeInstanceOf(Object);
}); });
it('should return an OpenVSXInstaller for "vscodium"', () => {
const installer = getIdeInstaller(DetectedIde.VSCodium);
expect(installer).not.toBeNull();
expect(installer).toBeInstanceOf(Object);
});
it('should return a DefaultIDEInstaller for an unknown IDE', () => {
const installer = getIdeInstaller('unknown' as DetectedIde);
// Assuming DefaultIDEInstaller is the fallback
expect(installer).not.toBeNull();
expect(installer).toBeInstanceOf(Object);
});
}); });
describe('VsCodeInstaller', () => { describe('VsCodeInstaller', () => {
@ -67,44 +54,4 @@ describe('ide-installer', () => {
}); });
}); });
}); });
describe('OpenVSXInstaller', () => {
let installer: IdeInstaller;
beforeEach(() => {
installer = getIdeInstaller(DetectedIde.VSCodium)!;
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('install', () => {
it('should call execSync with the correct command and return success', async () => {
const execSyncSpy = vi
.spyOn(child_process, 'execSync')
.mockImplementation(() => '');
const result = await installer.install();
expect(execSyncSpy).toHaveBeenCalledWith(
'npx ovsx get google.gemini-cli-vscode-ide-companion',
{ stdio: 'pipe' },
);
expect(result.success).toBe(true);
expect(result.message).toContain(
'VS Code companion extension was installed successfully from OpenVSX',
);
});
it('should return a failure message on failed installation', async () => {
vi.spyOn(child_process, 'execSync').mockImplementation(() => {
throw new Error('Command failed');
});
const result = await installer.install();
expect(result.success).toBe(false);
expect(result.message).toContain(
'Failed to install VS Code companion extension from OpenVSX',
);
});
});
});
}); });

View File

@ -147,31 +147,11 @@ class VsCodeInstaller implements IdeInstaller {
} }
} }
class OpenVSXInstaller implements IdeInstaller {
async install(): Promise<InstallResult> {
// TODO: Use the correct extension path.
const command = `npx ovsx get google.gemini-cli-vscode-ide-companion`;
try {
child_process.execSync(command, { stdio: 'pipe' });
return {
success: true,
message:
'VS Code companion extension was installed successfully from OpenVSX. Please restart your terminal to complete the setup.',
};
} catch (_error) {
return {
success: false,
message: `Failed to install VS Code companion extension from OpenVSX. Please try installing it manually.`,
};
}
}
}
export function getIdeInstaller(ide: DetectedIde): IdeInstaller | null { export function getIdeInstaller(ide: DetectedIde): IdeInstaller | null {
switch (ide) { switch (ide) {
case DetectedIde.VSCode: case DetectedIde.VSCode:
return new VsCodeInstaller(); return new VsCodeInstaller();
default: default:
return new OpenVSXInstaller(); return null;
} }
} }

View File

@ -50,7 +50,7 @@ export * from './services/gitService.js';
export * from './ide/ide-client.js'; export * from './ide/ide-client.js';
export * from './ide/ideContext.js'; export * from './ide/ideContext.js';
export * from './ide/ide-installer.js'; export * from './ide/ide-installer.js';
export { getIdeDisplayName, DetectedIde } from './ide/detect-ide.js'; export { getIdeInfo, DetectedIde, IdeInfo } from './ide/detect-ide.js';
// Export Shell Execution Service // Export Shell Execution Service
export * from './services/shellExecutionService.js'; export * from './services/shellExecutionService.js';