Add hint to enable IDE integration for users running in VS Code (#5610)

This commit is contained in:
Shreya Keshive 2025-08-06 15:47:58 -04:00 committed by GitHub
parent 1fb680bacc
commit 024b8207eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 244 additions and 5 deletions

View File

@ -115,6 +115,9 @@ export interface Settings {
/// IDE mode setting configured via slash command toggle.
ideMode?: boolean;
// Setting to track if the user has seen the IDE integration nudge.
hasSeenIdeIntegrationNudge?: boolean;
// Setting for disabling auto-update.
disableAutoUpdate?: boolean;

View File

@ -16,6 +16,7 @@ import {
SandboxConfig,
GeminiClient,
ideContext,
type AuthType,
} from '@google/gemini-cli-core';
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
import process from 'node:process';
@ -84,6 +85,7 @@ interface MockServerConfig {
getAllGeminiMdFilenames: Mock<() => string[]>;
getGeminiClient: Mock<() => GeminiClient | undefined>;
getUserTier: Mock<() => Promise<string | undefined>>;
getIdeClient: Mock<() => { getCurrentIde: Mock<() => string | undefined> }>;
}
// Mock @google/gemini-cli-core and its Config class
@ -157,6 +159,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
getWorkspaceContext: vi.fn(() => ({
getDirectories: vi.fn(() => []),
})),
getIdeClient: vi.fn(() => ({
getCurrentIde: vi.fn(() => 'vscode'),
})),
};
});
@ -182,6 +187,7 @@ vi.mock('./hooks/useGeminiStream', () => ({
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
})),
}));
@ -233,7 +239,7 @@ vi.mock('./utils/updateCheck.js', () => ({
checkForUpdates: vi.fn(),
}));
vi.mock('./config/auth.js', () => ({
vi.mock('../config/auth.js', () => ({
validateAuthMethod: vi.fn(),
}));

View File

@ -39,7 +39,7 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
import { Colors } from './colors.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
import { LoadedSettings } from '../config/settings.js';
import { LoadedSettings, SettingScope } from '../config/settings.js';
import { Tips } from './components/Tips.js';
import { ConsolePatcher } from './utils/ConsolePatcher.js';
import { registerCleanup } from '../utils/cleanup.js';
@ -62,6 +62,10 @@ import {
type IdeContext,
ideContext,
} from '@google/gemini-cli-core';
import {
IdeIntegrationNudge,
IdeIntegrationNudgeResult,
} from './IdeIntegrationNudge.js';
import { validateAuthMethod } from '../config/auth.js';
import { useLogger } from './hooks/useLogger.js';
import { StreamingContext } from './contexts/StreamingContext.js';
@ -115,6 +119,15 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const nightly = version.includes('nightly');
const { history, addItem, clearItems, loadHistory } = useHistory();
const [idePromptAnswered, setIdePromptAnswered] = useState(false);
const currentIDE = config.getIdeClient().getCurrentIde();
const shouldShowIdePrompt =
config.getIdeModeFeature() &&
currentIDE &&
!config.getIdeMode() &&
!settings.merged.hasSeenIdeIntegrationNudge &&
!idePromptAnswered;
useEffect(() => {
const cleanup = setUpdateHandler(addItem, setUpdateInfo);
return cleanup;
@ -538,6 +551,27 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
[submitQuery],
);
const handleIdePromptComplete = useCallback(
(result: IdeIntegrationNudgeResult) => {
if (result === 'yes') {
handleSlashCommand('/ide install');
settings.setValue(
SettingScope.User,
'hasSeenIdeIntegrationNudge',
true,
);
} else if (result === 'dismiss') {
settings.setValue(
SettingScope.User,
'hasSeenIdeIntegrationNudge',
true,
);
}
setIdePromptAnswered(true);
},
[handleSlashCommand, settings],
);
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
pendingHistoryItems.push(...pendingGeminiHistoryItems);
@ -768,6 +802,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Box>
);
}
const mainAreaWidth = Math.floor(terminalWidth * 0.9);
const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5));
// Arbitrary threshold to ensure that items in the static area are large
@ -859,7 +894,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Box>
)}
{shellConfirmationRequest ? (
{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."
onComplete={handleIdePromptComplete}
/>
) : shellConfirmationRequest ? (
<ShellConfirmationDialog request={shellConfirmationRequest} />
) : isThemeDialogOpen ? (
<Box flexDirection="column">

View File

@ -0,0 +1,70 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text, useInput } from 'ink';
import {
RadioButtonSelect,
RadioSelectItem,
} from './components/shared/RadioButtonSelect.js';
export type IdeIntegrationNudgeResult = 'yes' | 'no' | 'dismiss';
interface IdeIntegrationNudgeProps {
question: string;
description?: string;
onComplete: (result: IdeIntegrationNudgeResult) => void;
}
export function IdeIntegrationNudge({
question,
description,
onComplete,
}: IdeIntegrationNudgeProps) {
useInput((_input, key) => {
if (key.escape) {
onComplete('no');
}
});
const OPTIONS: Array<RadioSelectItem<IdeIntegrationNudgeResult>> = [
{
label: 'Yes',
value: 'yes',
},
{
label: 'No (esc)',
value: 'no',
},
{
label: "No, don't ask again",
value: 'dismiss',
},
];
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor="yellow"
padding={1}
width="100%"
marginLeft={1}
>
<Box marginBottom={1} flexDirection="column">
<Text>
<Text color="yellow">{'> '}</Text>
{question}
</Text>
{description && <Text dimColor>{description}</Text>}
</Box>
<RadioButtonSelect
items={OPTIONS}
onSelect={onComplete}
isFocused={true}
/>
</Box>
);
}

View File

@ -39,12 +39,12 @@
"commands": [
{
"command": "gemini.diff.accept",
"title": "Gemini CLI: Accept Current Diff",
"title": "Gemini CLI: Accept Diff",
"icon": "$(check)"
},
{
"command": "gemini.diff.cancel",
"title": "Cancel",
"title": "Gemini CLI: Close Diff Editor",
"icon": "$(close)"
},
{

View File

@ -0,0 +1,99 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import { activate } from './extension.js';
vi.mock('vscode', () => ({
window: {
createOutputChannel: vi.fn(() => ({
appendLine: vi.fn(),
})),
showInformationMessage: vi.fn(),
createTerminal: vi.fn(() => ({
show: vi.fn(),
sendText: vi.fn(),
})),
},
workspace: {
workspaceFolders: [],
onDidCloseTextDocument: vi.fn(),
registerTextDocumentContentProvider: vi.fn(),
onDidChangeWorkspaceFolders: vi.fn(),
},
commands: {
registerCommand: vi.fn(),
executeCommand: vi.fn(),
},
Uri: {
joinPath: vi.fn(),
},
ExtensionMode: {
Development: 1,
Production: 2,
},
EventEmitter: vi.fn(() => ({
event: vi.fn(),
fire: vi.fn(),
dispose: vi.fn(),
})),
}));
describe('activate', () => {
let context: vscode.ExtensionContext;
beforeEach(() => {
context = {
subscriptions: [],
environmentVariableCollection: {
replace: vi.fn(),
},
globalState: {
get: vi.fn(),
update: vi.fn(),
},
extensionUri: {
fsPath: '/path/to/extension',
},
} as unknown as vscode.ExtensionContext;
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should show the info message on first activation', async () => {
const showInformationMessageMock = vi
.mocked(vscode.window.showInformationMessage)
.mockResolvedValue(undefined as never);
vi.mocked(context.globalState.get).mockReturnValue(undefined);
await activate(context);
expect(showInformationMessageMock).toHaveBeenCalledWith(
'Gemini CLI Companion extension successfully installed. Please restart your terminal to enable full IDE integration.',
'Re-launch Gemini CLI',
);
});
it('should not show the info message on subsequent activations', async () => {
vi.mocked(context.globalState.get).mockReturnValue(true);
await activate(context);
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled();
});
it('should launch the Gemini CLI when the user clicks the button', async () => {
const showInformationMessageMock = vi
.mocked(vscode.window.showInformationMessage)
.mockResolvedValue('Re-launch Gemini CLI' as never);
vi.mocked(context.globalState.get).mockReturnValue(undefined);
await activate(context);
expect(showInformationMessageMock).toHaveBeenCalled();
await new Promise(process.nextTick); // Wait for the promise to resolve
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
'gemini-cli.runGeminiCLI',
);
});
});

View File

@ -9,6 +9,7 @@ import { IDEServer } from './ide-server.js';
import { DiffContentProvider, DiffManager } from './diff-manager.js';
import { createLogger } from './utils/logger.js';
const INFO_MESSAGE_SHOWN_KEY = 'geminiCliInfoMessageShown';
const IDE_WORKSPACE_PATH_ENV_VAR = 'GEMINI_CLI_IDE_WORKSPACE_PATH';
export const DIFF_SCHEME = 'gemini-diff';
@ -81,6 +82,25 @@ export async function activate(context: vscode.ExtensionContext) {
log(`Failed to start IDE server: ${message}`);
}
if (!context.globalState.get(INFO_MESSAGE_SHOWN_KEY)) {
void vscode.window
.showInformationMessage(
'Gemini CLI Companion extension successfully installed. Please restart your terminal to enable full IDE integration.',
'Re-launch Gemini CLI',
)
.then(
(selection) => {
if (selection === 'Re-launch Gemini CLI') {
void vscode.commands.executeCommand('gemini-cli.runGeminiCLI');
}
},
(err) => {
log(`Failed to show information message: ${String(err)}`);
},
);
context.globalState.update(INFO_MESSAGE_SHOWN_KEY, true);
}
context.subscriptions.push(
vscode.workspace.onDidChangeWorkspaceFolders(() => {
updateWorkspacePath(context);