Skip to content

Commit ee7f68e

Browse files
keithguerinjacob314
authored andcommitted
feat(cli): invert context window display to show usage (google-gemini#20071)
Co-authored-by: jacob314 <jacob314@gmail.com>
1 parent 8f14fe2 commit ee7f68e

File tree

19 files changed

+235
-68
lines changed

19 files changed

+235
-68
lines changed

docs/changelogs/index.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,9 @@ on GitHub.
464464
page in their default browser directly from the CLI using the `/extension`
465465
explore command. ([pr](https://github.com/google-gemini/gemini-cli/pull/11846)
466466
by [@JayadityaGit](https://github.com/JayadityaGit)).
467-
- **Configurable compression:** Users can modify the compression threshold in
468-
`/settings`. The default has been made more proactive
467+
- **Configurable compression:** Users can modify the context compression
468+
threshold in `/settings` (decimal with percentage display). The default has
469+
been made more proactive
469470
([pr](https://github.com/google-gemini/gemini-cli/pull/12317) by
470471
[@scidomino](https://github.com/scidomino)).
471472
- **API key authentication:** Users can now securely enter and store their

docs/cli/settings.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ they appear in the UI.
6060
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
6161
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
6262
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
63-
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
63+
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
6464
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
6565
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
6666
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
@@ -89,13 +89,13 @@ they appear in the UI.
8989

9090
### Model
9191

92-
| UI Label | Setting | Description | Default |
93-
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
94-
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
95-
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
96-
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
97-
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
98-
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
92+
| UI Label | Setting | Description | Default |
93+
| ----------------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
94+
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
95+
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
96+
| Context Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
97+
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
98+
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
9999

100100
### Context
101101

docs/reference/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ their corresponding top-level category object in your `settings.json` file.
263263
- **Default:** `false`
264264

265265
- **`ui.footer.hideContextPercentage`** (boolean):
266-
- **Description:** Hides the context window remaining percentage.
266+
- **Description:** Hides the context window usage percentage.
267267
- **Default:** `true`
268268

269269
- **`ui.hideFooter`** (boolean):

packages/cli/src/config/settingsSchema.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export interface SettingDefinition {
117117
* For map-like objects without explicit `properties`, describes the shape of the values.
118118
*/
119119
additionalProperties?: SettingCollectionDefinition;
120+
/**
121+
* Optional unit to display after the value (e.g. '%').
122+
*/
123+
unit?: string;
120124
/**
121125
* Optional reference identifier for generators that emit a `$ref`.
122126
*/
@@ -595,7 +599,7 @@ const SETTINGS_SCHEMA = {
595599
category: 'UI',
596600
requiresRestart: false,
597601
default: true,
598-
description: 'Hides the context window remaining percentage.',
602+
description: 'Hides the context window usage percentage.',
599603
showInDialog: true,
600604
},
601605
},
@@ -913,13 +917,14 @@ const SETTINGS_SCHEMA = {
913917
},
914918
compressionThreshold: {
915919
type: 'number',
916-
label: 'Compression Threshold',
920+
label: 'Context Compression Threshold',
917921
category: 'Model',
918922
requiresRestart: true,
919923
default: 0.5 as number,
920924
description:
921925
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
922926
showInDialog: true,
927+
unit: '%',
923928
},
924929
disableLoopDetection: {
925930
type: 'boolean',

packages/cli/src/ui/components/ContextUsageDisplay.test.tsx

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { render } from '../../test-utils/render.js';
7+
import { renderWithProviders } from '../../test-utils/render.js';
88
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
99
import { describe, it, expect, vi } from 'vitest';
1010

@@ -17,18 +17,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
1717
};
1818
});
1919

20-
vi.mock('../../config/settings.js', () => ({
21-
DEFAULT_MODEL_CONFIGS: {},
22-
LoadedSettings: class {
23-
constructor() {
24-
// this.merged = {};
25-
}
26-
},
27-
}));
28-
2920
describe('ContextUsageDisplay', () => {
30-
it('renders correct percentage left', async () => {
31-
const { lastFrame, waitUntilReady, unmount } = render(
21+
it('renders correct percentage used', async () => {
22+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
3223
<ContextUsageDisplay
3324
promptTokenCount={5000}
3425
model="gemini-pro"
@@ -37,27 +28,56 @@ describe('ContextUsageDisplay', () => {
3728
);
3829
await waitUntilReady();
3930
const output = lastFrame();
40-
expect(output).toContain('50% context left');
31+
expect(output).toContain('50% context used');
32+
unmount();
33+
});
34+
35+
it('renders correctly when usage is 0%', async () => {
36+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
37+
<ContextUsageDisplay
38+
promptTokenCount={0}
39+
model="gemini-pro"
40+
terminalWidth={120}
41+
/>,
42+
);
43+
await waitUntilReady();
44+
const output = lastFrame();
45+
expect(output).toContain('0% context used');
4146
unmount();
4247
});
4348

44-
it('renders short label when terminal width is small', async () => {
45-
const { lastFrame, waitUntilReady, unmount } = render(
49+
it('renders abbreviated label when terminal width is small', async () => {
50+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
4651
<ContextUsageDisplay
4752
promptTokenCount={2000}
4853
model="gemini-pro"
4954
terminalWidth={80}
5055
/>,
56+
{ width: 80 },
57+
);
58+
await waitUntilReady();
59+
const output = lastFrame();
60+
expect(output).toContain('20%');
61+
expect(output).not.toContain('context used');
62+
unmount();
63+
});
64+
65+
it('renders 80% correctly', async () => {
66+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
67+
<ContextUsageDisplay
68+
promptTokenCount={8000}
69+
model="gemini-pro"
70+
terminalWidth={120}
71+
/>,
5172
);
5273
await waitUntilReady();
5374
const output = lastFrame();
54-
expect(output).toContain('80%');
55-
expect(output).not.toContain('context left');
75+
expect(output).toContain('80% context used');
5676
unmount();
5777
});
5878

59-
it('renders 0% when full', async () => {
60-
const { lastFrame, waitUntilReady, unmount } = render(
79+
it('renders 100% when full', async () => {
80+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
6181
<ContextUsageDisplay
6282
promptTokenCount={10000}
6383
model="gemini-pro"
@@ -66,7 +86,7 @@ describe('ContextUsageDisplay', () => {
6686
);
6787
await waitUntilReady();
6888
const output = lastFrame();
69-
expect(output).toContain('0% context left');
89+
expect(output).toContain('100% context used');
7090
unmount();
7191
});
7292
});

packages/cli/src/ui/components/ContextUsageDisplay.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,42 @@
77
import { Text } from 'ink';
88
import { theme } from '../semantic-colors.js';
99
import { getContextUsagePercentage } from '../utils/contextUsage.js';
10+
import { useSettings } from '../contexts/SettingsContext.js';
11+
import {
12+
MIN_TERMINAL_WIDTH_FOR_FULL_LABEL,
13+
DEFAULT_COMPRESSION_THRESHOLD,
14+
} from '../constants.js';
1015

1116
export const ContextUsageDisplay = ({
1217
promptTokenCount,
1318
model,
1419
terminalWidth,
1520
}: {
1621
promptTokenCount: number;
17-
model: string;
22+
model: string | undefined;
1823
terminalWidth: number;
1924
}) => {
25+
const settings = useSettings();
2026
const percentage = getContextUsagePercentage(promptTokenCount, model);
21-
const percentageLeft = ((1 - percentage) * 100).toFixed(0);
27+
const percentageUsed = (percentage * 100).toFixed(0);
2228

23-
const label = terminalWidth < 100 ? '%' : '% context left';
29+
const threshold =
30+
settings.merged.model?.compressionThreshold ??
31+
DEFAULT_COMPRESSION_THRESHOLD;
32+
33+
let textColor = theme.text.secondary;
34+
if (percentage >= 1.0) {
35+
textColor = theme.status.error;
36+
} else if (percentage >= threshold) {
37+
textColor = theme.status.warning;
38+
}
39+
40+
const label =
41+
terminalWidth < MIN_TERMINAL_WIDTH_FOR_FULL_LABEL ? '%' : '% context used';
2442

2543
return (
26-
<Text color={theme.text.secondary}>
27-
{percentageLeft}
44+
<Text color={textColor}>
45+
{percentageUsed}
2846
{label}
2947
</Text>
3048
);

packages/cli/src/ui/components/Footer.test.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe('<Footer />', () => {
174174
);
175175
await waitUntilReady();
176176
expect(lastFrame()).toContain(defaultProps.model);
177-
expect(lastFrame()).toMatch(/\d+% context left/);
177+
expect(lastFrame()).toMatch(/\d+% context used/);
178178
unmount();
179179
});
180180

@@ -229,7 +229,7 @@ describe('<Footer />', () => {
229229
},
230230
);
231231
await waitUntilReady();
232-
expect(lastFrame()).not.toContain('Usage remaining');
232+
expect(lastFrame()).not.toContain('used');
233233
expect(lastFrame()).toMatchSnapshot();
234234
unmount();
235235
});
@@ -262,7 +262,7 @@ describe('<Footer />', () => {
262262
unmount();
263263
});
264264

265-
it('displays the model name and abbreviated context percentage', async () => {
265+
it('displays the model name and abbreviated context used label on narrow terminals', async () => {
266266
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
267267
<Footer />,
268268
{
@@ -280,6 +280,7 @@ describe('<Footer />', () => {
280280
await waitUntilReady();
281281
expect(lastFrame()).toContain(defaultProps.model);
282282
expect(lastFrame()).toMatch(/\d+%/);
283+
expect(lastFrame()).not.toContain('context used');
283284
unmount();
284285
});
285286

@@ -477,7 +478,7 @@ describe('<Footer />', () => {
477478
);
478479
await waitUntilReady();
479480
expect(lastFrame()).toContain(defaultProps.model);
480-
expect(lastFrame()).not.toMatch(/\d+% context left/);
481+
expect(lastFrame()).not.toMatch(/\d+% context used/);
481482
unmount();
482483
});
483484
it('shows the context percentage when hideContextPercentage is false', async () => {
@@ -497,7 +498,7 @@ describe('<Footer />', () => {
497498
);
498499
await waitUntilReady();
499500
expect(lastFrame()).toContain(defaultProps.model);
500-
expect(lastFrame()).toMatch(/\d+% context left/);
501+
expect(lastFrame()).toMatch(/\d+% context used/);
501502
unmount();
502503
});
503504
it('renders complete footer in narrow terminal (baseline narrow)', async () => {

packages/cli/src/ui/components/HistoryItemDisplay.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
9999
{itemForDisplay.type === 'info' && (
100100
<InfoMessage
101101
text={itemForDisplay.text}
102+
secondaryText={itemForDisplay.secondaryText}
102103
icon={itemForDisplay.icon}
103104
color={itemForDisplay.color}
104105
marginBottom={itemForDisplay.marginBottom}

packages/cli/src/ui/components/StatusDisplay.test.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,12 @@ const renderStatusDisplay = async (
8989
};
9090

9191
describe('StatusDisplay', () => {
92-
const originalEnv = process.env;
92+
beforeEach(() => {
93+
vi.stubEnv('GEMINI_SYSTEM_MD', '');
94+
});
9395

9496
afterEach(() => {
95-
process.env = { ...originalEnv };
96-
delete process.env['GEMINI_SYSTEM_MD'];
97+
vi.unstubAllEnvs();
9798
vi.restoreAllMocks();
9899
});
99100

@@ -112,7 +113,7 @@ describe('StatusDisplay', () => {
112113
});
113114

114115
it('renders system md indicator if env var is set', async () => {
115-
process.env['GEMINI_SYSTEM_MD'] = 'true';
116+
vi.stubEnv('GEMINI_SYSTEM_MD', 'true');
116117
const { lastFrame, unmount } = await renderStatusDisplay();
117118
expect(lastFrame()).toMatchSnapshot();
118119
unmount();

packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
1111
`;
1212

1313
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
14-
" ...s/to/make/it/long no sandbox /model gemini-pro 100%
14+
" ...s/to/make/it/long no sandbox /model gemini-pro 0%
1515
"
1616
`;
1717

1818
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
19-
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 100% context left
19+
" ...directories/to/make/it/long no sandbox (see /docs) /model gemini-pro 0% context used
2020
"
2121
`;
2222

0 commit comments

Comments
 (0)