Add hint to enable IDE integration for users running in VS Code (#5610)
This commit is contained in:
parent
1fb680bacc
commit
024b8207eb
|
@ -115,6 +115,9 @@ export interface Settings {
|
||||||
/// IDE mode setting configured via slash command toggle.
|
/// IDE mode setting configured via slash command toggle.
|
||||||
ideMode?: boolean;
|
ideMode?: boolean;
|
||||||
|
|
||||||
|
// Setting to track if the user has seen the IDE integration nudge.
|
||||||
|
hasSeenIdeIntegrationNudge?: boolean;
|
||||||
|
|
||||||
// Setting for disabling auto-update.
|
// Setting for disabling auto-update.
|
||||||
disableAutoUpdate?: boolean;
|
disableAutoUpdate?: boolean;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
SandboxConfig,
|
SandboxConfig,
|
||||||
GeminiClient,
|
GeminiClient,
|
||||||
ideContext,
|
ideContext,
|
||||||
|
type AuthType,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
@ -84,6 +85,7 @@ interface MockServerConfig {
|
||||||
getAllGeminiMdFilenames: Mock<() => string[]>;
|
getAllGeminiMdFilenames: Mock<() => string[]>;
|
||||||
getGeminiClient: Mock<() => GeminiClient | undefined>;
|
getGeminiClient: Mock<() => GeminiClient | undefined>;
|
||||||
getUserTier: Mock<() => Promise<string | undefined>>;
|
getUserTier: Mock<() => Promise<string | undefined>>;
|
||||||
|
getIdeClient: Mock<() => { getCurrentIde: Mock<() => string | undefined> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock @google/gemini-cli-core and its Config class
|
// Mock @google/gemini-cli-core and its Config class
|
||||||
|
@ -157,6 +159,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
getWorkspaceContext: vi.fn(() => ({
|
getWorkspaceContext: vi.fn(() => ({
|
||||||
getDirectories: vi.fn(() => []),
|
getDirectories: vi.fn(() => []),
|
||||||
})),
|
})),
|
||||||
|
getIdeClient: vi.fn(() => ({
|
||||||
|
getCurrentIde: vi.fn(() => 'vscode'),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -182,6 +187,7 @@ vi.mock('./hooks/useGeminiStream', () => ({
|
||||||
submitQuery: vi.fn(),
|
submitQuery: vi.fn(),
|
||||||
initError: null,
|
initError: null,
|
||||||
pendingHistoryItems: [],
|
pendingHistoryItems: [],
|
||||||
|
thought: null,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -233,7 +239,7 @@ vi.mock('./utils/updateCheck.js', () => ({
|
||||||
checkForUpdates: vi.fn(),
|
checkForUpdates: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./config/auth.js', () => ({
|
vi.mock('../config/auth.js', () => ({
|
||||||
validateAuthMethod: vi.fn(),
|
validateAuthMethod: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
||||||
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
||||||
import { Colors } from './colors.js';
|
import { Colors } from './colors.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../config/config.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 { Tips } from './components/Tips.js';
|
||||||
import { ConsolePatcher } from './utils/ConsolePatcher.js';
|
import { ConsolePatcher } from './utils/ConsolePatcher.js';
|
||||||
import { registerCleanup } from '../utils/cleanup.js';
|
import { registerCleanup } from '../utils/cleanup.js';
|
||||||
|
@ -62,6 +62,10 @@ import {
|
||||||
type IdeContext,
|
type IdeContext,
|
||||||
ideContext,
|
ideContext,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
import {
|
||||||
|
IdeIntegrationNudge,
|
||||||
|
IdeIntegrationNudgeResult,
|
||||||
|
} from './IdeIntegrationNudge.js';
|
||||||
import { validateAuthMethod } from '../config/auth.js';
|
import { validateAuthMethod } from '../config/auth.js';
|
||||||
import { useLogger } from './hooks/useLogger.js';
|
import { useLogger } from './hooks/useLogger.js';
|
||||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||||
|
@ -115,6 +119,15 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
const nightly = version.includes('nightly');
|
const nightly = version.includes('nightly');
|
||||||
const { history, addItem, clearItems, loadHistory } = useHistory();
|
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(() => {
|
useEffect(() => {
|
||||||
const cleanup = setUpdateHandler(addItem, setUpdateInfo);
|
const cleanup = setUpdateHandler(addItem, setUpdateInfo);
|
||||||
return cleanup;
|
return cleanup;
|
||||||
|
@ -538,6 +551,27 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
[submitQuery],
|
[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 { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||||
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
|
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
|
||||||
pendingHistoryItems.push(...pendingGeminiHistoryItems);
|
pendingHistoryItems.push(...pendingGeminiHistoryItems);
|
||||||
|
@ -768,6 +802,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainAreaWidth = Math.floor(terminalWidth * 0.9);
|
const mainAreaWidth = Math.floor(terminalWidth * 0.9);
|
||||||
const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5));
|
const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5));
|
||||||
// Arbitrary threshold to ensure that items in the static area are large
|
// Arbitrary threshold to ensure that items in the static area are large
|
||||||
|
@ -859,7 +894,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
</Box>
|
</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} />
|
<ShellConfirmationDialog request={shellConfirmationRequest} />
|
||||||
) : isThemeDialogOpen ? (
|
) : isThemeDialogOpen ? (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -39,12 +39,12 @@
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"command": "gemini.diff.accept",
|
"command": "gemini.diff.accept",
|
||||||
"title": "Gemini CLI: Accept Current Diff",
|
"title": "Gemini CLI: Accept Diff",
|
||||||
"icon": "$(check)"
|
"icon": "$(check)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "gemini.diff.cancel",
|
"command": "gemini.diff.cancel",
|
||||||
"title": "Cancel",
|
"title": "Gemini CLI: Close Diff Editor",
|
||||||
"icon": "$(close)"
|
"icon": "$(close)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,6 +9,7 @@ import { IDEServer } from './ide-server.js';
|
||||||
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
import { DiffContentProvider, DiffManager } from './diff-manager.js';
|
||||||
import { createLogger } from './utils/logger.js';
|
import { createLogger } from './utils/logger.js';
|
||||||
|
|
||||||
|
const INFO_MESSAGE_SHOWN_KEY = 'geminiCliInfoMessageShown';
|
||||||
const IDE_WORKSPACE_PATH_ENV_VAR = 'GEMINI_CLI_IDE_WORKSPACE_PATH';
|
const IDE_WORKSPACE_PATH_ENV_VAR = 'GEMINI_CLI_IDE_WORKSPACE_PATH';
|
||||||
export const DIFF_SCHEME = 'gemini-diff';
|
export const DIFF_SCHEME = 'gemini-diff';
|
||||||
|
|
||||||
|
@ -81,6 +82,25 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||||
log(`Failed to start IDE server: ${message}`);
|
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(
|
context.subscriptions.push(
|
||||||
vscode.workspace.onDidChangeWorkspaceFolders(() => {
|
vscode.workspace.onDidChangeWorkspaceFolders(() => {
|
||||||
updateWorkspacePath(context);
|
updateWorkspacePath(context);
|
||||||
|
|
Loading…
Reference in New Issue