summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
authorchristine betts <[email protected]>2025-07-30 22:36:24 +0000
committerGitHub <[email protected]>2025-07-30 22:36:24 +0000
commit325bb8913776c60b763ee5f66375a4ca90d22ce0 (patch)
treeeb2aaf9fa1f826e9cfeff3e1507457aeb61d8fdf /packages/cli/src
parentac1bb5ee4275e508dfc2256bbd5ca012e4a4f469 (diff)
Add toggleable IDE mode setting (#5146)
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/config/config.test.ts10
-rw-r--r--packages/cli/src/config/config.ts14
-rw-r--r--packages/cli/src/config/settings.ts4
-rw-r--r--packages/cli/src/ui/App.test.tsx1
-rw-r--r--packages/cli/src/ui/App.tsx7
-rw-r--r--packages/cli/src/ui/commands/ideCommand.test.ts52
-rw-r--r--packages/cli/src/ui/commands/ideCommand.ts183
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.test.ts1
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts4
9 files changed, 176 insertions, 100 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 1dd09f4b..d87d0c8f 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -916,7 +916,7 @@ describe('loadCliConfig extensions', () => {
});
});
-describe('loadCliConfig ideMode', () => {
+describe('loadCliConfig ideModeFeature', () => {
const originalArgv = process.argv;
const originalEnv = { ...process.env };
@@ -939,16 +939,16 @@ describe('loadCliConfig ideMode', () => {
const settings: Settings = {};
const argv = await parseArguments();
const config = await loadCliConfig(settings, [], 'test-session', argv);
- expect(config.getIdeMode()).toBe(false);
+ expect(config.getIdeModeFeature()).toBe(false);
});
- it('should be false when settings.ideMode is true, but SANDBOX is set', async () => {
+ it('should be false when settings.ideModeFeature is true, 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 settings: Settings = { ideModeFeature: true };
const config = await loadCliConfig(settings, [], 'test-session', argv);
- expect(config.getIdeMode()).toBe(false);
+ expect(config.getIdeModeFeature()).toBe(false);
});
});
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 1cc78888..d650a9af 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -59,7 +59,7 @@ export interface CliArgs {
experimentalAcp: boolean | undefined;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
- ideMode: boolean | undefined;
+ ideModeFeature: boolean | undefined;
proxy: string | undefined;
includeDirectories: string[] | undefined;
}
@@ -191,7 +191,7 @@ export async function parseArguments(): Promise<CliArgs> {
type: 'boolean',
description: 'List all available extensions and exit.',
})
- .option('ide-mode', {
+ .option('ide-mode-feature', {
type: 'boolean',
description: 'Run in IDE mode?',
})
@@ -268,10 +268,13 @@ export async function loadCliConfig(
(v) => v === 'true' || v === '1',
);
- const ideMode =
- (argv.ideMode ?? settings.ideMode ?? false) && !process.env.SANDBOX;
+ const ideMode = settings.ideMode ?? false;
- const ideClient = IdeClient.getInstance(ideMode);
+ const ideModeFeature =
+ (argv.ideModeFeature ?? settings.ideModeFeature ?? false) &&
+ !process.env.SANDBOX;
+
+ const ideClient = IdeClient.getInstance(ideMode && ideModeFeature);
const allExtensions = annotateActiveExtensions(
extensions,
@@ -429,6 +432,7 @@ export async function loadCliConfig(
noBrowser: !!process.env.NO_BROWSER,
summarizeToolOutput: settings.summarizeToolOutput,
ideMode,
+ ideModeFeature,
ideClient,
});
}
diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
index 17c1d0d5..5d1b1aaf 100644
--- a/packages/cli/src/config/settings.ts
+++ b/packages/cli/src/config/settings.ts
@@ -99,7 +99,9 @@ export interface Settings {
vimMode?: boolean;
- // Add other settings here.
+ // Flag to be removed post-launch.
+ ideModeFeature?: boolean;
+ /// IDE mode setting configured via slash command toggle.
ideMode?: boolean;
// Setting for disabling auto-update.
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index 13ddb77d..79b9ce86 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -151,6 +151,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
setFlashFallbackHandler: vi.fn(),
getSessionId: vi.fn(() => 'test-session-id'),
getUserTier: vi.fn().mockResolvedValue(undefined),
+ getIdeModeFeature: vi.fn(() => false),
getIdeMode: vi.fn(() => false),
getWorkspaceContext: vi.fn(() => ({
getDirectories: vi.fn(() => []),
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 2e899cc1..db9e5be4 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -573,7 +573,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (Object.keys(mcpServers || {}).length > 0) {
handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc');
}
- } else if (key.ctrl && input === 'e' && ideContextState) {
+ } else if (
+ key.ctrl &&
+ input === 'e' &&
+ config.getIdeMode() &&
+ ideContextState
+ ) {
setShowIDEContextDetail((prev) => !prev);
} else if (key.ctrl && (input === 'c' || input === 'C')) {
handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef);
diff --git a/packages/cli/src/ui/commands/ideCommand.test.ts b/packages/cli/src/ui/commands/ideCommand.test.ts
index 3c73f52c..3c73549c 100644
--- a/packages/cli/src/ui/commands/ideCommand.test.ts
+++ b/packages/cli/src/ui/commands/ideCommand.test.ts
@@ -32,11 +32,19 @@ describe('ideCommand', () => {
ui: {
addItem: vi.fn(),
},
+ services: {
+ settings: {
+ setValue: vi.fn(),
+ },
+ },
} as unknown as CommandContext;
mockConfig = {
+ getIdeModeFeature: vi.fn(),
getIdeMode: vi.fn(),
getIdeClient: vi.fn(),
+ setIdeMode: vi.fn(),
+ setIdeClientDisconnected: vi.fn(),
} as unknown as Config;
platformSpy = vi.spyOn(process, 'platform', 'get');
@@ -46,13 +54,14 @@ describe('ideCommand', () => {
vi.restoreAllMocks();
});
- it('should return null if ideMode is not enabled', () => {
- vi.mocked(mockConfig.getIdeMode).mockReturnValue(false);
+ it('should return null if ideModeFeature is not enabled', () => {
+ vi.mocked(mockConfig.getIdeModeFeature).mockReturnValue(false);
const command = ideCommand(mockConfig);
expect(command).toBeNull();
});
- it('should return the ide command if ideMode is enabled', () => {
+ it('should return the ide command if ideModeFeature is enabled', () => {
+ vi.mocked(mockConfig.getIdeModeFeature).mockReturnValue(true);
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getCurrentIde: () => DetectedIde.VSCode,
@@ -60,19 +69,20 @@ describe('ideCommand', () => {
const command = ideCommand(mockConfig);
expect(command).not.toBeNull();
expect(command?.name).toBe('ide');
- expect(command?.subCommands).toHaveLength(2);
- expect(command?.subCommands?.[0].name).toBe('status');
- expect(command?.subCommands?.[1].name).toBe('install');
+ expect(command?.subCommands).toHaveLength(3);
+ expect(command?.subCommands?.[0].name).toBe('disable');
+ expect(command?.subCommands?.[1].name).toBe('status');
+ expect(command?.subCommands?.[2].name).toBe('install');
});
describe('status subcommand', () => {
const mockGetConnectionStatus = vi.fn();
beforeEach(() => {
- vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
+ vi.mocked(mockConfig.getIdeModeFeature).mockReturnValue(true);
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getConnectionStatus: mockGetConnectionStatus,
getCurrentIde: () => DetectedIde.VSCode,
- } as ReturnType<Config['getIdeClient']>);
+ } as unknown as ReturnType<Config['getIdeClient']>);
});
it('should show connected status', () => {
@@ -80,7 +90,8 @@ describe('ideCommand', () => {
status: core.IDEConnectionStatus.Connected,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands![0].action!(mockContext, '');
+ const result = command!.subCommands!.find((c) => c.name === 'status')!
+ .action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -94,7 +105,8 @@ describe('ideCommand', () => {
status: core.IDEConnectionStatus.Connecting,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands![0].action!(mockContext, '');
+ const result = command!.subCommands!.find((c) => c.name === 'status')!
+ .action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -107,7 +119,8 @@ describe('ideCommand', () => {
status: core.IDEConnectionStatus.Disconnected,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands![0].action!(mockContext, '');
+ const result = command!.subCommands!.find((c) => c.name === 'status')!
+ .action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -123,7 +136,8 @@ describe('ideCommand', () => {
details,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands![0].action!(mockContext, '');
+ const result = command!.subCommands!.find((c) => c.name === 'status')!
+ .action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -136,10 +150,12 @@ describe('ideCommand', () => {
describe('install subcommand', () => {
const mockInstall = vi.fn();
beforeEach(() => {
+ vi.mocked(mockConfig.getIdeModeFeature).mockReturnValue(true);
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getCurrentIde: () => DetectedIde.VSCode,
- } as ReturnType<Config['getIdeClient']>);
+ getConnectionStatus: vi.fn(),
+ } as unknown as ReturnType<Config['getIdeClient']>);
vi.mocked(core.getIdeInstaller).mockReturnValue({
install: mockInstall,
isInstalled: vi.fn(),
@@ -154,7 +170,10 @@ describe('ideCommand', () => {
});
const command = ideCommand(mockConfig);
- await command!.subCommands![1].action!(mockContext, '');
+ await command!.subCommands!.find((c) => c.name === 'install')!.action!(
+ mockContext,
+ '',
+ );
expect(core.getIdeInstaller).toHaveBeenCalledWith('vscode');
expect(mockInstall).toHaveBeenCalled();
@@ -181,7 +200,10 @@ describe('ideCommand', () => {
});
const command = ideCommand(mockConfig);
- await command!.subCommands![1].action!(mockContext, '');
+ await command!.subCommands!.find((c) => c.name === 'install')!.action!(
+ mockContext,
+ '',
+ );
expect(core.getIdeInstaller).toHaveBeenCalledWith('vscode');
expect(mockInstall).toHaveBeenCalled();
diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts
index 26b0f57d..1da7d6b0 100644
--- a/packages/cli/src/ui/commands/ideCommand.ts
+++ b/packages/cli/src/ui/commands/ideCommand.ts
@@ -6,9 +6,9 @@
import {
Config,
+ IDEConnectionStatus,
getIdeDisplayName,
getIdeInstaller,
- IDEConnectionStatus,
} from '@google/gemini-cli-core';
import {
CommandContext,
@@ -16,91 +16,130 @@ import {
SlashCommandActionReturn,
CommandKind,
} from './types.js';
+import { SettingScope } from '../../config/settings.js';
export const ideCommand = (config: Config | null): SlashCommand | null => {
- if (!config?.getIdeMode()) {
+ if (!config?.getIdeModeFeature()) {
return null;
}
const currentIDE = config.getIdeClient().getCurrentIde();
if (!currentIDE) {
- throw new Error(
- 'IDE slash command should not be available if not running in an IDE',
- );
+ return null;
}
- return {
+ const ideSlashCommand: SlashCommand = {
name: 'ide',
description: 'manage IDE integration',
kind: CommandKind.BUILT_IN,
- subCommands: [
- {
- name: 'status',
- description: 'check status of IDE integration',
- kind: CommandKind.BUILT_IN,
- action: (_context: CommandContext): SlashCommandActionReturn => {
- const connection = config.getIdeClient().getConnectionStatus();
- switch (connection?.status) {
- case IDEConnectionStatus.Connected:
- return {
- type: 'message',
- messageType: 'info',
- content: `🟢 Connected`,
- } as const;
- case IDEConnectionStatus.Connecting:
- return {
- type: 'message',
- messageType: 'info',
- content: `🟡 Connecting...`,
- } as const;
- default: {
- let content = `🔴 Disconnected`;
- if (connection?.details) {
- content += `: ${connection.details}`;
- }
- return {
- type: 'message',
- messageType: 'error',
- content,
- } as const;
- }
- }
- },
- },
- {
- name: 'install',
- description: `install required IDE companion ${getIdeDisplayName(currentIDE)} extension `,
- kind: CommandKind.BUILT_IN,
- action: async (context) => {
- const installer = getIdeInstaller(currentIDE);
- if (!installer) {
- context.ui.addItem(
- {
- type: 'error',
- text: 'No installer available for your configured IDE.',
- },
- Date.now(),
- );
- return;
+ subCommands: [],
+ };
+
+ const statusCommand: SlashCommand = {
+ name: 'status',
+ description: 'check status of IDE integration',
+ kind: CommandKind.BUILT_IN,
+ action: (_context: CommandContext): SlashCommandActionReturn => {
+ const connection = config.getIdeClient().getConnectionStatus();
+ switch (connection?.status) {
+ case IDEConnectionStatus.Connected:
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: `🟢 Connected`,
+ } as const;
+ case IDEConnectionStatus.Connecting:
+ return {
+ type: 'message',
+ messageType: 'info',
+ content: `🟡 Connecting...`,
+ } as const;
+ default: {
+ let content = `🔴 Disconnected`;
+ if (connection?.details) {
+ content += `: ${connection.details}`;
}
+ return {
+ type: 'message',
+ messageType: 'error',
+ content,
+ } as const;
+ }
+ }
+ },
+ };
- context.ui.addItem(
- {
- type: 'info',
- text: `Installing IDE companion extension...`,
- },
- Date.now(),
- );
+ const installCommand: SlashCommand = {
+ name: 'install',
+ description: `install required IDE companion ${getIdeDisplayName(currentIDE)} extension `,
+ kind: CommandKind.BUILT_IN,
+ action: async (context) => {
+ const installer = getIdeInstaller(currentIDE);
+ if (!installer) {
+ context.ui.addItem(
+ {
+ type: 'error',
+ text: 'No installer available for your configured IDE.',
+ },
+ Date.now(),
+ );
+ return;
+ }
- const result = await installer.install();
- context.ui.addItem(
- {
- type: result.success ? 'info' : 'error',
- text: result.message,
- },
- Date.now(),
- );
+ context.ui.addItem(
+ {
+ type: 'info',
+ text: `Installing IDE companion extension...`,
},
- },
- ],
+ Date.now(),
+ );
+
+ const result = await installer.install();
+ context.ui.addItem(
+ {
+ type: result.success ? 'info' : 'error',
+ text: result.message,
+ },
+ Date.now(),
+ );
+ },
+ };
+
+ const enableCommand: SlashCommand = {
+ name: 'enable',
+ description: 'enable IDE integration',
+ kind: CommandKind.BUILT_IN,
+ action: async (context: CommandContext) => {
+ context.services.settings.setValue(SettingScope.User, 'ideMode', true);
+ config.setIdeMode(true);
+ config.setIdeClientConnected();
+ },
+ };
+
+ const disableCommand: SlashCommand = {
+ name: 'disable',
+ description: 'disable IDE integration',
+ kind: CommandKind.BUILT_IN,
+ action: async (context: CommandContext) => {
+ context.services.settings.setValue(SettingScope.User, 'ideMode', false);
+ config.setIdeMode(false);
+ config.setIdeClientDisconnected();
+ },
};
+
+ const ideModeEnabled = config.getIdeMode();
+ if (ideModeEnabled) {
+ ideSlashCommand.subCommands = [
+ disableCommand,
+ statusCommand,
+ installCommand,
+ ];
+ } else {
+ ideSlashCommand.subCommands = [
+ enableCommand,
+ statusCommand,
+ installCommand,
+ ];
+ }
+
+ return ideSlashCommand;
};
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
index 2dc206d7..d9fe8530 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts
@@ -101,6 +101,7 @@ describe('useSlashCommandProcessor', () => {
setHistory: vi.fn().mockResolvedValue(undefined),
})),
getExtensions: vi.fn(() => []),
+ getIdeMode: vi.fn(() => false),
} as unknown as Config;
const mockSettings = {} as LoadedSettings;
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index e315ba97..a2a1837d 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -185,6 +185,8 @@ export const useSlashCommandProcessor = (
],
);
+ const ideMode = config?.getIdeMode();
+
useEffect(() => {
const controller = new AbortController();
const load = async () => {
@@ -205,7 +207,7 @@ export const useSlashCommandProcessor = (
return () => {
controller.abort();
};
- }, [config]);
+ }, [config, ideMode]);
const handleSlashCommand = useCallback(
async (