summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/config/settings.ts3
-rw-r--r--packages/cli/src/ui/App.test.tsx8
-rw-r--r--packages/cli/src/ui/App.tsx45
-rw-r--r--packages/cli/src/ui/IdeIntegrationNudge.tsx70
4 files changed, 123 insertions, 3 deletions
diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
index bb8c87b8..93641ae0 100644
--- a/packages/cli/src/config/settings.ts
+++ b/packages/cli/src/config/settings.ts
@@ -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;
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index fc6dbb5a..a5c2a9c6 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -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(),
}));
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index f2dcc79e..2be681e5 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -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">
diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx
new file mode 100644
index 00000000..72cd1756
--- /dev/null
+++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx
@@ -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>
+ );
+}