diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 0712d810..56093562 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -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', }); @@ -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 () => { @@ -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( { ); 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); @@ -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); @@ -364,7 +374,7 @@ 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 () => { @@ -372,6 +382,7 @@ describe('App UI', () => { workspace: { contextFileName: 'AGENTS.md', theme: 'Default' }, }); mockConfig.getGeminiMdFileCount.mockReturnValue(1); + mockConfig.getAllGeminiMdFilenames.mockReturnValue(['AGENTS.md']); mockConfig.getDebugMode.mockReturnValue(false); mockConfig.getShowMemoryUsage.mockReturnValue(false); @@ -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 () => { @@ -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); @@ -407,7 +422,7 @@ 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 () => { @@ -415,6 +430,11 @@ describe('App UI', () => { 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); @@ -427,7 +447,7 @@ 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 () => { @@ -435,6 +455,7 @@ describe('App UI', () => { workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' }, }); mockConfig.getGeminiMdFileCount.mockReturnValue(0); + mockConfig.getAllGeminiMdFilenames.mockReturnValue([]); mockConfig.getDebugMode.mockReturnValue(false); mockConfig.getShowMemoryUsage.mockReturnValue(false); @@ -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, }); @@ -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, @@ -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 () => { diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 566d6bd5..bd99f01b 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -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 { @@ -148,6 +149,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const [showErrorDetails, setShowErrorDetails] = useState(false); const [showToolDescriptions, setShowToolDescriptions] = useState(false); + const [showIDEContextDetail, setShowIDEContextDetail] = + useState(false); const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false); const [quittingMessages, setQuittingMessages] = useState< HistoryItem[] | null @@ -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')) { @@ -861,6 +866,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { } elapsedTime={elapsedTime} /> + { {shellModeActive && } - + {showIDEContextDetail && ( + + )} {showErrorDetails && ( diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx index 626a2fa5..b166056a 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx @@ -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; @@ -34,16 +33,17 @@ export const ContextSummaryDisplay: React.FC = ({ geminiMdFileCount === 0 && mcpServerCount === 0 && blockedMcpServerCount === 0 && - !openFiles?.activeFile + (openFiles?.recentOpenFiles?.length ?? 0) === 0 ) { return ; // 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)`; })(); const geminiMdText = (() => { @@ -51,8 +51,8 @@ export const ContextSummaryDisplay: React.FC = ({ 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' : '' }`; })(); @@ -65,14 +65,14 @@ export const ContextSummaryDisplay: React.FC = ({ 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); } @@ -81,8 +81,8 @@ export const ContextSummaryDisplay: React.FC = ({ let summaryText = 'Using: '; const summaryParts = []; - if (activeFileText) { - summaryParts.push(activeFileText); + if (recentFilesText) { + summaryParts.push(recentFilesText); } if (geminiMdText) { summaryParts.push(geminiMdText); diff --git a/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx b/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx new file mode 100644 index 00000000..8d4fb2c9 --- /dev/null +++ b/packages/cli/src/ui/components/IDEContextDetailDisplay.tsx @@ -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 ( + + + IDE Context (ctrl+e to toggle) + + {recentFiles.length > 0 && ( + + Recent files: + {recentFiles.map((file) => ( + + - {path.basename(file.filePath)} + {file.filePath === openFiles.activeFile ? ' (active)' : ''} + + ))} + + )} + + ); +} diff --git a/packages/vscode-ide-companion/src/ide-server.ts b/packages/vscode-ide-companion/src/ide-server.ts index aad86a08..3072029b 100644 --- a/packages/vscode-ide-companion/src/ide-server.ts +++ b/packages/vscode-ide-companion/src/ide-server.ts @@ -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}` }],