diff options
| author | Shreya Keshive <[email protected]> | 2025-07-18 18:14:46 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-18 22:14:46 +0000 |
| commit | 73745ecd0323882fc951e387250fe2efef374e81 (patch) | |
| tree | ab899051fb0d900d34a71c974410b466ff652bb8 /packages/cli/src | |
| parent | 4915050ad47236a6d8349ed87b68cd202f96efbe (diff) | |
Display open IDE file in context section above input box rather than in the footer (#4470)
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/ui/App.test.tsx | 81 | ||||
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 11 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/ContextSummaryDisplay.tsx | 54 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/Footer.tsx | 41 |
4 files changed, 121 insertions, 66 deletions
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index ed4418e9..e03c80ae 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -15,6 +15,7 @@ import { AccessibilitySettings, SandboxConfig, GeminiClient, + ideContext, } from '@google/gemini-cli-core'; import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js'; import process from 'node:process'; @@ -146,11 +147,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { getIdeMode: vi.fn(() => false), }; }); + + const ideContextMock = { + getActiveFileContext: vi.fn(), + subscribeToActiveFile: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function + }; + return { ...actualCore, Config: ConfigClassMock, MCPServerConfig: actualCore.MCPServerConfig, getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']), + ideContext: ideContextMock, }; }); @@ -257,6 +265,7 @@ describe('App UI', () => { // Ensure a theme is set so the theme dialog does not appear. mockSettings = createMockSettings({ workspace: { theme: 'Default' } }); + vi.mocked(ideContext.getActiveFileContext).mockReturnValue(undefined); }); afterEach(() => { @@ -267,6 +276,64 @@ describe('App UI', () => { vi.clearAllMocks(); // Clear mocks after each test }); + it('should display active file when available', async () => { + vi.mocked(ideContext.getActiveFileContext).mockReturnValue({ + filePath: '/path/to/my-file.ts', + content: 'const a = 1;', + cursor: 0, + }); + + const { lastFrame, unmount } = render( + <App + config={mockConfig as unknown as ServerConfig} + settings={mockSettings} + version={mockVersion} + />, + ); + currentUnmount = unmount; + await Promise.resolve(); + expect(lastFrame()).toContain('Open File (my-file.ts)'); + }); + + it('should not display active file when not available', async () => { + vi.mocked(ideContext.getActiveFileContext).mockReturnValue({ + filePath: '', + content: '', + cursor: 0, + }); + + const { lastFrame, unmount } = render( + <App + config={mockConfig as unknown as ServerConfig} + settings={mockSettings} + version={mockVersion} + />, + ); + currentUnmount = unmount; + await Promise.resolve(); + expect(lastFrame()).not.toContain('Open File'); + }); + + it('should display active file and other context', async () => { + vi.mocked(ideContext.getActiveFileContext).mockReturnValue({ + filePath: '/path/to/my-file.ts', + content: 'const a = 1;', + cursor: 0, + }); + mockConfig.getGeminiMdFileCount.mockReturnValue(1); + + const { lastFrame, unmount } = render( + <App + config={mockConfig as unknown as ServerConfig} + settings={mockSettings} + version={mockVersion} + />, + ); + currentUnmount = unmount; + await Promise.resolve(); + expect(lastFrame()).toContain('Open File (my-file.ts) | 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); // For this test, ensure showMemoryUsage is false or debugMode is false if it relies on that @@ -282,7 +349,7 @@ 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 () => { @@ -299,7 +366,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 () => { @@ -319,7 +386,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 () => { @@ -342,7 +409,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 () => { @@ -362,7 +429,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 () => { @@ -402,7 +469,7 @@ describe('App UI', () => { ); currentUnmount = unmount; await Promise.resolve(); - expect(lastFrame()).toContain('server'); + expect(lastFrame()).toContain('1 MCP Server'); }); it('should display only MCP server count when GEMINI.md count is 0', async () => { @@ -423,7 +490,7 @@ describe('App UI', () => { ); currentUnmount = unmount; await Promise.resolve(); - expect(lastFrame()).toContain('Using 2 MCP servers'); + expect(lastFrame()).toContain('Using: 2 MCP Servers'); }); 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 782e2ff8..39a1f14c 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -58,6 +58,8 @@ import { FlashFallbackEvent, logFlashFallback, AuthType, + type ActiveFile, + ideContext, } from '@google/gemini-cli-core'; import { validateAuthMethod } from '../config/auth.js'; import { useLogger } from './hooks/useLogger.js'; @@ -158,6 +160,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] = useState<boolean>(false); const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined); + const [activeFile, setActiveFile] = useState<ActiveFile | undefined>(); + + useEffect(() => { + const unsubscribe = ideContext.subscribeToActiveFile(setActiveFile); + // Set the initial value + setActiveFile(ideContext.getActiveFileContext()); + return unsubscribe; + }, []); const openPrivacyNotice = useCallback(() => { setShowPrivacyNotice(true); @@ -883,6 +893,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { </Text> ) : ( <ContextSummaryDisplay + activeFile={activeFile} geminiMdFileCount={geminiMdFileCount} contextFileNames={contextFileNames} mcpServers={config.getMcpServers()} diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx index 314e8ebd..d1ef8135 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx @@ -7,7 +7,8 @@ import React from 'react'; import { Text } from 'ink'; import { Colors } from '../colors.js'; -import { type MCPServerConfig } from '@google/gemini-cli-core'; +import { type ActiveFile, type MCPServerConfig } from '@google/gemini-cli-core'; +import path from 'path'; interface ContextSummaryDisplayProps { geminiMdFileCount: number; @@ -15,6 +16,7 @@ interface ContextSummaryDisplayProps { mcpServers?: Record<string, MCPServerConfig>; blockedMcpServers?: Array<{ name: string; extensionName: string }>; showToolDescriptions?: boolean; + activeFile?: ActiveFile; } export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({ @@ -23,6 +25,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({ mcpServers, blockedMcpServers, showToolDescriptions, + activeFile, }) => { const mcpServerCount = Object.keys(mcpServers || {}).length; const blockedMcpServerCount = blockedMcpServers?.length || 0; @@ -30,18 +33,26 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({ if ( geminiMdFileCount === 0 && mcpServerCount === 0 && - blockedMcpServerCount === 0 + blockedMcpServerCount === 0 && + !activeFile?.filePath ) { return <Text> </Text>; // Render an empty space to reserve height } + const activeFileText = (() => { + if (!activeFile?.filePath) { + return ''; + } + return `Open File (${path.basename(activeFile.filePath)})`; + })(); + 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' : '' }`; })(); @@ -54,36 +65,39 @@ 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`; + let blockedText = `${blockedMcpServerCount} Blocked`; if (mcpServerCount === 0) { - blockedText += ` MCP server${blockedMcpServerCount > 1 ? 's' : ''}`; + blockedText += ` MCP Server${blockedMcpServerCount > 1 ? 's' : ''}`; } parts.push(blockedText); } return parts.join(', '); })(); - let summaryText = 'Using '; - if (geminiMdText) { - summaryText += geminiMdText; + let summaryText = 'Using: '; + const summaryParts = []; + if (activeFileText) { + summaryParts.push(activeFileText); } - if (geminiMdText && mcpText) { - summaryText += ' and '; + if (geminiMdText) { + summaryParts.push(geminiMdText); } if (mcpText) { - summaryText += mcpText; - // Add ctrl+t hint when MCP servers are available - if (mcpServers && Object.keys(mcpServers).length > 0) { - if (showToolDescriptions) { - summaryText += ' (ctrl+t to toggle)'; - } else { - summaryText += ' (ctrl+t to view)'; - } + summaryParts.push(mcpText); + } + summaryText += summaryParts.join(' | '); + + // Add ctrl+t hint when MCP servers are available + if (mcpServers && Object.keys(mcpServers).length > 0) { + if (showToolDescriptions) { + summaryText += ' (ctrl+t to toggle)'; + } else { + summaryText += ' (ctrl+t to view)'; } } diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 5524114b..95904cd9 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -4,16 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; -import { - shortenPath, - tildeifyPath, - tokenLimit, - ideContext, - ActiveFile, -} from '@google/gemini-cli-core'; +import { shortenPath, tildeifyPath, tokenLimit } from '@google/gemini-cli-core'; import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js'; import process from 'node:process'; import Gradient from 'ink-gradient'; @@ -49,24 +43,6 @@ export const Footer: React.FC<FooterProps> = ({ const limit = tokenLimit(model); const percentage = promptTokenCount / limit; - const [activeFile, setActiveFile] = useState<ActiveFile | undefined>( - undefined, - ); - - useEffect(() => { - const updateActiveFile = () => { - const currentActiveFile = ideContext.getActiveFileContext(); - setActiveFile(currentActiveFile); - }; - - updateActiveFile(); - - const unsubscribe = ideContext.subscribeToActiveFile(setActiveFile); - return () => { - unsubscribe(); - }; - }, []); - return ( <Box marginTop={1} justifyContent="space-between" width="100%"> <Box> @@ -83,19 +59,6 @@ export const Footer: React.FC<FooterProps> = ({ {branchName && <Text color={Colors.Gray}> ({branchName}*)</Text>} </Text> )} - {activeFile && activeFile.filePath && ( - <Text> - <Text color={Colors.Gray}> | </Text> - <Text color={Colors.LightBlue}> - {shortenPath(tildeifyPath(activeFile.filePath), 70)} - </Text> - {activeFile.cursor && ( - <Text color={Colors.Gray}> - :{activeFile.cursor.line}:{activeFile.cursor.character} - </Text> - )} - </Text> - )} {debugMode && ( <Text color={Colors.AccentRed}> {' ' + (debugMessage || '--debug')} |
