[ide-mode] Create an IDE manager class to handle connecting to and exposing methods from the IDE server (#4797)

This commit is contained in:
christine betts
2025-07-25 17:46:55 +00:00
committed by GitHub
parent 3c16429fc4
commit 1b8ba5ca6b
14 changed files with 178 additions and 256 deletions

View File

@@ -19,25 +19,10 @@ import { type Config } from '@google/gemini-cli-core';
import * as child_process from 'child_process';
import { glob } from 'glob';
import {
getMCPDiscoveryState,
getMCPServerStatus,
IDE_SERVER_NAME,
MCPDiscoveryState,
MCPServerStatus,
} from '@google/gemini-cli-core';
import { IDEConnectionStatus } from '@google/gemini-cli-core/index.js';
vi.mock('child_process');
vi.mock('glob');
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...original,
getMCPServerStatus: vi.fn(),
getMCPDiscoveryState: vi.fn(),
};
});
function regexEscape(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -49,8 +34,6 @@ describe('ideCommand', () => {
let execSyncSpy: MockInstance;
let globSyncSpy: MockInstance;
let platformSpy: MockInstance;
let getMCPServerStatusSpy: MockInstance;
let getMCPDiscoveryStateSpy: MockInstance;
beforeEach(() => {
mockContext = {
@@ -61,13 +44,12 @@ describe('ideCommand', () => {
mockConfig = {
getIdeMode: vi.fn(),
getIdeClient: vi.fn(),
} as unknown as Config;
execSyncSpy = vi.spyOn(child_process, 'execSync');
globSyncSpy = vi.spyOn(glob, 'sync');
platformSpy = vi.spyOn(process, 'platform', 'get');
getMCPServerStatusSpy = vi.mocked(getMCPServerStatus);
getMCPDiscoveryStateSpy = vi.mocked(getMCPDiscoveryState);
});
afterEach(() => {
@@ -91,15 +73,21 @@ describe('ideCommand', () => {
});
describe('status subcommand', () => {
const mockGetConnectionStatus = vi.fn();
beforeEach(() => {
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
getConnectionStatus: mockGetConnectionStatus,
} as ReturnType<Config['getIdeClient']>);
});
it('should show connected status', () => {
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.CONNECTED);
mockGetConnectionStatus.mockReturnValue({
status: IDEConnectionStatus.Connected,
});
const command = ideCommand(mockConfig);
const result = command!.subCommands![0].action!(mockContext, '');
expect(getMCPServerStatusSpy).toHaveBeenCalledWith(IDE_SERVER_NAME);
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -108,37 +96,45 @@ describe('ideCommand', () => {
});
it('should show connecting status', () => {
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.CONNECTING);
mockGetConnectionStatus.mockReturnValue({
status: IDEConnectionStatus.Connecting,
});
const command = ideCommand(mockConfig);
const result = command!.subCommands![0].action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: '🔄 Initializing...',
content: `🟡 Connecting...`,
});
});
it('should show discovery in progress status', () => {
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.DISCONNECTED);
getMCPDiscoveryStateSpy.mockReturnValue(MCPDiscoveryState.IN_PROGRESS);
const command = ideCommand(mockConfig);
const result = command!.subCommands![0].action!(mockContext, '');
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: '🔄 Initializing...',
});
});
it('should show disconnected status', () => {
getMCPServerStatusSpy.mockReturnValue(MCPServerStatus.DISCONNECTED);
getMCPDiscoveryStateSpy.mockReturnValue(MCPDiscoveryState.COMPLETED);
mockGetConnectionStatus.mockReturnValue({
status: IDEConnectionStatus.Disconnected,
});
const command = ideCommand(mockConfig);
const result = command!.subCommands![0].action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: '🔴 Disconnected',
content: `🔴 Disconnected`,
});
});
it('should show disconnected status with details', () => {
const details = 'Something went wrong';
mockGetConnectionStatus.mockReturnValue({
status: IDEConnectionStatus.Disconnected,
details,
});
const command = ideCommand(mockConfig);
const result = command!.subCommands![0].action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: `🔴 Disconnected: ${details}`,
});
});
});

View File

@@ -5,14 +5,7 @@
*/
import { fileURLToPath } from 'url';
import {
Config,
getMCPDiscoveryState,
getMCPServerStatus,
IDE_SERVER_NAME,
MCPDiscoveryState,
MCPServerStatus,
} from '@google/gemini-cli-core';
import { Config, IDEConnectionStatus } from '@google/gemini-cli-core';
import {
CommandContext,
SlashCommand,
@@ -56,36 +49,31 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
description: 'check status of IDE integration',
kind: CommandKind.BUILT_IN,
action: (_context: CommandContext): SlashCommandActionReturn => {
const status = getMCPServerStatus(IDE_SERVER_NAME);
const discoveryState = getMCPDiscoveryState();
switch (status) {
case MCPServerStatus.CONNECTED:
const connection = config.getIdeClient()?.getConnectionStatus();
switch (connection?.status) {
case IDEConnectionStatus.Connected:
return {
type: 'message',
messageType: 'info',
content: `🟢 Connected`,
};
case MCPServerStatus.CONNECTING:
} as const;
case IDEConnectionStatus.Connecting:
return {
type: 'message',
messageType: 'info',
content: `🔄 Initializing...`,
};
case MCPServerStatus.DISCONNECTED:
default:
if (discoveryState === MCPDiscoveryState.IN_PROGRESS) {
return {
type: 'message',
messageType: 'info',
content: `🔄 Initializing...`,
};
} else {
return {
type: 'message',
messageType: 'error',
content: `🔴 Disconnected`,
};
content: `🟡 Connecting...`,
} as const;
default: {
let content = `🔴 Disconnected`;
if (connection?.details) {
content += `: ${connection.details}`;
}
return {
type: 'message',
messageType: 'error',
content,
} as const;
}
}
},
},