Few IDE integration polishes (#5727)
This commit is contained in:
parent
19491b7b94
commit
4d4eacfc40
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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) => {
|
||||||
|
const basename = path.basename(file.path);
|
||||||
|
const isDuplicate = (basenameCounts.get(basename) || 0) > 1;
|
||||||
|
const parentDir = path.basename(path.dirname(file.path));
|
||||||
|
const displayName = isDuplicate
|
||||||
|
? `${basename} (/${parentDir})`
|
||||||
|
: basename;
|
||||||
|
|
||||||
|
return (
|
||||||
<Text key={file.path}>
|
<Text key={file.path}>
|
||||||
- {path.basename(file.path)}
|
- {displayName}
|
||||||
{file.isActive ? ' (active)' : ''}
|
{file.isActive ? ' (active)' : ''}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -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 │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
`;
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue