From fadc477001cfcf988b9565b5d9d6ccd0da15a35c Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Mon, 14 Jul 2025 12:04:08 -0400 Subject: [PATCH] Add feature flag for IDE integration (#3927) Co-authored-by: Scott Densmore --- packages/cli/src/config/config.test.ts | 114 +++++++++++++++++++++++++ packages/cli/src/config/config.ts | 30 +++++++ packages/cli/src/config/settings.ts | 1 + packages/core/src/config/config.ts | 7 ++ 4 files changed, 152 insertions(+) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 08a85e4d..ad8b1d44 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -768,3 +768,117 @@ describe('loadCliConfig extensions', () => { expect(config.getExtensionContextFilePaths()).toEqual(['/path/to/ext1.md']); }); }); + +describe('loadCliConfig ideMode', () => { + const originalArgv = process.argv; + const originalEnv = { ...process.env }; + + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + process.env.GEMINI_API_KEY = 'test-api-key'; + // Explicitly delete TERM_PROGRAM and SANDBOX before each test + delete process.env.TERM_PROGRAM; + delete process.env.SANDBOX; + }); + + afterEach(() => { + process.argv = originalArgv; + process.env = originalEnv; + vi.restoreAllMocks(); + }); + + it('should be false by default', async () => { + process.argv = ['node', 'script.js']; + const settings: Settings = {}; + const argv = await parseArguments(); + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(false); + }); + + it('should be false if --ide-mode is true but TERM_PROGRAM is not vscode', async () => { + process.argv = ['node', 'script.js', '--ide-mode']; + const settings: Settings = {}; + const argv = await parseArguments(); + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(false); + }); + + it('should be false if settings.ideMode is true but TERM_PROGRAM is not vscode', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const settings: Settings = { ideMode: true }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(false); + }); + + it('should be true when --ide-mode is set and TERM_PROGRAM is vscode', async () => { + process.argv = ['node', 'script.js', '--ide-mode']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(true); + }); + + it('should be true when settings.ideMode is true and TERM_PROGRAM is vscode', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + const settings: Settings = { ideMode: true }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(true); + }); + + it('should prioritize --ide-mode (true) over settings (false) when TERM_PROGRAM is vscode', async () => { + process.argv = ['node', 'script.js', '--ide-mode']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + const settings: Settings = { ideMode: false }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(true); + }); + + it('should prioritize --no-ide-mode (false) over settings (true) even when TERM_PROGRAM is vscode', async () => { + process.argv = ['node', 'script.js', '--no-ide-mode']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + const settings: Settings = { ideMode: true }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(false); + }); + + it('should be false when --ide-mode is true, TERM_PROGRAM is vscode, but SANDBOX is set', async () => { + process.argv = ['node', 'script.js', '--ide-mode']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + process.env.SANDBOX = 'true'; + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(false); + }); + + it('should be false when settings.ideMode is true, TERM_PROGRAM is vscode, but SANDBOX is set', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + process.env.SANDBOX = 'true'; + const settings: Settings = { ideMode: true }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(false); + }); + + it('should add __ide_server when ideMode is true', async () => { + process.argv = ['node', 'script.js', '--ide-mode']; + const argv = await parseArguments(); + process.env.TERM_PROGRAM = 'vscode'; + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getIdeMode()).toBe(true); + const mcpServers = config.getMcpServers(); + expect(mcpServers['_ide_server']).toBeDefined(); + expect(mcpServers['_ide_server'].httpUrl).toBe('http://localhost:3000/mcp'); + expect(mcpServers['_ide_server'].description).toBe('IDE connection'); + expect(mcpServers['_ide_server'].trust).toBe(false); + }); +}); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 1c1f0746..626f23e1 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -17,6 +17,7 @@ import { DEFAULT_GEMINI_EMBEDDING_MODEL, FileDiscoveryService, TelemetryTarget, + MCPServerConfig, } from '@google/gemini-cli-core'; import { Settings } from './settings.js'; @@ -54,6 +55,7 @@ export interface CliArgs { allowedMcpServerNames: string[] | undefined; extensions: string[] | undefined; listExtensions: boolean | undefined; + ideMode: boolean | undefined; } export async function parseArguments(): Promise { @@ -175,6 +177,10 @@ export async function parseArguments(): Promise { type: 'boolean', description: 'List all available extensions and exit.', }) + .option('ide-mode', { + type: 'boolean', + description: 'Run in IDE mode?', + }) .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') @@ -230,6 +236,11 @@ export async function loadCliConfig( (v) => v === 'true' || v === '1', ); + const ideMode = + (argv.ideMode ?? settings.ideMode ?? false) && + process.env.TERM_PROGRAM === 'vscode' && + !process.env.SANDBOX; + const activeExtensions = filterActiveExtensions( extensions, argv.extensions || [], @@ -273,6 +284,24 @@ export async function loadCliConfig( } } + if (ideMode) { + mcpServers['_ide_server'] = new MCPServerConfig( + undefined, // command + undefined, // args + undefined, // env + undefined, // cwd + undefined, // url + 'http://localhost:3000/mcp', // httpUrl + undefined, // headers + undefined, // tcp + undefined, // timeout + false, // trust + 'IDE connection', // description + undefined, // includeTools + undefined, // excludeTools + ); + } + const sandboxConfig = await loadSandboxConfig(settings, argv); return new Config({ @@ -333,6 +362,7 @@ export async function loadCliConfig( version: e.config.version, })), noBrowser: !!process.env.NO_BROWSER, + ideMode, }); } diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index d9e6e4c4..50c07cf1 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -85,6 +85,7 @@ export interface Settings { maxSessionTurns?: number; // Add other settings here. + ideMode?: boolean; } export interface SettingsError { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index dc85c61a..7d982385 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -144,6 +144,7 @@ export interface ConfigParameters { listExtensions?: boolean; activeExtensions?: ActiveExtension[]; noBrowser?: boolean; + ideMode?: boolean; } export class Config { @@ -183,6 +184,7 @@ export class Config { private readonly model: string; private readonly extensionContextFilePaths: string[]; private readonly noBrowser: boolean; + private readonly ideMode: boolean; private modelSwitchedDuringSession: boolean = false; private readonly maxSessionTurns: number; private readonly listExtensions: boolean; @@ -234,6 +236,7 @@ export class Config { this.listExtensions = params.listExtensions ?? false; this._activeExtensions = params.activeExtensions ?? []; this.noBrowser = params.noBrowser ?? false; + this.ideMode = params.ideMode ?? false; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -498,6 +501,10 @@ export class Config { return this.noBrowser; } + getIdeMode(): boolean { + return this.ideMode; + } + async getGitService(): Promise { if (!this.gitService) { this.gitService = new GitService(this.targetDir);