Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions packages/cli/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ describe('App UI', () => {
it('should display active file when available', async () => {
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
activeFile: '/path/to/my-file.ts',
recentOpenFiles: [{ filePath: '/path/to/my-file.ts', content: 'hello' }],
selectedText: 'hello',
});

Expand All @@ -293,7 +294,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Open File (my-file.ts)');
expect(lastFrame()).toContain('1 recent file (ctrl+e to view)');
});

it('should not display active file when not available', async () => {
Expand All @@ -316,9 +317,11 @@ describe('App UI', () => {
it('should display active file and other context', async () => {
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
activeFile: '/path/to/my-file.ts',
recentOpenFiles: [{ filePath: '/path/to/my-file.ts', content: 'hello' }],
selectedText: 'hello',
});
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);

const { lastFrame, unmount } = render(
<App
Expand All @@ -329,11 +332,14 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Open File (my-file.ts) | 1 GEMINI.md File');
expect(lastFrame()).toContain(
'Using: 1 recent file (ctrl+e to view) | 1 GEMINI.md file',
);
});

it('should display default "GEMINI.md" in footer when contextFileName is not set and count is 1', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
// For this test, ensure showMemoryUsage is false or debugMode is false if it relies on that
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
Expand All @@ -347,11 +353,15 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve(); // Wait for any async updates
expect(lastFrame()).toContain('Using: 1 GEMINI.md File');
expect(lastFrame()).toContain('Using: 1 GEMINI.md file');
});

it('should display default "GEMINI.md" with plural when contextFileName is not set and count is > 1', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'GEMINI.md',
'GEMINI.md',
]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);

Expand All @@ -364,14 +374,15 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using: 2 GEMINI.md Files');
expect(lastFrame()).toContain('Using: 2 GEMINI.md files');
});

it('should display custom contextFileName in footer when set and count is 1', async () => {
mockSettings = createMockSettings({
workspace: { contextFileName: 'AGENTS.md', theme: 'Default' },
});
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['AGENTS.md']);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);

Expand All @@ -384,7 +395,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using: 1 AGENTS.md File');
expect(lastFrame()).toContain('Using: 1 AGENTS.md file');
});

it('should display a generic message when multiple context files with different names are provided', async () => {
Expand All @@ -395,6 +406,10 @@ describe('App UI', () => {
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'AGENTS.md',
'CONTEXT.md',
]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);

Expand All @@ -407,14 +422,19 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using: 2 Context Files');
expect(lastFrame()).toContain('Using: 2 context files');
});

it('should display custom contextFileName with plural when set and count is > 1', async () => {
mockSettings = createMockSettings({
workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' },
});
mockConfig.getGeminiMdFileCount.mockReturnValue(3);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'MY_NOTES.TXT',
'MY_NOTES.TXT',
'MY_NOTES.TXT',
]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);

Expand All @@ -427,14 +447,15 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using: 3 MY_NOTES.TXT Files');
expect(lastFrame()).toContain('Using: 3 MY_NOTES.TXT files');
});

it('should not display context file message if count is 0, even if contextFileName is set', async () => {
mockSettings = createMockSettings({
workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' },
});
mockConfig.getGeminiMdFileCount.mockReturnValue(0);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);

Expand All @@ -452,6 +473,10 @@ describe('App UI', () => {

it('should display GEMINI.md and MCP server count when both are present', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'GEMINI.md',
'GEMINI.md',
]);
mockConfig.getMcpServers.mockReturnValue({
server1: {} as MCPServerConfig,
});
Expand All @@ -467,11 +492,12 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('1 MCP Server');
expect(lastFrame()).toContain('1 MCP server');
});

