Add support for VSCode-like editors (#5699)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
christine betts 2025-08-11 21:01:37 +00:00 committed by GitHub
parent 4656f17524
commit 0e98641b51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 118 additions and 14 deletions

View File

@ -939,8 +939,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{shouldShowIdePrompt ? (
<IdeIntegrationNudge
question="Do you want to connect your VS Code editor to Gemini CLI?"
description="If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in VS Code."
ideName={config.getIdeClient().getDetectedIdeDisplayName()}
onComplete={handleIdePromptComplete}
/>
) : isFolderTrustDialogOpen ? (

View File

@ -13,14 +13,12 @@ import {
export type IdeIntegrationNudgeResult = 'yes' | 'no' | 'dismiss';
interface IdeIntegrationNudgeProps {
question: string;
description?: string;
ideName?: string;
onComplete: (result: IdeIntegrationNudgeResult) => void;
}
export function IdeIntegrationNudge({
question,
description,
ideName,
onComplete,
}: IdeIntegrationNudgeProps) {
useInput((_input, key) => {
@ -56,9 +54,11 @@ export function IdeIntegrationNudge({
<Box marginBottom={1} flexDirection="column">
<Text>
<Text color="yellow">{'> '}</Text>
{question}
{`Do you want to connect your ${ideName ?? 'your'} editor to Gemini CLI?`}
</Text>
{description && <Text dimColor>{description}</Text>}
<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>
<RadioButtonSelect
items={OPTIONS}

View File

@ -6,12 +6,33 @@
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 {
switch (ide) {
case DetectedIde.VSCode:
return 'VS Code';
case DetectedIde.VSCodium:
return 'VSCodium';
case DetectedIde.Cursor:
return 'Cursor';
case DetectedIde.CloudShell:
return 'Cloud Shell';
case DetectedIde.Codespaces:
return 'GitHub Codespaces';
case DetectedIde.Windsurf:
return 'Windsurf';
case DetectedIde.FirebaseStudio:
return 'Firebase Studio';
case DetectedIde.Trae:
return 'Trae';
default: {
// This ensures that if a new IDE is added to the enum, we get a compile-time error.
const exhaustiveCheck: never = ide;
@ -21,8 +42,24 @@ export function getIdeDisplayName(ide: DetectedIde): string {
}
export function detectIde(): DetectedIde | undefined {
if (process.env.TERM_PROGRAM === 'vscode') {
return DetectedIde.VSCode;
}
// Only VSCode-based integrations are currently supported.
if (process.env.TERM_PROGRAM !== 'vscode') {
return undefined;
}
if (process.env.CURSOR_TRACE_ID) {
return DetectedIde.Cursor;
}
if (process.env.CODESPACES) {
return DetectedIde.Codespaces;
}
if (process.env.EDITOR_IN_CLOUD_SHELL) {
return DetectedIde.CloudShell;
}
if (process.env.TERM_PRODUCT === 'Trae') {
return DetectedIde.Trae;
}
if (process.env.FIREBASE_DEPLOY_AGENT) {
return DetectedIde.FirebaseStudio;
}
return DetectedIde.VSCode;
}

View File

@ -24,9 +24,17 @@ describe('ide-installer', () => {
expect(installer).toBeInstanceOf(Object);
});
it('should return null for an unknown IDE', () => {
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);
expect(installer).toBeNull();
// Assuming DefaultIDEInstaller is the fallback
expect(installer).not.toBeNull();
expect(installer).toBeInstanceOf(Object);
});
});
@ -59,4 +67,44 @@ 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,11 +147,31 @@ 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 {
switch (ide) {
case DetectedIde.VSCode:
return new VsCodeInstaller();
default:
return null;
return new OpenVSXInstaller();
}
}