mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-19 18:40:57 -07:00
feat(cli): invert context window display to show usage (#20071)
Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
@@ -17,18 +17,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
DEFAULT_MODEL_CONFIGS: {},
|
||||
LoadedSettings: class {
|
||||
constructor() {
|
||||
// this.merged = {};
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ContextUsageDisplay', () => {
|
||||
it('renders correct percentage left', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
it('renders correct percentage used', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={5000}
|
||||
model="gemini-pro"
|
||||
@@ -37,27 +28,56 @@ describe('ContextUsageDisplay', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('50% context left');
|
||||
expect(output).toContain('50% context used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders short label when terminal width is small', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
it('renders correctly when usage is 0%', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={0}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('0% context used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders abbreviated label when terminal width is small', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={2000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('80%');
|
||||
expect(output).not.toContain('context left');
|
||||
expect(output).toContain('20%');
|
||||
expect(output).not.toContain('context used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders 0% when full', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
it('renders 80% correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={8000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('80% context used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders 100% when full', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={10000}
|
||||
model="gemini-pro"
|
||||
@@ -66,7 +86,7 @@ describe('ContextUsageDisplay', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('0% context left');
|
||||
expect(output).toContain('100% context used');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
import { Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { getContextUsagePercentage } from '../utils/contextUsage.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import {
|
||||
MIN_TERMINAL_WIDTH_FOR_FULL_LABEL,
|
||||
DEFAULT_COMPRESSION_THRESHOLD,
|
||||
} from '../constants.js';
|
||||
|
||||
export const ContextUsageDisplay = ({
|
||||
promptTokenCount,
|
||||
@@ -14,17 +19,30 @@ export const ContextUsageDisplay = ({
|
||||
terminalWidth,
|
||||
}: {
|
||||
promptTokenCount: number;
|
||||
model: string;
|
||||
model: string | undefined;
|
||||
terminalWidth: number;
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const percentage = getContextUsagePercentage(promptTokenCount, model);
|
||||
const percentageLeft = ((1 - percentage) * 100).toFixed(0);
|
||||
const percentageUsed = (percentage * 100).toFixed(0);
|
||||
|
||||
const label = terminalWidth < 100 ? '%' : '% context left';
|
||||
const threshold =
|
||||
settings.merged.model?.compressionThreshold ??
|
||||
DEFAULT_COMPRESSION_THRESHOLD;
|
||||
|
||||
let textColor = theme.text.secondary;
|
||||
if (percentage >= 1.0) {
|
||||
textColor = theme.status.error;
|
||||
} else if (percentage >= threshold) {
|
||||
textColor = theme.status.warning;
|
||||
}
|
||||
|
||||
const label =
|
||||
terminalWidth < MIN_TERMINAL_WIDTH_FOR_FULL_LABEL ? '%' : '% context used';
|
||||
|
||||
return (
|
||||
<Text color={theme.text.secondary}>
|
||||
{percentageLeft}
|
||||
<Text color={textColor}>
|
||||
{percentageUsed}
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('<Footer />', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\d+% context left/);
|
||||
expect(lastFrame()).toMatch(/\d+% context used/);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -229,7 +229,7 @@ describe('<Footer />', () => {
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('Usage remaining');
|
||||
expect(lastFrame()).not.toContain('used');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -262,7 +262,7 @@ describe('<Footer />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays the model name and abbreviated context percentage', async () => {
|
||||
it('displays the model name and abbreviated context used label on narrow terminals', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
@@ -280,6 +280,7 @@ describe('<Footer />', () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\d+%/);
|
||||
expect(lastFrame()).not.toContain('context used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -477,7 +478,7 @@ describe('<Footer />', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).not.toMatch(/\d+% context left/);
|
||||
expect(lastFrame()).not.toMatch(/\d+% context used/);
|
||||
unmount();
|
||||
});
|
||||
it('shows the context percentage when hideContextPercentage is false', async () => {
|
||||
@@ -497,7 +498,7 @@ describe('<Footer />', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\d+% context left/);
|
||||
expect(lastFrame()).toMatch(/\d+% context used/);
|
||||
unmount();
|
||||
});
|
||||
it('renders complete footer in narrow terminal (baseline narrow)', async () => {
|
||||
|
||||
@@ -99,6 +99,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'info' && (
|
||||
<InfoMessage
|
||||
text={itemForDisplay.text}
|
||||
secondaryText={itemForDisplay.secondaryText}
|
||||
icon={itemForDisplay.icon}
|
||||
color={itemForDisplay.color}
|
||||
marginBottom={itemForDisplay.marginBottom}
|
||||
|
||||
@@ -89,11 +89,12 @@ const renderStatusDisplay = async (
|
||||
};
|
||||
|
||||
describe('StatusDisplay', () => {
|
||||
const originalEnv = process.env;
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('GEMINI_SYSTEM_MD', '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env['GEMINI_SYSTEM_MD'];
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -112,7 +113,7 @@ describe('StatusDisplay', () => {
|
||||
});
|
||||
|
||||
it('renders system md indicator if env var is set', async () => {
|
||||
process.env['GEMINI_SYSTEM_MD'] = 'true';
|
||||
vi.stubEnv('GEMINI_SYSTEM_MD', 'true');
|
||||
const { lastFrame, unmount } = await renderStatusDisplay();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
|
||||
@@ -11,12 +11,12 @@ exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
|
||||
" ...s/to/make/it/long no sandbox /model gemini-pro 100%
|
||||
" ...s/to/make/it/long no sandbox /model gemini-pro 0%
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
|
||||
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 100% context left
|
||||
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 0% context used
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
|
||||
interface InfoMessageProps {
|
||||
text: string;
|
||||
secondaryText?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
marginBottom?: number;
|
||||
@@ -18,6 +19,7 @@ interface InfoMessageProps {
|
||||
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
text,
|
||||
secondaryText,
|
||||
icon,
|
||||
color,
|
||||
marginBottom,
|
||||
@@ -35,6 +37,9 @@ export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
{text.split('\n').map((line, index) => (
|
||||
<Text wrap="wrap" key={index}>
|
||||
<RenderInline text={line} defaultColor={color} />
|
||||
{index === text.split('\n').length - 1 && secondaryText && (
|
||||
<Text color={theme.text.secondary}> {secondaryText}</Text>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user