From 4d4eacfc40f87ecc991aaecc12c046d49654425c Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Thu, 7 Aug 2025 17:19:31 -0400 Subject: [PATCH] Few IDE integration polishes (#5727) --- .../IDEContextDetailDisplay.test.tsx | 66 +++++++++++++++++++ .../ui/components/IDEContextDetailDisplay.tsx | 27 ++++++-- .../IDEContextDetailDisplay.test.tsx.snap | 24 +++++++ packages/core/src/ide/ide-client.ts | 43 ++++++++++-- 4 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap 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 {