Few IDE integration polishes (#5727)

This commit is contained in:
Shreya Keshive 2025-08-07 17:19:31 -04:00 committed by GitHub
parent 19491b7b94
commit 4d4eacfc40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 10 deletions

View File

@ -0,0 +1,66 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { describe, it, expect } from 'vitest';
import { IDEContextDetailDisplay } from './IDEContextDetailDisplay.js';
import { type IdeContext } from '@google/gemini-cli-core';
describe('IDEContextDetailDisplay', () => {
it('renders an empty string when there are no open files', () => {
const ideContext: IdeContext = {
workspaceState: {
openFiles: [],
},
};
const { lastFrame } = render(
<IDEContextDetailDisplay
ideContext={ideContext}
detectedIdeDisplay="VS Code"
/>,
);
expect(lastFrame()).toBe('');
});
it('renders a list of open files with active status', () => {
const ideContext: IdeContext = {
workspaceState: {
openFiles: [
{ path: '/foo/bar.txt', isActive: true },
{ path: '/foo/baz.txt', isActive: false },
],
},
};
const { lastFrame } = render(
<IDEContextDetailDisplay
ideContext={ideContext}
detectedIdeDisplay="VS Code"
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
it('handles duplicate basenames by showing path hints', () => {
const ideContext: IdeContext = {
workspaceState: {
openFiles: [
{ path: '/foo/bar.txt', isActive: true },
{ path: '/qux/bar.txt', isActive: false },
{ path: '/foo/unique.txt', isActive: false },
],
},
};
const { lastFrame } = render(
<IDEContextDetailDisplay
ideContext={ideContext}
detectedIdeDisplay="VS Code"
/>,
);
const output = lastFrame();
expect(output).toMatchSnapshot();
});
});

View File

@ -23,6 +23,12 @@ export function IDEContextDetailDisplay({
return null; return null;
} }
const basenameCounts = new Map<string, number>();
for (const file of openFiles) {
const basename = path.basename(file.path);
basenameCounts.set(basename, (basenameCounts.get(basename) || 0) + 1);
}
return ( return (
<Box <Box
flexDirection="column" flexDirection="column"
@ -38,12 +44,21 @@ export function IDEContextDetailDisplay({
{openFiles.length > 0 && ( {openFiles.length > 0 && (
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
<Text bold>Open files:</Text> <Text bold>Open files:</Text>
{openFiles.map((file: File) => ( {openFiles.map((file: File) => {
<Text key={file.path}> const basename = path.basename(file.path);
- {path.basename(file.path)} const isDuplicate = (basenameCounts.get(basename) || 0) > 1;
{file.isActive ? ' (active)' : ''} const parentDir = path.basename(path.dirname(file.path));
</Text> const displayName = isDuplicate
))} ? `${basename} (/${parentDir})`
: basename;
return (
<Text key={file.path}>
- {displayName}
{file.isActive ? ' (active)' : ''}
</Text>
);
})}
</Box> </Box>
)} )}
</Box> </Box>

View File

@ -0,0 +1,24 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`IDEContextDetailDisplay > handles duplicate basenames by showing path hints 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ VS Code Context (ctrl+e to toggle) │
│ │
│ Open files: │
│ - bar.txt (/foo) (active) │
│ - bar.txt (/qux) │
│ - unique.txt │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`IDEContextDetailDisplay > renders a list of open files with active status 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ VS Code Context (ctrl+e to toggle) │
│ │
│ Open files: │
│ - bar.txt (active) │
│ - baz.txt │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import * as fs from 'node:fs';
import { import {
detectIde, detectIde,
DetectedIde, DetectedIde,
@ -23,6 +24,8 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
const logger = { const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) => console.debug('[DEBUG] [IDEClient]', ...args), debug: (...args: any[]) => console.debug('[DEBUG] [IDEClient]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => console.error('[ERROR] [IDEClient]', ...args),
}; };
export type IDEConnectionState = { export type IDEConnectionState = {
@ -36,6 +39,16 @@ export enum IDEConnectionStatus {
Connecting = 'connecting', Connecting = 'connecting',
} }
function getRealPath(path: string): string {
try {
return fs.realpathSync(path);
} catch (_e) {
// If realpathSync fails, it might be because the path doesn't exist.
// In that case, we can fall back to the original path.
return path;
}
}
/** /**
* Manages the connection to and interaction with the IDE server. * Manages the connection to and interaction with the IDE server.
*/ */
@ -69,7 +82,15 @@ export class IdeClient {
this.setState(IDEConnectionStatus.Connecting); this.setState(IDEConnectionStatus.Connecting);
if (!this.currentIde || !this.currentIdeDisplayName) { if (!this.currentIde || !this.currentIdeDisplayName) {
this.setState(IDEConnectionStatus.Disconnected); this.setState(
IDEConnectionStatus.Disconnected,
`IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: ${Object.values(
DetectedIde,
)
.map((ide) => getIdeDisplayName(ide))
.join(', ')}`,
true,
);
return; return;
} }
@ -174,7 +195,11 @@ export class IdeClient {
return this.currentIdeDisplayName; return this.currentIdeDisplayName;
} }
private setState(status: IDEConnectionStatus, details?: string) { private setState(
status: IDEConnectionStatus,
details?: string,
logToConsole = false,
) {
const isAlreadyDisconnected = const isAlreadyDisconnected =
this.state.status === IDEConnectionStatus.Disconnected && this.state.status === IDEConnectionStatus.Disconnected &&
status === IDEConnectionStatus.Disconnected; status === IDEConnectionStatus.Disconnected;
@ -186,7 +211,10 @@ export class IdeClient {
} }
if (status === IDEConnectionStatus.Disconnected) { if (status === IDEConnectionStatus.Disconnected) {
logger.debug('IDE integration disconnected:', details); if (logToConsole) {
logger.error(details);
}
logger.debug(details);
ideContext.clearIdeContext(); ideContext.clearIdeContext();
} }
} }
@ -197,6 +225,7 @@ export class IdeClient {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`, `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`,
true,
); );
return false; return false;
} }
@ -204,13 +233,15 @@ export class IdeClient {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`To use this feature, please open a single workspace folder in ${this.currentIdeDisplayName} and try again.`, `To use this feature, please open a single workspace folder in ${this.currentIdeDisplayName} and try again.`,
true,
); );
return false; return false;
} }
if (ideWorkspacePath !== process.cwd()) { if (getRealPath(ideWorkspacePath) !== getRealPath(process.cwd())) {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`Directory mismatch. Gemini CLI is running in a different location than the open workspace in ${this.currentIdeDisplayName}. Please run the CLI from the same directory as your project's root folder.`, `Directory mismatch. Gemini CLI is running in a different location than the open workspace in ${this.currentIdeDisplayName}. Please run the CLI from the same directory as your project's root folder.`,
true,
); );
return false; return false;
} }
@ -223,6 +254,7 @@ export class IdeClient {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`, `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`,
true,
); );
return undefined; return undefined;
} }
@ -244,12 +276,14 @@ export class IdeClient {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`, `IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`,
true,
); );
}; };
this.client.onclose = () => { this.client.onclose = () => {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`, `IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`,
true,
); );
}; };
this.client.setNotificationHandler( this.client.setNotificationHandler(
@ -299,6 +333,7 @@ export class IdeClient {
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
`Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`, `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try refreshing your terminal. To install the extension, run /ide install.`,
true,
); );
if (transport) { if (transport) {
try { try {