[ide-mode] Use active files and selected text in user prompt (#4614)

This commit is contained in:
christine betts 2025-07-21 20:52:02 +00:00 committed by GitHub
parent d7a57d85a3
commit 1969d805f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 194 additions and 113 deletions

View File

@ -151,8 +151,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
}); });
const ideContextMock = { const ideContextMock = {
getActiveFileContext: vi.fn(), getOpenFilesContext: vi.fn(),
subscribeToActiveFile: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function subscribeToOpenFiles: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function
}; };
return { return {
@ -267,7 +267,7 @@ describe('App UI', () => {
// Ensure a theme is set so the theme dialog does not appear. // Ensure a theme is set so the theme dialog does not appear.
mockSettings = createMockSettings({ workspace: { theme: 'Default' } }); mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
vi.mocked(ideContext.getActiveFileContext).mockReturnValue(undefined); vi.mocked(ideContext.getOpenFilesContext).mockReturnValue(undefined);
}); });
afterEach(() => { afterEach(() => {
@ -279,10 +279,9 @@ describe('App UI', () => {
}); });
it('should display active file when available', async () => { it('should display active file when available', async () => {
vi.mocked(ideContext.getActiveFileContext).mockReturnValue({ vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
filePath: '/path/to/my-file.ts', activeFile: '/path/to/my-file.ts',
content: 'const a = 1;', selectedText: 'hello',
cursor: 0,
}); });
const { lastFrame, unmount } = render( const { lastFrame, unmount } = render(
@ -298,10 +297,8 @@ describe('App UI', () => {
}); });
it('should not display active file when not available', async () => { it('should not display active file when not available', async () => {
vi.mocked(ideContext.getActiveFileContext).mockReturnValue({ vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
filePath: '', activeFile: '',
content: '',
cursor: 0,
}); });
const { lastFrame, unmount } = render( const { lastFrame, unmount } = render(
@ -317,10 +314,9 @@ describe('App UI', () => {
}); });
it('should display active file and other context', async () => { it('should display active file and other context', async () => {
vi.mocked(ideContext.getActiveFileContext).mockReturnValue({ vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
filePath: '/path/to/my-file.ts', activeFile: '/path/to/my-file.ts',
content: 'const a = 1;', selectedText: 'hello',
cursor: 0,
}); });
mockConfig.getGeminiMdFileCount.mockReturnValue(1); mockConfig.getGeminiMdFileCount.mockReturnValue(1);

View File

@ -58,7 +58,7 @@ import {
FlashFallbackEvent, FlashFallbackEvent,
logFlashFallback, logFlashFallback,
AuthType, AuthType,
type ActiveFile, type OpenFiles,
ideContext, ideContext,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js'; import { validateAuthMethod } from '../config/auth.js';
@ -160,12 +160,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] = const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
useState<boolean>(false); useState<boolean>(false);
const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined); const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
const [activeFile, setActiveFile] = useState<ActiveFile | undefined>(); const [openFiles, setOpenFiles] = useState<OpenFiles | undefined>();
useEffect(() => { useEffect(() => {
const unsubscribe = ideContext.subscribeToActiveFile(setActiveFile); const unsubscribe = ideContext.subscribeToOpenFiles(setOpenFiles);
// Set the initial value // Set the initial value
setActiveFile(ideContext.getActiveFileContext()); setOpenFiles(ideContext.getOpenFilesContext());
return unsubscribe; return unsubscribe;
}, []); }, []);
@ -880,7 +880,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Text> </Text>
) : ( ) : (
<ContextSummaryDisplay <ContextSummaryDisplay
activeFile={activeFile} openFiles={openFiles}
geminiMdFileCount={geminiMdFileCount} geminiMdFileCount={geminiMdFileCount}
contextFileNames={contextFileNames} contextFileNames={contextFileNames}
mcpServers={config.getMcpServers()} mcpServers={config.getMcpServers()}

View File

@ -7,7 +7,7 @@
import React from 'react'; import React from 'react';
import { Text } from 'ink'; import { Text } from 'ink';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import { type ActiveFile, type MCPServerConfig } from '@google/gemini-cli-core'; import { type OpenFiles, type MCPServerConfig } from '@google/gemini-cli-core';
import path from 'path'; import path from 'path';
interface ContextSummaryDisplayProps { interface ContextSummaryDisplayProps {
@ -16,7 +16,7 @@ interface ContextSummaryDisplayProps {
mcpServers?: Record<string, MCPServerConfig>; mcpServers?: Record<string, MCPServerConfig>;
blockedMcpServers?: Array<{ name: string; extensionName: string }>; blockedMcpServers?: Array<{ name: string; extensionName: string }>;
showToolDescriptions?: boolean; showToolDescriptions?: boolean;
activeFile?: ActiveFile; openFiles?: OpenFiles;
} }
export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
@ -25,7 +25,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
mcpServers, mcpServers,
blockedMcpServers, blockedMcpServers,
showToolDescriptions, showToolDescriptions,
activeFile, openFiles,
}) => { }) => {
const mcpServerCount = Object.keys(mcpServers || {}).length; const mcpServerCount = Object.keys(mcpServers || {}).length;
const blockedMcpServerCount = blockedMcpServers?.length || 0; const blockedMcpServerCount = blockedMcpServers?.length || 0;
@ -34,16 +34,16 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
geminiMdFileCount === 0 && geminiMdFileCount === 0 &&
mcpServerCount === 0 && mcpServerCount === 0 &&
blockedMcpServerCount === 0 && blockedMcpServerCount === 0 &&
!activeFile?.filePath !openFiles?.activeFile
) { ) {
return <Text> </Text>; // Render an empty space to reserve height return <Text> </Text>; // Render an empty space to reserve height
} }
const activeFileText = (() => { const activeFileText = (() => {
if (!activeFile?.filePath) { if (!openFiles?.activeFile) {
return ''; return '';
} }
return `Open File (${path.basename(activeFile.filePath)})`; return `Open File (${path.basename(openFiles.activeFile)})`;
})(); })();
const geminiMdText = (() => { const geminiMdText = (() => {

View File

@ -23,6 +23,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { setSimulate429 } from '../utils/testUtils.js'; import { setSimulate429 } from '../utils/testUtils.js';
import { tokenLimit } from './tokenLimits.js'; import { tokenLimit } from './tokenLimits.js';
import { ideContext } from '../services/ideContext.js';
// --- Mocks --- // --- Mocks ---
const mockChatCreateFn = vi.fn(); const mockChatCreateFn = vi.fn();
@ -71,6 +72,7 @@ vi.mock('../telemetry/index.js', () => ({
logApiResponse: vi.fn(), logApiResponse: vi.fn(),
logApiError: vi.fn(), logApiError: vi.fn(),
})); }));
vi.mock('../services/ideContext.js');
describe('findIndexAfterFraction', () => { describe('findIndexAfterFraction', () => {
const history: Content[] = [ const history: Content[] = [
@ -642,6 +644,69 @@ describe('Gemini Client (client.ts)', () => {
}); });
describe('sendMessageStream', () => { describe('sendMessageStream', () => {
it('should include IDE context when ideMode is enabled', async () => {
// Arrange
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
activeFile: '/path/to/active/file.ts',
selectedText: 'hello',
cursor: { line: 5, character: 10 },
recentOpenFiles: [
{ filePath: '/path/to/recent/file1.ts', timestamp: Date.now() },
{ filePath: '/path/to/recent/file2.ts', timestamp: Date.now() },
],
});
vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
const mockStream = (async function* () {
yield { type: 'content', value: 'Hello' };
})();
mockTurnRunFn.mockReturnValue(mockStream);
const mockChat: Partial<GeminiChat> = {
addHistory: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
};
client['chat'] = mockChat as GeminiChat;
const mockGenerator: Partial<ContentGenerator> = {
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
generateContent: mockGenerateContentFn,
};
client['contentGenerator'] = mockGenerator as ContentGenerator;
const initialRequest = [{ text: 'Hi' }];
// Act
const stream = client.sendMessageStream(
initialRequest,
new AbortController().signal,
'prompt-id-ide',
);
for await (const _ of stream) {
// consume stream
}
// Assert
expect(ideContext.getOpenFilesContext).toHaveBeenCalled();
const expectedContext = `
This is the file that the user was most recently looking at:
- Path: /path/to/active/file.ts
This is the cursor position in the file:
- Cursor Position: Line 5, Character 10
This is the selected text in the active file:
- hello
Here are files the user has recently opened, with the most recent at the top:
- /path/to/recent/file1.ts
- /path/to/recent/file2.ts
`.trim();
const expectedRequest = [{ text: expectedContext }, ...initialRequest];
expect(mockTurnRunFn).toHaveBeenCalledWith(
expectedRequest,
expect.any(Object),
);
});
it('should return the turn instance after the stream is complete', async () => { it('should return the turn instance after the stream is complete', async () => {
// Arrange // Arrange
const mockStream = (async function* () { const mockStream = (async function* () {

View File

@ -311,22 +311,42 @@ export class GeminiClient {
} }
if (this.config.getIdeMode()) { if (this.config.getIdeMode()) {
const activeFile = ideContext.getActiveFileContext(); const openFiles = ideContext.getOpenFilesContext();
if (activeFile?.filePath) { if (openFiles) {
let context = ` const contextParts: string[] = [];
This is the file that the user was most recently looking at: if (openFiles.activeFile) {
- Path: ${activeFile.filePath}`; contextParts.push(
if (activeFile.cursor) { `This is the file that the user was most recently looking at:\n- Path: ${openFiles.activeFile}`,
context += ` );
This is the cursor position in the file: if (openFiles.cursor) {
- Cursor Position: Line ${activeFile.cursor.line}, Character ${activeFile.cursor.character}`; contextParts.push(
`This is the cursor position in the file:\n- Cursor Position: Line ${openFiles.cursor.line}, Character ${openFiles.cursor.character}`,
);
} }
if (openFiles.selectedText) {
contextParts.push(
`This is the selected text in the active file:\n- ${openFiles.selectedText}`,
);
}
}
if (openFiles.recentOpenFiles && openFiles.recentOpenFiles.length > 0) {
const recentFiles = openFiles.recentOpenFiles
.map((file) => `- ${file.filePath}`)
.join('\n');
contextParts.push(
`Here are files the user has recently opened, with the most recent at the top:\n${recentFiles}`,
);
}
if (contextParts.length > 0) {
request = [ request = [
{ text: context }, { text: contextParts.join('\n') },
...(Array.isArray(request) ? request : [request]), ...(Array.isArray(request) ? request : [request]),
]; ];
} }
} }
}
const turn = new Turn(this.getChat(), prompt_id); const turn = new Turn(this.getChat(), prompt_id);

View File

@ -16,59 +16,59 @@ describe('ideContext - Active File', () => {
}); });
it('should return undefined initially for active file context', () => { it('should return undefined initially for active file context', () => {
expect(ideContext.getActiveFileContext()).toBeUndefined(); expect(ideContext.getOpenFilesContext()).toBeUndefined();
}); });
it('should set and retrieve the active file context', () => { it('should set and retrieve the active file context', () => {
const testFile = { const testFile = {
filePath: '/path/to/test/file.ts', activeFile: '/path/to/test/file.ts',
cursor: { line: 5, character: 10 }, selectedText: '1234',
}; };
ideContext.setActiveFileContext(testFile); ideContext.setOpenFilesContext(testFile);
const activeFile = ideContext.getActiveFileContext(); const activeFile = ideContext.getOpenFilesContext();
expect(activeFile).toEqual(testFile); expect(activeFile).toEqual(testFile);
}); });
it('should update the active file context when called multiple times', () => { it('should update the active file context when called multiple times', () => {
const firstFile = { const firstFile = {
filePath: '/path/to/first.js', activeFile: '/path/to/first.js',
cursor: { line: 1, character: 1 }, selectedText: '1234',
}; };
ideContext.setActiveFileContext(firstFile); ideContext.setOpenFilesContext(firstFile);
const secondFile = { const secondFile = {
filePath: '/path/to/second.py', activeFile: '/path/to/second.py',
cursor: { line: 20, character: 30 }, cursor: { line: 20, character: 30 },
}; };
ideContext.setActiveFileContext(secondFile); ideContext.setOpenFilesContext(secondFile);
const activeFile = ideContext.getActiveFileContext(); const activeFile = ideContext.getOpenFilesContext();
expect(activeFile).toEqual(secondFile); expect(activeFile).toEqual(secondFile);
}); });
it('should handle empty string for file path', () => { it('should handle empty string for file path', () => {
const testFile = { const testFile = {
filePath: '', activeFile: '',
cursor: { line: 0, character: 0 }, selectedText: '1234',
}; };
ideContext.setActiveFileContext(testFile); ideContext.setOpenFilesContext(testFile);
expect(ideContext.getActiveFileContext()).toEqual(testFile); expect(ideContext.getOpenFilesContext()).toEqual(testFile);
}); });
it('should notify subscribers when active file context changes', () => { it('should notify subscribers when active file context changes', () => {
const subscriber1 = vi.fn(); const subscriber1 = vi.fn();
const subscriber2 = vi.fn(); const subscriber2 = vi.fn();
ideContext.subscribeToActiveFile(subscriber1); ideContext.subscribeToOpenFiles(subscriber1);
ideContext.subscribeToActiveFile(subscriber2); ideContext.subscribeToOpenFiles(subscriber2);
const testFile = { const testFile = {
filePath: '/path/to/subscribed.ts', activeFile: '/path/to/subscribed.ts',
cursor: { line: 15, character: 25 }, cursor: { line: 15, character: 25 },
}; };
ideContext.setActiveFileContext(testFile); ideContext.setOpenFilesContext(testFile);
expect(subscriber1).toHaveBeenCalledTimes(1); expect(subscriber1).toHaveBeenCalledTimes(1);
expect(subscriber1).toHaveBeenCalledWith(testFile); expect(subscriber1).toHaveBeenCalledWith(testFile);
@ -77,10 +77,10 @@ describe('ideContext - Active File', () => {
// Test with another update // Test with another update
const newFile = { const newFile = {
filePath: '/path/to/new.js', activeFile: '/path/to/new.js',
cursor: { line: 1, character: 1 }, selectedText: '1234',
}; };
ideContext.setActiveFileContext(newFile); ideContext.setOpenFilesContext(newFile);
expect(subscriber1).toHaveBeenCalledTimes(2); expect(subscriber1).toHaveBeenCalledTimes(2);
expect(subscriber1).toHaveBeenCalledWith(newFile); expect(subscriber1).toHaveBeenCalledWith(newFile);
@ -92,21 +92,21 @@ describe('ideContext - Active File', () => {
const subscriber1 = vi.fn(); const subscriber1 = vi.fn();
const subscriber2 = vi.fn(); const subscriber2 = vi.fn();
const unsubscribe1 = ideContext.subscribeToActiveFile(subscriber1); const unsubscribe1 = ideContext.subscribeToOpenFiles(subscriber1);
ideContext.subscribeToActiveFile(subscriber2); ideContext.subscribeToOpenFiles(subscriber2);
ideContext.setActiveFileContext({ ideContext.setOpenFilesContext({
filePath: '/path/to/file1.txt', activeFile: '/path/to/file1.txt',
cursor: { line: 1, character: 1 }, selectedText: '1234',
}); });
expect(subscriber1).toHaveBeenCalledTimes(1); expect(subscriber1).toHaveBeenCalledTimes(1);
expect(subscriber2).toHaveBeenCalledTimes(1); expect(subscriber2).toHaveBeenCalledTimes(1);
unsubscribe1(); unsubscribe1();
ideContext.setActiveFileContext({ ideContext.setOpenFilesContext({
filePath: '/path/to/file2.txt', activeFile: '/path/to/file2.txt',
cursor: { line: 2, character: 2 }, selectedText: '1234',
}); });
expect(subscriber1).toHaveBeenCalledTimes(1); // Should not be called again expect(subscriber1).toHaveBeenCalledTimes(1); // Should not be called again
expect(subscriber2).toHaveBeenCalledTimes(2); expect(subscriber2).toHaveBeenCalledTimes(2);
@ -114,27 +114,27 @@ describe('ideContext - Active File', () => {
it('should allow the cursor to be optional', () => { it('should allow the cursor to be optional', () => {
const testFile = { const testFile = {
filePath: '/path/to/test/file.ts', activeFile: '/path/to/test/file.ts',
}; };
ideContext.setActiveFileContext(testFile); ideContext.setOpenFilesContext(testFile);
const activeFile = ideContext.getActiveFileContext(); const activeFile = ideContext.getOpenFilesContext();
expect(activeFile).toEqual(testFile); expect(activeFile).toEqual(testFile);
}); });
it('should clear the active file context', () => { it('should clear the active file context', () => {
const testFile = { const testFile = {
filePath: '/path/to/test/file.ts', activeFile: '/path/to/test/file.ts',
cursor: { line: 5, character: 10 }, selectedText: '1234',
}; };
ideContext.setActiveFileContext(testFile); ideContext.setOpenFilesContext(testFile);
expect(ideContext.getActiveFileContext()).toEqual(testFile); expect(ideContext.getOpenFilesContext()).toEqual(testFile);
ideContext.clearActiveFileContext(); ideContext.clearOpenFilesContext();
expect(ideContext.getActiveFileContext()).toBeUndefined(); expect(ideContext.getOpenFilesContext()).toBeUndefined();
}); });
}); });

View File

@ -10,7 +10,6 @@ import { z } from 'zod';
* The reserved server name for the IDE's MCP server. * The reserved server name for the IDE's MCP server.
*/ */
export const IDE_SERVER_NAME = '_ide_server'; export const IDE_SERVER_NAME = '_ide_server';
/** /**
* Zod schema for validating a cursor position. * Zod schema for validating a cursor position.
*/ */
@ -23,8 +22,9 @@ export type Cursor = z.infer<typeof CursorSchema>;
/** /**
* Zod schema for validating an active file context from the IDE. * Zod schema for validating an active file context from the IDE.
*/ */
export const ActiveFileSchema = z.object({ export const OpenFilesSchema = z.object({
filePath: z.string(), activeFile: z.string(),
selectedText: z.string().optional(),
cursor: CursorSchema.optional(), cursor: CursorSchema.optional(),
recentOpenFiles: z recentOpenFiles: z
.array( .array(
@ -35,17 +35,17 @@ export const ActiveFileSchema = z.object({
) )
.optional(), .optional(),
}); });
export type ActiveFile = z.infer<typeof ActiveFileSchema>; export type OpenFiles = z.infer<typeof OpenFilesSchema>;
/** /**
* Zod schema for validating the 'ide/activeFileChanged' notification from the IDE. * Zod schema for validating the 'ide/openFilesChanged' notification from the IDE.
*/ */
export const ActiveFileNotificationSchema = z.object({ export const OpenFilesNotificationSchema = z.object({
method: z.literal('ide/activeFileChanged'), method: z.literal('ide/openFilesChanged'),
params: ActiveFileSchema, params: OpenFilesSchema,
}); });
type ActiveFileSubscriber = (activeFile: ActiveFile | undefined) => void; type OpenFilesSubscriber = (openFiles: OpenFiles | undefined) => void;
/** /**
* Creates a new store for managing the IDE's active file context. * Creates a new store for managing the IDE's active file context.
@ -55,41 +55,41 @@ type ActiveFileSubscriber = (activeFile: ActiveFile | undefined) => void;
* @returns An object with methods to interact with the active file context. * @returns An object with methods to interact with the active file context.
*/ */
export function createIdeContextStore() { export function createIdeContextStore() {
let activeFileContext: ActiveFile | undefined = undefined; let openFilesContext: OpenFiles | undefined = undefined;
const subscribers = new Set<ActiveFileSubscriber>(); const subscribers = new Set<OpenFilesSubscriber>();
/** /**
* Notifies all registered subscribers about the current active file context. * Notifies all registered subscribers about the current active file context.
*/ */
function notifySubscribers(): void { function notifySubscribers(): void {
for (const subscriber of subscribers) { for (const subscriber of subscribers) {
subscriber(activeFileContext); subscriber(openFilesContext);
} }
} }
/** /**
* Sets the active file context and notifies all registered subscribers of the change. * Sets the active file context and notifies all registered subscribers of the change.
* @param newActiveFile The new active file context from the IDE. * @param newOpenFiles The new active file context from the IDE.
*/ */
function setActiveFileContext(newActiveFile: ActiveFile): void { function setOpenFilesContext(newOpenFiles: OpenFiles): void {
activeFileContext = newActiveFile; openFilesContext = newOpenFiles;
notifySubscribers(); notifySubscribers();
} }
/** /**
* Clears the active file context and notifies all registered subscribers of the change. * Clears the active file context and notifies all registered subscribers of the change.
*/ */
function clearActiveFileContext(): void { function clearOpenFilesContext(): void {
activeFileContext = undefined; openFilesContext = undefined;
notifySubscribers(); notifySubscribers();
} }
/** /**
* Retrieves the current active file context. * Retrieves the current active file context.
* @returns The `ActiveFile` object if a file is active, otherwise `undefined`. * @returns The `OpenFiles` object if a file is active, otherwise `undefined`.
*/ */
function getActiveFileContext(): ActiveFile | undefined { function getOpenFilesContext(): OpenFiles | undefined {
return activeFileContext; return openFilesContext;
} }
/** /**
@ -101,7 +101,7 @@ export function createIdeContextStore() {
* @param subscriber The function to be called when the active file context changes. * @param subscriber The function to be called when the active file context changes.
* @returns A function that, when called, will unsubscribe the provided subscriber. * @returns A function that, when called, will unsubscribe the provided subscriber.
*/ */
function subscribeToActiveFile(subscriber: ActiveFileSubscriber): () => void { function subscribeToOpenFiles(subscriber: OpenFilesSubscriber): () => void {
subscribers.add(subscriber); subscribers.add(subscriber);
return () => { return () => {
subscribers.delete(subscriber); subscribers.delete(subscriber);
@ -109,10 +109,10 @@ export function createIdeContextStore() {
} }
return { return {
setActiveFileContext, setOpenFilesContext,
getActiveFileContext, getOpenFilesContext,
subscribeToActiveFile, subscribeToOpenFiles,
clearActiveFileContext, clearOpenFilesContext,
}; };
} }

View File

@ -22,7 +22,7 @@ import { DiscoveredMCPTool } from './mcp-tool.js';
import { FunctionDeclaration, mcpToTool } from '@google/genai'; import { FunctionDeclaration, mcpToTool } from '@google/genai';
import { ToolRegistry } from './tool-registry.js'; import { ToolRegistry } from './tool-registry.js';
import { import {
ActiveFileNotificationSchema, OpenFilesNotificationSchema,
IDE_SERVER_NAME, IDE_SERVER_NAME,
ideContext, ideContext,
} from '../services/ideContext.js'; } from '../services/ideContext.js';
@ -217,15 +217,15 @@ export async function connectAndDiscover(
console.error(`MCP ERROR (${mcpServerName}):`, error.toString()); console.error(`MCP ERROR (${mcpServerName}):`, error.toString());
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED); updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
if (mcpServerName === IDE_SERVER_NAME) { if (mcpServerName === IDE_SERVER_NAME) {
ideContext.clearActiveFileContext(); ideContext.clearOpenFilesContext();
} }
}; };
if (mcpServerName === IDE_SERVER_NAME) { if (mcpServerName === IDE_SERVER_NAME) {
mcpClient.setNotificationHandler( mcpClient.setNotificationHandler(
ActiveFileNotificationSchema, OpenFilesNotificationSchema,
(notification) => { (notification) => {
ideContext.setActiveFileContext(notification.params); ideContext.setOpenFilesContext(notification.params);
}, },
); );
} }

View File

@ -19,7 +19,7 @@ import { RecentFilesManager } from './recent-files-manager.js';
const MCP_SESSION_ID_HEADER = 'mcp-session-id'; const MCP_SESSION_ID_HEADER = 'mcp-session-id';
const IDE_SERVER_PORT_ENV_VAR = 'GEMINI_CLI_IDE_SERVER_PORT'; const IDE_SERVER_PORT_ENV_VAR = 'GEMINI_CLI_IDE_SERVER_PORT';
function sendActiveFileChangedNotification( function sendOpenFilesChangedNotification(
transport: StreamableHTTPServerTransport, transport: StreamableHTTPServerTransport,
logger: vscode.OutputChannel, logger: vscode.OutputChannel,
recentFilesManager: RecentFilesManager, recentFilesManager: RecentFilesManager,
@ -29,9 +29,9 @@ function sendActiveFileChangedNotification(
logger.appendLine(`Sending active file changed notification: ${filePath}`); logger.appendLine(`Sending active file changed notification: ${filePath}`);
const notification: JSONRPCNotification = { const notification: JSONRPCNotification = {
jsonrpc: '2.0', jsonrpc: '2.0',
method: 'ide/activeFileChanged', method: 'ide/openFilesChanged',
params: { params: {
filePath, activeFile: filePath,
recentOpenFiles: recentFilesManager.recentFiles, recentOpenFiles: recentFilesManager.recentFiles,
}, },
}; };
@ -60,7 +60,7 @@ export class IDEServer {
const recentFilesManager = new RecentFilesManager(context); const recentFilesManager = new RecentFilesManager(context);
const disposable = recentFilesManager.onDidChange(() => { const disposable = recentFilesManager.onDidChange(() => {
for (const transport of Object.values(transports)) { for (const transport of Object.values(transports)) {
sendActiveFileChangedNotification( sendOpenFilesChangedNotification(
transport, transport,
this.logger, this.logger,
recentFilesManager, recentFilesManager,
@ -168,7 +168,7 @@ export class IDEServer {
} }
if (!sessionsWithInitialNotification.has(sessionId)) { if (!sessionsWithInitialNotification.has(sessionId)) {
sendActiveFileChangedNotification( sendOpenFilesChangedNotification(
transport, transport,
this.logger, this.logger,
recentFilesManager, recentFilesManager,
@ -224,7 +224,7 @@ const createMcpServer = () => {
{ capabilities: { logging: {} } }, { capabilities: { logging: {} } },
); );
server.registerTool( server.registerTool(
'getActiveFile', 'getOpenFiles',
{ {
description: description:
'(IDE Tool) Get the path of the file currently active in VS Code.', '(IDE Tool) Get the path of the file currently active in VS Code.',