This commit is contained in:
Shreya Keshive
2026-01-27 17:15:27 -05:00
parent 89337d7d79
commit b55bf440f9
15 changed files with 324 additions and 672 deletions

View File

@@ -13,6 +13,8 @@ import {
StartSessionEvent,
logCliConfiguration,
startupProfiler,
type ConnectionConfig,
type IdeInfo,
} from '@google/gemini-cli-core';
import { type LoadedSettings } from '../config/settings.js';
import { performInitialAuth } from './auth.js';
@@ -23,6 +25,9 @@ export interface InitializationResult {
themeError: string | null;
shouldOpenAuthDialog: boolean;
geminiMdFileCount: number;
availableIdeConnections?: Array<
ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }
>;
}
/**
@@ -52,10 +57,30 @@ export async function initializeApp(
new StartSessionEvent(config, config.getToolRegistry()),
);
let availableIdeConnections:
| Array<ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }>
| undefined;
if (config.getIdeMode()) {
const ideClient = await IdeClient.getInstance();
await ideClient.connect();
logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
// Try to auto-connect if possible (legacy behavior or single match)
// We attempt to connect. If it requires selection, we get a list back?
// No, I need to implement the logic here.
const connections = await ideClient.discoverAvailableConnections();
// Heuristic: If we have a PID match, prioritize it.
// Ideally IdeClient.getInstance() already did some detection but didn't connect.
// Actually IdeClient.connect() (without args) tries to find "the one" config.
// If I want to support multiple, I should check here.
if (connections.length > 1) {
// Multiple connections found, let the UI handle selection
availableIdeConnections = connections;
} else {
// 0 or 1 connection, or let connect() handle the "best guess" fallback
await ideClient.connect();
logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
}
}
return {
@@ -63,5 +88,6 @@ export async function initializeApp(
themeError,
shouldOpenAuthDialog,
geminiMdFileCount: config.getGeminiMdFileCount(),
availableIdeConnections,
};
}

View File

@@ -65,7 +65,12 @@ import {
generateSummary,
type AgentsDiscoveredPayload,
ChangeAuthRequestedError,
IdeConnectionEvent,
IdeConnectionType,
logIdeConnection,
type ConnectionConfig,
} from '@google/gemini-cli-core';
import { IdeConnectionSelector } from './components/IdeConnectionSelector.js';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
import { useHistory } from './hooks/useHistoryManager.js';
@@ -750,7 +755,40 @@ Logging in with Google... Restarting Gemini CLI to continue.
dispatchExtensionStateUpdate,
addConfirmUpdateExtensionRequest,
setText: (text: string) => buffer.setText(text),
promptIdeConnection: async () => {
const ideClient = await IdeClient.getInstance();
await ideClient.disconnect();
const connections = await ideClient.discoverAvailableConnections();
if (connections.length > 1) {
setAvailableIdeConnections(connections);
} else if (connections.length === 1) {
await ideClient.connect();
logIdeConnection(
config,
new IdeConnectionEvent(IdeConnectionType.START),
);
// Show success message
historyManager.addItem(
{
type: MessageType.INFO,
text: `Connected to IDE: ${connections[0].ideInfo?.displayName || 'Unknown IDE'}`,
},
Date.now(),
);
} else {
// Show error message
historyManager.addItem(
{
type: MessageType.ERROR,
text: 'No IDE connections found.',
},
Date.now(),
);
}
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
setAuthState,
openThemeDialog,
@@ -1965,6 +2003,43 @@ Logging in with Google... Restarting Gemini CLI to continue.
],
);
// ... (existing imports)
// Inside AppContainer function:
// ... (existing state)
const [availableIdeConnections, setAvailableIdeConnections] = useState<
| Array<ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }>
| undefined
>(initializationResult.availableIdeConnections);
// ... (existing effects/hooks)
if (availableIdeConnections && availableIdeConnections.length > 0) {
return (
<IdeConnectionSelector
connections={availableIdeConnections}
onSelect={async (
conn: ConnectionConfig & {
workspacePath?: string;
ideInfo?: IdeInfo;
},
) => {
const ideClient = await IdeClient.getInstance();
await ideClient.connect({ connectionConfig: conn });
logIdeConnection(
config,
new IdeConnectionEvent(IdeConnectionType.START),
);
setAvailableIdeConnections(undefined);
}}
onCancel={() => {
setAvailableIdeConnections(undefined);
}}
/>
);
}
if (authState === AuthState.AwaitingGoogleLoginRestart) {
return (
<LoginWithGoogleRestartDialog

View File

@@ -136,20 +136,6 @@ async function setIdeModeAndSyncConnection(
export const ideCommand = async (): Promise<SlashCommand> => {
const ideClient = await IdeClient.getInstance();
const currentIDE = ideClient.getCurrentIde();
if (!currentIDE) {
return {
name: 'ide',
description: 'Manage IDE integration',
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: (): SlashCommandActionReturn =>
({
type: 'message',
messageType: 'error',
content: `IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: Antigravity, VS Code, or VS Code forks.`,
}) as const,
};
}
const ideSlashCommand: SlashCommand = {
name: 'ide',
@@ -181,6 +167,16 @@ export const ideCommand = async (): Promise<SlashCommand> => {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
if (!currentIDE) {
context.ui.addItem(
{
type: 'error',
text: 'No IDE detected. Cannot run installer.',
},
Date.now(),
);
return;
}
const installer = getIdeInstaller(currentIDE);
if (!installer) {
context.ui.addItem(
@@ -297,15 +293,30 @@ export const ideCommand = async (): Promise<SlashCommand> => {
},
};
const switchCommand: SlashCommand = {
name: 'switch',
description: 'Switch to a different IDE connection',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
await context.ui.promptIdeConnection();
},
};
const { status } = ideClient.getConnectionStatus();
const isConnected = status === IDEConnectionStatus.Connected;
if (isConnected) {
ideSlashCommand.subCommands = [statusCommand, disableCommand];
ideSlashCommand.subCommands = [
statusCommand,
switchCommand,
disableCommand,
];
} else {
ideSlashCommand.subCommands = [
enableCommand,
statusCommand,
switchCommand,
installCommand,
];
}

View File

@@ -83,7 +83,9 @@ export interface CommandContext {
extensionsUpdateState: Map<string, ExtensionUpdateStatus>;
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
addConfirmUpdateExtensionRequest: (value: ConfirmationRequest) => void;
removeComponent: () => void;
promptIdeConnection: () => Promise<void>;
};
// Session-specific data
session: {

View File

@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text } from 'ink';
import { type ConnectionConfig, type IdeInfo } from '@google/gemini-cli-core';
import {
RadioButtonSelect,
type RadioSelectItem,
} from './shared/RadioButtonSelect.js';
interface IdeConnectionSelectorProps {
connections: Array<
ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }
>;
onSelect: (
connection: ConnectionConfig & {
workspacePath?: string;
ideInfo?: IdeInfo;
},
) => void;
onCancel: () => void;
}
export const IdeConnectionSelector = ({
connections,
onSelect,
onCancel,
}: IdeConnectionSelectorProps) => {
const items: Array<RadioSelectItem<number>> = connections.map(
(conn, index) => {
const label = `${conn.ideInfo?.displayName || 'Unknown IDE'} (${conn.workspacePath || 'No workspace'})`;
return {
label,
value: index,
key: index.toString(),
};
},
);
// Add an option to skip/cancel
items.push({
label: 'Do not connect to an IDE',
value: -1,
key: 'cancel',
});
return (
<Box
flexDirection="column"
padding={1}
borderStyle="round"
borderColor="cyan"
>
<Text bold color="cyan">
Multiple IDE connections found. Please select one:
</Text>
<Box marginTop={1}>
<RadioButtonSelect
items={items}
onSelect={(value: number) => {
if (value === -1) {
onCancel();
} else {
onSelect(connections[value]);
}
}}
/>
</Box>
</Box>
);
};

View File

@@ -213,7 +213,9 @@ describe('useSlashCommandProcessor', () => {
toggleDebugProfiler: vi.fn(),
dispatchExtensionStateUpdate: vi.fn(),
addConfirmUpdateExtensionRequest: vi.fn(),
setText: vi.fn(),
promptIdeConnection: vi.fn(),
},
new Map(), // extensionsUpdateState
true, // isConfigInitialized

View File

@@ -83,6 +83,7 @@ interface SlashCommandProcessorActions {
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void;
setText: (text: string) => void;
promptIdeConnection: () => Promise<void>;
}
/**
@@ -237,6 +238,7 @@ export const useSlashCommandProcessor = (
addConfirmUpdateExtensionRequest:
actions.addConfirmUpdateExtensionRequest,
removeComponent: () => setCustomDialog(null),
promptIdeConnection: actions.promptIdeConnection,
},
session: {
stats: session.stats,

View File

@@ -29,5 +29,6 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {},
addConfirmUpdateExtensionRequest: (_request) => {},
removeComponent: () => {},
promptIdeConnection: async () => {},
};
}