it('should display only MCP server count when GEMINI.md count is 0', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(0);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([]);
mockConfig.getMcpServers.mockReturnValue({
server1: {} as MCPServerConfig,
server2: {} as MCPServerConfig,
Expand All @@ -488,7 +514,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using: 2 MCP Servers');
expect(lastFrame()).toContain('Using: 2 MCP servers (ctrl+t to view)');
});

it('should display Tips component by default', async () => {
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ 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 {
Expand Down Expand Up @@ -148,6 +149,8 @@ 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
Expand Down Expand Up @@ -465,6 +468,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (Object.keys(mcpServers || {}).length > 0) {
handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc');
}
} else if (key.ctrl && input === 'e' && ideContext) {
setShowIDEContextDetail((prev) => !prev);
} else if (key.ctrl && (input === 'c' || input === 'C')) {
handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef);
} else if (key.ctrl && (input === 'd' || input === 'D')) {
Comment thread
chrstnb marked this conversation as resolved.
Expand Down Expand Up @@ -861,6 +866,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}
elapsedTime={elapsedTime}
/>

<Box
marginTop={1}
display="flex"
Expand Down Expand Up @@ -900,7 +906,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{shellModeActive && <ShellModeIndicator />}
</Box>
</Box>

{showIDEContextDetail && (
<IDEContextDetailDisplay openFiles={openFiles} />
)}
{showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
Expand Down
22 changes: 11 additions & 11 deletions packages/cli/src/ui/components/ContextSummaryDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import React from 'react';
import { Text } from 'ink';
import { Colors } from '../colors.js';
import { type OpenFiles, type MCPServerConfig } from '@google/gemini-cli-core';
import path from 'path';

interface ContextSummaryDisplayProps {
geminiMdFileCount: number;
Expand All @@ -34,25 +33,26 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
geminiMdFileCount === 0 &&
mcpServerCount === 0 &&
blockedMcpServerCount === 0 &&
!openFiles?.activeFile
(openFiles?.recentOpenFiles?.length ?? 0) === 0
) {
return <Text> </Text>; // Render an empty space to reserve height
}

const activeFileText = (() => {
if (!openFiles?.activeFile) {
const recentFilesText = (() => {
const count = openFiles?.recentOpenFiles?.length ?? 0;
if (count === 0) {
return '';
}
return `Open File (${path.basename(openFiles.activeFile)})`;
return `${count} recent file${count > 1 ? 's' : ''} (ctrl+e to view)`;
Comment thread
chrstnb marked this conversation as resolved.
})();

const geminiMdText = (() => {
if (geminiMdFileCount === 0) {
return '';
}
const allNamesTheSame = new Set(contextFileNames).size < 2;
const name = allNamesTheSame ? contextFileNames[0] : 'Context';
return `${geminiMdFileCount} ${name} File${
const name = allNamesTheSame ? contextFileNames[0] : 'context';
return `${geminiMdFileCount} ${name} file${
geminiMdFileCount > 1 ? 's' : ''
}`;
})();
Expand All @@ -65,14 +65,14 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
const parts = [];
if (mcpServerCount > 0) {
parts.push(
`${mcpServerCount} MCP Server${mcpServerCount > 1 ? 's' : ''}`,
`${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}`,
);
}

if (blockedMcpServerCount > 0) {
let blockedText = `${blockedMcpServerCount} Blocked`;
if (mcpServerCount === 0) {
blockedText += ` MCP Server${blockedMcpServerCount > 1 ? 's' : ''}`;
blockedText += ` MCP server${blockedMcpServerCount > 1 ? 's' : ''}`;
}
parts.push(blockedText);
}
Expand All @@ -81,8 +81,8 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({

let summaryText = 'Using: ';
const summaryParts = [];
if (activeFileText) {
summaryParts.push(activeFileText);
if (recentFilesText) {
summaryParts.push(recentFilesText);
}
if (geminiMdText) {
summaryParts.push(geminiMdText);
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/src/ui/components/IDEContextDetailDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { Box, Text } from 'ink';
import { type OpenFiles } from '@google/gemini-cli-core';
import { Colors } from '../colors.js';
import path from 'node:path';

interface IDEContextDetailDisplayProps {
openFiles: OpenFiles | undefined;
}

export function IDEContextDetailDisplay({
openFiles,
}: IDEContextDetailDisplayProps) {
if (
!openFiles ||
!openFiles.recentOpenFiles ||
openFiles.recentOpenFiles.length === 0
) {
return null;
}
const recentFiles = openFiles.recentOpenFiles || [];

return (
<Box
flexDirection="column"
marginTop={1}
borderStyle="round"
borderColor={Colors.AccentCyan}
paddingX={1}
>
<Text color={Colors.AccentCyan} bold>
IDE Context (ctrl+e to toggle)
Comment thread
chrstnb marked this conversation as resolved.
</Text>
{recentFiles.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text bold>Recent files:</Text>
{recentFiles.map((file) => (
<Text key={file.filePath}>
- {path.basename(file.filePath)}
{file.filePath === openFiles.activeFile ? ' (active)' : ''}
</Text>
))}
</Box>
)}
</Box>
);
}
7 changes: 5 additions & 2 deletions packages/vscode-ide-companion/src/ide-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,11 @@ const createMcpServer = () => {
inputSchema: {},
},
async () => {
const activeEditor = vscode.window.activeTextEditor;
const filePath = activeEditor ? activeEditor.document.uri.fsPath : '';
const editor = vscode.window.activeTextEditor;
const filePath =
editor && editor.document.uri.scheme === 'file'
? editor.document.uri.fsPath
: '';
if (filePath) {
return {
content: [{ type: 'text', text: `Active file: ${filePath}` }],
Expand Down