diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index e952d6b2..ab30b730 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -576,14 +576,18 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const handleIdePromptComplete = useCallback( (result: IdeIntegrationNudgeResult) => { - if (result === 'yes') { - handleSlashCommand('/ide install'); + if (result.userSelection === 'yes') { + if (result.isExtensionPreInstalled) { + handleSlashCommand('/ide enable'); + } else { + handleSlashCommand('/ide install'); + } settings.setValue( SettingScope.User, 'hasSeenIdeIntegrationNudge', true, ); - } else if (result === 'dismiss') { + } else if (result.userSelection === 'dismiss') { settings.setValue( SettingScope.User, 'hasSeenIdeIntegrationNudge', @@ -942,9 +946,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { )} - {shouldShowIdePrompt ? ( + {shouldShowIdePrompt && currentIDE ? ( ) : isFolderTrustDialogOpen ? ( diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx index f0c6172d..13f70a75 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx @@ -4,44 +4,74 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { DetectedIde, getIdeInfo } from '@google/gemini-cli-core'; import { Box, Text, useInput } from 'ink'; import { RadioButtonSelect, RadioSelectItem, } from './components/shared/RadioButtonSelect.js'; -export type IdeIntegrationNudgeResult = 'yes' | 'no' | 'dismiss'; +export type IdeIntegrationNudgeResult = { + userSelection: 'yes' | 'no' | 'dismiss'; + isExtensionPreInstalled: boolean; +}; interface IdeIntegrationNudgeProps { - ideName?: string; + ide: DetectedIde; onComplete: (result: IdeIntegrationNudgeResult) => void; } export function IdeIntegrationNudge({ - ideName, + ide, onComplete, }: IdeIntegrationNudgeProps) { useInput((_input, key) => { 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> = [ { label: 'Yes', - value: 'yes', + value: { + userSelection: 'yes', + isExtensionPreInstalled, + }, }, { label: 'No (esc)', - value: 'no', + value: { + userSelection: 'no', + isExtensionPreInstalled, + }, }, { 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 ( {'> '} - {`Do you want to connect your ${ideName ?? 'your'} editor to Gemini CLI?`} + {`Do you want to connect ${ideName ?? 'your'} editor to Gemini CLI?`} - {`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'}.`} + {installText} { 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)) + .map((ide) => getIdeInfo(ide).displayName) .join(', ')}`, }) as const, }; diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index 759c4103..ef07994c 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -6,33 +6,43 @@ export enum DetectedIde { VSCode = 'vscode', - VSCodium = 'vscodium', Cursor = 'cursor', CloudShell = 'cloudshell', Codespaces = 'codespaces', - Windsurf = 'windsurf', FirebaseStudio = 'firebasestudio', Trae = 'trae', } -export function getIdeDisplayName(ide: DetectedIde): string { +export interface IdeInfo { + displayName: string; +} + +export function getIdeInfo(ide: DetectedIde): IdeInfo { switch (ide) { case DetectedIde.VSCode: - return 'VS Code'; - case DetectedIde.VSCodium: - return 'VSCodium'; + return { + displayName: 'VS Code', + }; case DetectedIde.Cursor: - return 'Cursor'; + return { + displayName: 'Cursor', + }; case DetectedIde.CloudShell: - return 'Cloud Shell'; + return { + displayName: 'Cloud Shell', + }; case DetectedIde.Codespaces: - return 'GitHub Codespaces'; - case DetectedIde.Windsurf: - return 'Windsurf'; + return { + displayName: 'GitHub Codespaces', + }; case DetectedIde.FirebaseStudio: - return 'Firebase Studio'; + return { + displayName: 'Firebase Studio', + }; case DetectedIde.Trae: - return 'Trae'; + return { + displayName: 'Trae', + }; default: { // This ensures that if a new IDE is added to the enum, we get a compile-time error. const exhaustiveCheck: never = ide; diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts index 74d0df74..fe605eb2 100644 --- a/packages/core/src/ide/ide-client.ts +++ b/packages/core/src/ide/ide-client.ts @@ -6,11 +6,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { - detectIde, - DetectedIde, - getIdeDisplayName, -} from '../ide/detect-ide.js'; +import { detectIde, DetectedIde, getIdeInfo } from '../ide/detect-ide.js'; import { ideContext, IdeContextNotificationSchema, @@ -68,7 +64,7 @@ export class IdeClient { private constructor() { this.currentIde = detectIde(); 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( DetectedIde, ) - .map((ide) => getIdeDisplayName(ide)) + .map((ide) => getIdeInfo(ide).displayName) .join(', ')}`, false, ); diff --git a/packages/core/src/ide/ide-installer.test.ts b/packages/core/src/ide/ide-installer.test.ts index 1afd7a36..e43e1b34 100644 --- a/packages/core/src/ide/ide-installer.test.ts +++ b/packages/core/src/ide/ide-installer.test.ts @@ -23,19 +23,6 @@ describe('ide-installer', () => { // A more specific check might be needed if we export the class 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', () => { @@ -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', - ); - }); - }); - }); }); diff --git a/packages/core/src/ide/ide-installer.ts b/packages/core/src/ide/ide-installer.ts index e6192bfa..7db8e2d2 100644 --- a/packages/core/src/ide/ide-installer.ts +++ b/packages/core/src/ide/ide-installer.ts @@ -147,31 +147,11 @@ class VsCodeInstaller implements IdeInstaller { } } -class OpenVSXInstaller implements IdeInstaller { - async install(): Promise { - // 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 { switch (ide) { case DetectedIde.VSCode: return new VsCodeInstaller(); default: - return new OpenVSXInstaller(); + return null; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e60bd048..791446e3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -50,7 +50,7 @@ export * from './services/gitService.js'; export * from './ide/ide-client.js'; export * from './ide/ideContext.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 * from './services/shellExecutionService.js';