Skip to content

Commit 1be38d8

Browse files
fix(core): Improve compression message clarity for small history cases (google-gemini#4404)
Co-authored-by: Jacob Richman <[email protected]>
1 parent 67f7dae commit 1be38d8

File tree

2 files changed

+235
-9
lines changed

2 files changed

+235
-9
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from 'ink-testing-library';
8+
import type { CompressionDisplayProps } from './CompressionMessage.js';
9+
import { CompressionMessage } from './CompressionMessage.js';
10+
import { CompressionStatus } from '@google/gemini-cli-core';
11+
import type { CompressionProps } from '../../types.js';
12+
import { describe, it, expect } from 'vitest';
13+
14+
describe('<CompressionMessage />', () => {
15+
const createCompressionProps = (
16+
overrides: Partial<CompressionProps> = {},
17+
): CompressionDisplayProps => ({
18+
compression: {
19+
isPending: false,
20+
originalTokenCount: null,
21+
newTokenCount: null,
22+
compressionStatus: CompressionStatus.COMPRESSED,
23+
...overrides,
24+
},
25+
});
26+
27+
describe('pending state', () => {
28+
it('renders pending message when compression is in progress', () => {
29+
const props = createCompressionProps({ isPending: true });
30+
const { lastFrame } = render(<CompressionMessage {...props} />);
31+
const output = lastFrame();
32+
33+
expect(output).toContain('Compressing chat history');
34+
});
35+
});
36+
37+
describe('normal compression (successful token reduction)', () => {
38+
it('renders success message when tokens are reduced', () => {
39+
const props = createCompressionProps({
40+
isPending: false,
41+
originalTokenCount: 100,
42+
newTokenCount: 50,
43+
compressionStatus: CompressionStatus.COMPRESSED,
44+
});
45+
const { lastFrame } = render(<CompressionMessage {...props} />);
46+
const output = lastFrame();
47+
48+
expect(output).toContain('✦');
49+
expect(output).toContain(
50+
'Chat history compressed from 100 to 50 tokens.',
51+
);
52+
});
53+
54+
it('renders success message for large successful compressions', () => {
55+
const testCases = [
56+
{ original: 50000, new: 25000 }, // Large compression
57+
{ original: 700000, new: 350000 }, // Very large compression
58+
];
59+
60+
testCases.forEach(({ original, new: newTokens }) => {
61+
const props = createCompressionProps({
62+
isPending: false,
63+
originalTokenCount: original,
64+
newTokenCount: newTokens,
65+
compressionStatus: CompressionStatus.COMPRESSED,
66+
});
67+
const { lastFrame } = render(<CompressionMessage {...props} />);
68+
const output = lastFrame();
69+
70+
expect(output).toContain('✦');
71+
expect(output).toContain(
72+
`compressed from ${original} to ${newTokens} tokens`,
73+
);
74+
expect(output).not.toContain('Skipping compression');
75+
expect(output).not.toContain('did not reduce size');
76+
});
77+
});
78+
});
79+
80+
describe('skipped compression (tokens increased or same)', () => {
81+
it('renders skip message when compression would increase token count', () => {
82+
const props = createCompressionProps({
83+
isPending: false,
84+
originalTokenCount: 50,
85+
newTokenCount: 75,
86+
compressionStatus:
87+
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
88+
});
89+
const { lastFrame } = render(<CompressionMessage {...props} />);
90+
const output = lastFrame();
91+
92+
expect(output).toContain('✦');
93+
expect(output).toContain(
94+
'Compression was not beneficial for this history size.',
95+
);
96+
});
97+
98+
it('renders skip message when token counts are equal', () => {
99+
const props = createCompressionProps({
100+
isPending: false,
101+
originalTokenCount: 50,
102+
newTokenCount: 50,
103+
compressionStatus:
104+
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
105+
});
106+
const { lastFrame } = render(<CompressionMessage {...props} />);
107+
const output = lastFrame();
108+
109+
expect(output).toContain(
110+
'Compression was not beneficial for this history size.',
111+
);
112+
});
113+
});
114+
115+
describe('message content validation', () => {
116+
it('displays correct compression statistics', () => {
117+
const testCases = [
118+
{
119+
original: 200,
120+
new: 80,
121+
expected: 'compressed from 200 to 80 tokens',
122+
},
123+
{
124+
original: 500,
125+
new: 150,
126+
expected: 'compressed from 500 to 150 tokens',
127+
},
128+
{
129+
original: 1500,
130+
new: 400,
131+
expected: 'compressed from 1500 to 400 tokens',
132+
},
133+
];
134+
135+
testCases.forEach(({ original, new: newTokens, expected }) => {
136+
const props = createCompressionProps({
137+
isPending: false,
138+
originalTokenCount: original,
139+
newTokenCount: newTokens,
140+
compressionStatus: CompressionStatus.COMPRESSED,
141+
});
142+
const { lastFrame } = render(<CompressionMessage {...props} />);
143+
const output = lastFrame();
144+
145+
expect(output).toContain(expected);
146+
});
147+
});
148+
149+
it('shows skip message for small histories when new tokens >= original tokens', () => {
150+
const testCases = [
151+
{ original: 50, new: 60 }, // Increased
152+
{ original: 100, new: 100 }, // Same
153+
{ original: 49999, new: 50000 }, // Just under 50k threshold
154+
];
155+
156+
testCases.forEach(({ original, new: newTokens }) => {
157+
const props = createCompressionProps({
158+
isPending: false,
159+
originalTokenCount: original,
160+
newTokenCount: newTokens,
161+
compressionStatus:
162+
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
163+
});
164+
const { lastFrame } = render(<CompressionMessage {...props} />);
165+
const output = lastFrame();
166+
167+
expect(output).toContain(
168+
'Compression was not beneficial for this history size.',
169+
);
170+
expect(output).not.toContain('compressed from');
171+
});
172+
});
173+
174+
it('shows compression failure message for large histories when new tokens >= original tokens', () => {
175+
const testCases = [
176+
{ original: 50000, new: 50100 }, // At 50k threshold
177+
{ original: 700000, new: 710000 }, // Large history case
178+
{ original: 100000, new: 100000 }, // Large history, same count
179+
];
180+
181+
testCases.forEach(({ original, new: newTokens }) => {
182+
const props = createCompressionProps({
183+
isPending: false,
184+
originalTokenCount: original,
185+
newTokenCount: newTokens,
186+
compressionStatus:
187+
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
188+
});
189+
const { lastFrame } = render(<CompressionMessage {...props} />);
190+
const output = lastFrame();
191+
192+
expect(output).toContain('compression did not reduce size');
193+
expect(output).not.toContain('compressed from');
194+
expect(output).not.toContain('Compression was not beneficial');
195+
});
196+
});
197+
});
198+
});

packages/cli/src/ui/components/messages/CompressionMessage.tsx

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

7-
import type React from 'react';
87
import { Box, Text } from 'ink';
98
import type { CompressionProps } from '../../types.js';
109
import Spinner from 'ink-spinner';
1110
import { theme } from '../../semantic-colors.js';
1211
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
12+
import { CompressionStatus } from '@google/gemini-cli-core';
1313

1414
export interface CompressionDisplayProps {
1515
compression: CompressionProps;
@@ -19,18 +19,46 @@ export interface CompressionDisplayProps {
1919
* Compression messages appear when the /compress command is run, and show a loading spinner
2020
* while compression is in progress, followed up by some compression stats.
2121
*/
22-
export const CompressionMessage: React.FC<CompressionDisplayProps> = ({
22+
export function CompressionMessage({
2323
compression,
24-
}) => {
25-
const text = compression.isPending
26-
? 'Compressing chat history'
27-
: `Chat history compressed from ${compression.originalTokenCount ?? 'unknown'}` +
28-
` to ${compression.newTokenCount ?? 'unknown'} tokens.`;
24+
}: CompressionDisplayProps): React.JSX.Element {
25+
const { isPending, originalTokenCount, newTokenCount, compressionStatus } =
26+
compression;
27+
28+
const originalTokens = originalTokenCount ?? 0;
29+
const newTokens = newTokenCount ?? 0;
30+
31+
const getCompressionText = () => {
32+
if (isPending) {
33+
return 'Compressing chat history';
34+
}
35+
36+
switch (compressionStatus) {
37+
case CompressionStatus.COMPRESSED:
38+
return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`;
39+
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
40+
// For smaller histories (< 50k tokens), compression overhead likely exceeds benefits
41+
if (originalTokens < 50000) {
42+
return 'Compression was not beneficial for this history size.';
43+
}
44+
// For larger histories where compression should work but didn't,
45+
// this suggests an issue with the compression process itself
46+
return 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.';
47+
case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
48+
return 'Could not compress chat history due to a token counting error.';
49+
case CompressionStatus.NOOP:
50+
return 'Chat history is already compressed.';
51+
default:
52+
return '';
53+
}
54+
};
55+
56+
const text = getCompressionText();
2957

3058
return (
3159
<Box flexDirection="row">
3260
<Box marginRight={1}>
33-
{compression.isPending ? (
61+
{isPending ? (
3462
<Spinner type="dots" />
3563
) : (
3664
<Text color={theme.text.accent}></Text>
@@ -48,4 +76,4 @@ export const CompressionMessage: React.FC<CompressionDisplayProps> = ({
4876
</Box>
4977
</Box>
5078
);
51-
};
79+
}

0 commit comments

Comments
 (0)