diff --git a/packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx b/packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx
new file mode 100644
index 00000000..629d6c2e
--- /dev/null
+++ b/packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx
@@ -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(
+ ,
+ );
+ 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(
+ ,
+ );
+ 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(
+ ,
+ );
+ const output = lastFrame();
+ expect(output).toMatchSnapshot();
+ });
+});
diff --git a/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx b/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx
index a1739227..ec3c2dad 100644
--- a/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx
+++ b/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx
@@ -23,6 +23,12 @@ export function IDEContextDetailDisplay({
return null;
}
+ const basenameCounts = new Map();
+ for (const file of openFiles) {
+ const basename = path.basename(file.path);
+ basenameCounts.set(basename, (basenameCounts.get(basename) || 0) + 1);
+ }
+
return (
0 && (
Open files:
- {openFiles.map((file: File) => (
-
- - {path.basename(file.path)}
- {file.isActive ? ' (active)' : ''}
-
- ))}
+ {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 (
+
+ - {displayName}
+ {file.isActive ? ' (active)' : ''}
+
+ );
+ })}
)}
diff --git a/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap
new file mode 100644
index 00000000..8b84e1f3
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap
@@ -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 │
+╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts
index 42b79c44..508dfea1 100644
--- a/packages/core/src/ide/ide-client.ts
+++ b/packages/core/src/ide/ide-client.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import * as fs from 'node:fs';
import {
detectIde,
DetectedIde,
@@ -23,6 +24,8 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 = {
@@ -36,6 +39,16 @@ export enum IDEConnectionStatus {
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.
*/
@@ -69,7 +82,15 @@ export class IdeClient {
this.setState(IDEConnectionStatus.Connecting);
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;
}
@@ -174,7 +195,11 @@ export class IdeClient {
return this.currentIdeDisplayName;
}
- private setState(status: IDEConnectionStatus, details?: string) {
+ private setState(
+ status: IDEConnectionStatus,
+ details?: string,
+ logToConsole = false,
+ ) {
const isAlreadyDisconnected =
this.state.status === IDEConnectionStatus.Disconnected &&
status === IDEConnectionStatus.Disconnected;
@@ -186,7 +211,10 @@ export class IdeClient {
}
if (status === IDEConnectionStatus.Disconnected) {
- logger.debug('IDE integration disconnected:', details);
+ if (logToConsole) {
+ logger.error(details);
+ }
+ logger.debug(details);
ideContext.clearIdeContext();
}
}
@@ -197,6 +225,7 @@ export class IdeClient {
this.setState(
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.`,
+ true,
);
return false;
}
@@ -204,13 +233,15 @@ export class IdeClient {
this.setState(
IDEConnectionStatus.Disconnected,
`To use this feature, please open a single workspace folder in ${this.currentIdeDisplayName} and try again.`,
+ true,
);
return false;
}
- if (ideWorkspacePath !== process.cwd()) {
+ if (getRealPath(ideWorkspacePath) !== getRealPath(process.cwd())) {
this.setState(
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.`,
+ true,
);
return false;
}
@@ -223,6 +254,7 @@ export class IdeClient {
this.setState(
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.`,
+ true,
);
return undefined;
}
@@ -244,12 +276,14 @@ export class IdeClient {
this.setState(
IDEConnectionStatus.Disconnected,
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`,
+ true,
);
};
this.client.onclose = () => {
this.setState(
IDEConnectionStatus.Disconnected,
`IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`,
+ true,
);
};
this.client.setNotificationHandler(
@@ -299,6 +333,7 @@ export class IdeClient {
this.setState(
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.`,
+ true,
);
if (transport) {
try {