summaryrefslogtreecommitdiff
path: root/packages/cli
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli')
-rw-r--r--packages/cli/src/ui/App.tsx15
-rw-r--r--packages/cli/src/ui/commands/ideCommand.test.ts28
-rw-r--r--packages/cli/src/ui/commands/ideCommand.ts75
-rw-r--r--packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx66
-rw-r--r--packages/cli/src/ui/components/IDEContextDetailDisplay.tsx66
5 files changed, 90 insertions, 160 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 9550faa2..9f18fe55 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -48,7 +48,6 @@ import { registerCleanup } from '../utils/cleanup.js';
import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js';
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js';
-import { IDEContextDetailDisplay } from './components/IDEContextDetailDisplay.js';
import { useHistory } from './hooks/useHistoryManager.js';
import process from 'node:process';
import {
@@ -174,8 +173,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [showToolDescriptions, setShowToolDescriptions] =
useState<boolean>(false);
- const [showIDEContextDetail, setShowIDEContextDetail] =
- useState<boolean>(false);
+
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
const [quittingMessages, setQuittingMessages] = useState<
HistoryItem[] | null
@@ -640,7 +638,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
config.getIdeMode() &&
ideContextState
) {
- setShowIDEContextDetail((prev) => !prev);
+ handleSlashCommand('/ide status');
} else if (key.ctrl && (input === 'c' || input === 'C')) {
if (isAuthenticating) {
// Let AuthInProgress component handle the input.
@@ -1040,14 +1038,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{shellModeActive && <ShellModeIndicator />}
</Box>
</Box>
- {showIDEContextDetail && (
- <IDEContextDetailDisplay
- ideContext={ideContextState}
- detectedIdeDisplay={config
- .getIdeClient()
- .getDetectedIdeDisplayName()}
- />
- )}
+
{showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
diff --git a/packages/cli/src/ui/commands/ideCommand.test.ts b/packages/cli/src/ui/commands/ideCommand.test.ts
index 9898b1e8..10a97e2a 100644
--- a/packages/cli/src/ui/commands/ideCommand.test.ts
+++ b/packages/cli/src/ui/commands/ideCommand.test.ts
@@ -93,13 +93,14 @@ describe('ideCommand', () => {
} as unknown as ReturnType<Config['getIdeClient']>);
});
- it('should show connected status', () => {
+ it('should show connected status', async () => {
mockGetConnectionStatus.mockReturnValue({
status: core.IDEConnectionStatus.Connected,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands!.find((c) => c.name === 'status')!
- .action!(mockContext, '');
+ const result = await command!.subCommands!.find(
+ (c) => c.name === 'status',
+ )!.action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -108,13 +109,14 @@ describe('ideCommand', () => {
});
});
- it('should show connecting status', () => {
+ it('should show connecting status', async () => {
mockGetConnectionStatus.mockReturnValue({
status: core.IDEConnectionStatus.Connecting,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands!.find((c) => c.name === 'status')!
- .action!(mockContext, '');
+ const result = await command!.subCommands!.find(
+ (c) => c.name === 'status',
+ )!.action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -122,13 +124,14 @@ describe('ideCommand', () => {
content: `🟡 Connecting...`,
});
});
- it('should show disconnected status', () => {
+ it('should show disconnected status', async () => {
mockGetConnectionStatus.mockReturnValue({
status: core.IDEConnectionStatus.Disconnected,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands!.find((c) => c.name === 'status')!
- .action!(mockContext, '');
+ const result = await command!.subCommands!.find(
+ (c) => c.name === 'status',
+ )!.action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
@@ -137,15 +140,16 @@ describe('ideCommand', () => {
});
});
- it('should show disconnected status with details', () => {
+ it('should show disconnected status with details', async () => {
const details = 'Something went wrong';
mockGetConnectionStatus.mockReturnValue({
status: core.IDEConnectionStatus.Disconnected,
details,
});
const command = ideCommand(mockConfig);
- const result = command!.subCommands!.find((c) => c.name === 'status')!
- .action!(mockContext, '');
+ const result = await command!.subCommands!.find(
+ (c) => c.name === 'status',
+ )!.action!(mockContext, '');
expect(mockGetConnectionStatus).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts
index fe9f764a..29e264d4 100644
--- a/packages/cli/src/ui/commands/ideCommand.ts
+++ b/packages/cli/src/ui/commands/ideCommand.ts
@@ -11,7 +11,10 @@ import {
getIdeDisplayName,
getIdeInstaller,
IdeClient,
+ type File,
+ ideContext,
} from '@google/gemini-cli-core';
+import path from 'node:path';
import {
CommandContext,
SlashCommand,
@@ -49,6 +52,70 @@ function getIdeStatusMessage(ideClient: IdeClient): {
}
}
+function formatFileList(openFiles: File[]): string {
+ 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);
+ }
+
+ const fileList = 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)' : ''}`;
+ })
+ .join('\n');
+
+ return `\n\nOpen files:\n${fileList}`;
+}
+
+async function getIdeStatusMessageWithFiles(ideClient: IdeClient): Promise<{
+ messageType: 'info' | 'error';
+ content: string;
+}> {
+ const connection = ideClient.getConnectionStatus();
+ switch (connection.status) {
+ case IDEConnectionStatus.Connected: {
+ let content = `🟢 Connected to ${ideClient.getDetectedIdeDisplayName()}`;
+ try {
+ const context = await ideContext.getIdeContext();
+ const openFiles = context?.workspaceState?.openFiles;
+
+ if (openFiles && openFiles.length > 0) {
+ content += formatFileList(openFiles);
+ }
+ } catch (_e) {
+ // Ignore
+ }
+ return {
+ messageType: 'info',
+ content,
+ };
+ }
+ case IDEConnectionStatus.Connecting:
+ return {
+ messageType: 'info',
+ content: `🟡 Connecting...`,
+ };
+ default: {
+ let content = `🔴 Disconnected`;
+ if (connection?.details) {
+ content += `: ${connection.details}`;
+ }
+ return {
+ messageType: 'error',
+ content,
+ };
+ }
+ }
+}
+
export const ideCommand = (config: Config | null): SlashCommand | null => {
if (!config || !config.getIdeModeFeature()) {
return null;
@@ -66,8 +133,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
messageType: 'error',
content: `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))
+ ).map((ide) => getIdeDisplayName(ide))}
.join(', ')}`,
}) as const,
};
@@ -84,8 +150,9 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
name: 'status',
description: 'check status of IDE integration',
kind: CommandKind.BUILT_IN,
- action: (): SlashCommandActionReturn => {
- const { messageType, content } = getIdeStatusMessage(ideClient);
+ action: async (): Promise<SlashCommandActionReturn> => {
+ const { messageType, content } =
+ await getIdeStatusMessageWithFiles(ideClient);
return {
type: 'message',
messageType,
diff --git a/packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx b/packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx
deleted file mode 100644
index 629d6c2e..00000000
--- a/packages/cli/src/ui/components/IDEContextDetailDisplay.test.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @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();
- });
-});
diff --git a/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx b/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx
deleted file mode 100644
index ec3c2dad..00000000
--- a/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { type File, type IdeContext } from '@google/gemini-cli-core';
-import { Box, Text } from 'ink';
-import path from 'node:path';
-import { Colors } from '../colors.js';
-
-interface IDEContextDetailDisplayProps {
- ideContext: IdeContext | undefined;
- detectedIdeDisplay: string | undefined;
-}
-
-export function IDEContextDetailDisplay({
- ideContext,
- detectedIdeDisplay,
-}: IDEContextDetailDisplayProps) {
- const openFiles = ideContext?.workspaceState?.openFiles;
- if (!openFiles || openFiles.length === 0) {
- 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 (
- <Box
- flexDirection="column"
- marginTop={1}
- borderStyle="round"
- borderColor={Colors.AccentCyan}
- paddingX={1}
- >
- <Text color={Colors.AccentCyan} bold>
- {detectedIdeDisplay ? detectedIdeDisplay : 'IDE'} Context (ctrl+e to
- toggle)
- </Text>
- {openFiles.length > 0 && (
- <Box flexDirection="column" marginTop={1}>
- <Text bold>Open files:</Text>
- {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}>
- - {displayName}
- {file.isActive ? ' (active)' : ''}
- </Text>
- );
- })}
- </Box>
- )}
- </Box>
- );
-}