[ide-mode] Update installation logic and nudge (#6068)
This commit is contained in:
parent
8524cce7b9
commit
74fd0841d0
|
@ -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 ? (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue