Display open IDE file in context section above input box rather than in the footer (#4470)
This commit is contained in:
parent
4915050ad4
commit
73745ecd03
|
@ -15,6 +15,7 @@ import {
|
||||||
AccessibilitySettings,
|
AccessibilitySettings,
|
||||||
SandboxConfig,
|
SandboxConfig,
|
||||||
GeminiClient,
|
GeminiClient,
|
||||||
|
ideContext,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
@ -146,11 +147,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
getIdeMode: vi.fn(() => false),
|
getIdeMode: vi.fn(() => false),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ideContextMock = {
|
||||||
|
getActiveFileContext: vi.fn(),
|
||||||
|
subscribeToActiveFile: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...actualCore,
|
...actualCore,
|
||||||
Config: ConfigClassMock,
|
Config: ConfigClassMock,
|
||||||
MCPServerConfig: actualCore.MCPServerConfig,
|
MCPServerConfig: actualCore.MCPServerConfig,
|
||||||
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
|
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.
|
// Ensure a theme is set so the theme dialog does not appear.
|
||||||
mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
|
mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
|
||||||
|
vi.mocked(ideContext.getActiveFileContext).mockReturnValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -267,6 +276,64 @@ describe('App UI', () => {
|
||||||
vi.clearAllMocks(); // Clear mocks after each test
|
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 () => {
|
it('should display default "GEMINI.md" in footer when contextFileName is not set and count is 1', async () => {
|
||||||
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
||||||
// For this test, ensure showMemoryUsage is false or debugMode is false if it relies on that
|
// 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;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve(); // Wait for any async updates
|
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 () => {
|
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;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve();
|
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 () => {
|
it('should display custom contextFileName in footer when set and count is 1', async () => {
|
||||||
|
@ -319,7 +386,7 @@ describe('App UI', () => {
|
||||||
);
|
);
|
||||||
currentUnmount = unmount;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve();
|
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 () => {
|
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;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve();
|
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 () => {
|
it('should display custom contextFileName with plural when set and count is > 1', async () => {
|
||||||
|
@ -362,7 +429,7 @@ describe('App UI', () => {
|
||||||
);
|
);
|
||||||
currentUnmount = unmount;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve();
|
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 () => {
|
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;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve();
|
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 () => {
|
it('should display only MCP server count when GEMINI.md count is 0', async () => {
|
||||||
|
@ -423,7 +490,7 @@ describe('App UI', () => {
|
||||||
);
|
);
|
||||||
currentUnmount = unmount;
|
currentUnmount = unmount;
|
||||||
await Promise.resolve();
|
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 () => {
|
it('should display Tips component by default', async () => {
|
||||||
|
|
|
@ -58,6 +58,8 @@ import {
|
||||||
FlashFallbackEvent,
|
FlashFallbackEvent,
|
||||||
logFlashFallback,
|
logFlashFallback,
|
||||||
AuthType,
|
AuthType,
|
||||||
|
type ActiveFile,
|
||||||
|
ideContext,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { validateAuthMethod } from '../config/auth.js';
|
import { validateAuthMethod } from '../config/auth.js';
|
||||||
import { useLogger } from './hooks/useLogger.js';
|
import { useLogger } from './hooks/useLogger.js';
|
||||||
|
@ -158,6 +160,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
|
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
|
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(() => {
|
const openPrivacyNotice = useCallback(() => {
|
||||||
setShowPrivacyNotice(true);
|
setShowPrivacyNotice(true);
|
||||||
|
@ -883,6 +893,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<ContextSummaryDisplay
|
<ContextSummaryDisplay
|
||||||
|
activeFile={activeFile}
|
||||||
geminiMdFileCount={geminiMdFileCount}
|
geminiMdFileCount={geminiMdFileCount}
|
||||||
contextFileNames={contextFileNames}
|
contextFileNames={contextFileNames}
|
||||||
mcpServers={config.getMcpServers()}
|
mcpServers={config.getMcpServers()}
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text } from 'ink';
|
import { Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
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 {
|
interface ContextSummaryDisplayProps {
|
||||||
geminiMdFileCount: number;
|
geminiMdFileCount: number;
|
||||||
|
@ -15,6 +16,7 @@ interface ContextSummaryDisplayProps {
|
||||||
mcpServers?: Record<string, MCPServerConfig>;
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
blockedMcpServers?: Array<{ name: string; extensionName: string }>;
|
blockedMcpServers?: Array<{ name: string; extensionName: string }>;
|
||||||
showToolDescriptions?: boolean;
|
showToolDescriptions?: boolean;
|
||||||
|
activeFile?: ActiveFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||||
|
@ -23,6 +25,7 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||||
mcpServers,
|
mcpServers,
|
||||||
blockedMcpServers,
|
blockedMcpServers,
|
||||||
showToolDescriptions,
|
showToolDescriptions,
|
||||||
|
activeFile,
|
||||||
}) => {
|
}) => {
|
||||||
const mcpServerCount = Object.keys(mcpServers || {}).length;
|
const mcpServerCount = Object.keys(mcpServers || {}).length;
|
||||||
const blockedMcpServerCount = blockedMcpServers?.length || 0;
|
const blockedMcpServerCount = blockedMcpServers?.length || 0;
|
||||||
|
@ -30,18 +33,26 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||||
if (
|
if (
|
||||||
geminiMdFileCount === 0 &&
|
geminiMdFileCount === 0 &&
|
||||||
mcpServerCount === 0 &&
|
mcpServerCount === 0 &&
|
||||||
blockedMcpServerCount === 0
|
blockedMcpServerCount === 0 &&
|
||||||
|
!activeFile?.filePath
|
||||||
) {
|
) {
|
||||||
return <Text> </Text>; // Render an empty space to reserve height
|
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 = (() => {
|
const geminiMdText = (() => {
|
||||||
if (geminiMdFileCount === 0) {
|
if (geminiMdFileCount === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const allNamesTheSame = new Set(contextFileNames).size < 2;
|
const allNamesTheSame = new Set(contextFileNames).size < 2;
|
||||||
const name = allNamesTheSame ? contextFileNames[0] : 'context';
|
const name = allNamesTheSame ? contextFileNames[0] : 'Context';
|
||||||
return `${geminiMdFileCount} ${name} file${
|
return `${geminiMdFileCount} ${name} File${
|
||||||
geminiMdFileCount > 1 ? 's' : ''
|
geminiMdFileCount > 1 ? 's' : ''
|
||||||
}`;
|
}`;
|
||||||
})();
|
})();
|
||||||
|
@ -54,36 +65,39 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (mcpServerCount > 0) {
|
if (mcpServerCount > 0) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`${mcpServerCount} MCP server${mcpServerCount > 1 ? 's' : ''}`,
|
`${mcpServerCount} MCP Server${mcpServerCount > 1 ? 's' : ''}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockedMcpServerCount > 0) {
|
if (blockedMcpServerCount > 0) {
|
||||||
let blockedText = `${blockedMcpServerCount} blocked`;
|
let blockedText = `${blockedMcpServerCount} Blocked`;
|
||||||
if (mcpServerCount === 0) {
|
if (mcpServerCount === 0) {
|
||||||
blockedText += ` MCP server${blockedMcpServerCount > 1 ? 's' : ''}`;
|
blockedText += ` MCP Server${blockedMcpServerCount > 1 ? 's' : ''}`;
|
||||||
}
|
}
|
||||||
parts.push(blockedText);
|
parts.push(blockedText);
|
||||||
}
|
}
|
||||||
return parts.join(', ');
|
return parts.join(', ');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
let summaryText = 'Using ';
|
let summaryText = 'Using: ';
|
||||||
if (geminiMdText) {
|
const summaryParts = [];
|
||||||
summaryText += geminiMdText;
|
if (activeFileText) {
|
||||||
|
summaryParts.push(activeFileText);
|
||||||
}
|
}
|
||||||
if (geminiMdText && mcpText) {
|
if (geminiMdText) {
|
||||||
summaryText += ' and ';
|
summaryParts.push(geminiMdText);
|
||||||
}
|
}
|
||||||
if (mcpText) {
|
if (mcpText) {
|
||||||
summaryText += mcpText;
|
summaryParts.push(mcpText);
|
||||||
// Add ctrl+t hint when MCP servers are available
|
}
|
||||||
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
summaryText += summaryParts.join(' | ');
|
||||||
if (showToolDescriptions) {
|
|
||||||
summaryText += ' (ctrl+t to toggle)';
|
// Add ctrl+t hint when MCP servers are available
|
||||||
} else {
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
||||||
summaryText += ' (ctrl+t to view)';
|
if (showToolDescriptions) {
|
||||||
}
|
summaryText += ' (ctrl+t to toggle)';
|
||||||
|
} else {
|
||||||
|
summaryText += ' (ctrl+t to view)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,10 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import {
|
import { shortenPath, tildeifyPath, tokenLimit } from '@google/gemini-cli-core';
|
||||||
shortenPath,
|
|
||||||
tildeifyPath,
|
|
||||||
tokenLimit,
|
|
||||||
ideContext,
|
|
||||||
ActiveFile,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import Gradient from 'ink-gradient';
|
import Gradient from 'ink-gradient';
|
||||||
|
@ -49,24 +43,6 @@ export const Footer: React.FC<FooterProps> = ({
|
||||||
const limit = tokenLimit(model);
|
const limit = tokenLimit(model);
|
||||||
const percentage = promptTokenCount / limit;
|
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 (
|
return (
|
||||||
<Box marginTop={1} justifyContent="space-between" width="100%">
|
<Box marginTop={1} justifyContent="space-between" width="100%">
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -83,19 +59,6 @@ export const Footer: React.FC<FooterProps> = ({
|
||||||
{branchName && <Text color={Colors.Gray}> ({branchName}*)</Text>}
|
{branchName && <Text color={Colors.Gray}> ({branchName}*)</Text>}
|
||||||
</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 && (
|
{debugMode && (
|
||||||
<Text color={Colors.AccentRed}>
|
<Text color={Colors.AccentRed}>
|
||||||
{' ' + (debugMessage || '--debug')}
|
{' ' + (debugMessage || '--debug')}
|
||||||
|
|
Loading…
Reference in New Issue