diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx
new file mode 100644
index 0000000000..1c56a7326a
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx
@@ -0,0 +1,198 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from 'ink-testing-library';
+import type { CompressionDisplayProps } from './CompressionMessage.js';
+import { CompressionMessage } from './CompressionMessage.js';
+import { CompressionStatus } from '@google/gemini-cli-core';
+import type { CompressionProps } from '../../types.js';
+import { describe, it, expect } from 'vitest';
+
+describe('', () => {
+ const createCompressionProps = (
+ overrides: Partial = {},
+ ): CompressionDisplayProps => ({
+ compression: {
+ isPending: false,
+ originalTokenCount: null,
+ newTokenCount: null,
+ compressionStatus: CompressionStatus.COMPRESSED,
+ ...overrides,
+ },
+ });
+
+ describe('pending state', () => {
+ it('renders pending message when compression is in progress', () => {
+ const props = createCompressionProps({ isPending: true });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain('Compressing chat history');
+ });
+ });
+
+ describe('normal compression (successful token reduction)', () => {
+ it('renders success message when tokens are reduced', () => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: 100,
+ newTokenCount: 50,
+ compressionStatus: CompressionStatus.COMPRESSED,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain('✦');
+ expect(output).toContain(
+ 'Chat history compressed from 100 to 50 tokens.',
+ );
+ });
+
+ it('renders success message for large successful compressions', () => {
+ const testCases = [
+ { original: 50000, new: 25000 }, // Large compression
+ { original: 700000, new: 350000 }, // Very large compression
+ ];
+
+ testCases.forEach(({ original, new: newTokens }) => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: original,
+ newTokenCount: newTokens,
+ compressionStatus: CompressionStatus.COMPRESSED,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain('✦');
+ expect(output).toContain(
+ `compressed from ${original} to ${newTokens} tokens`,
+ );
+ expect(output).not.toContain('Skipping compression');
+ expect(output).not.toContain('did not reduce size');
+ });
+ });
+ });
+
+ describe('skipped compression (tokens increased or same)', () => {
+ it('renders skip message when compression would increase token count', () => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: 50,
+ newTokenCount: 75,
+ compressionStatus:
+ CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain('✦');
+ expect(output).toContain(
+ 'Compression was not beneficial for this history size.',
+ );
+ });
+
+ it('renders skip message when token counts are equal', () => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: 50,
+ newTokenCount: 50,
+ compressionStatus:
+ CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain(
+ 'Compression was not beneficial for this history size.',
+ );
+ });
+ });
+
+ describe('message content validation', () => {
+ it('displays correct compression statistics', () => {
+ const testCases = [
+ {
+ original: 200,
+ new: 80,
+ expected: 'compressed from 200 to 80 tokens',
+ },
+ {
+ original: 500,
+ new: 150,
+ expected: 'compressed from 500 to 150 tokens',
+ },
+ {
+ original: 1500,
+ new: 400,
+ expected: 'compressed from 1500 to 400 tokens',
+ },
+ ];
+
+ testCases.forEach(({ original, new: newTokens, expected }) => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: original,
+ newTokenCount: newTokens,
+ compressionStatus: CompressionStatus.COMPRESSED,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain(expected);
+ });
+ });
+
+ it('shows skip message for small histories when new tokens >= original tokens', () => {
+ const testCases = [
+ { original: 50, new: 60 }, // Increased
+ { original: 100, new: 100 }, // Same
+ { original: 49999, new: 50000 }, // Just under 50k threshold
+ ];
+
+ testCases.forEach(({ original, new: newTokens }) => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: original,
+ newTokenCount: newTokens,
+ compressionStatus:
+ CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain(
+ 'Compression was not beneficial for this history size.',
+ );
+ expect(output).not.toContain('compressed from');
+ });
+ });
+
+ it('shows compression failure message for large histories when new tokens >= original tokens', () => {
+ const testCases = [
+ { original: 50000, new: 50100 }, // At 50k threshold
+ { original: 700000, new: 710000 }, // Large history case
+ { original: 100000, new: 100000 }, // Large history, same count
+ ];
+
+ testCases.forEach(({ original, new: newTokens }) => {
+ const props = createCompressionProps({
+ isPending: false,
+ originalTokenCount: original,
+ newTokenCount: newTokens,
+ compressionStatus:
+ CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
+ });
+ const { lastFrame } = render();
+ const output = lastFrame();
+
+ expect(output).toContain('compression did not reduce size');
+ expect(output).not.toContain('compressed from');
+ expect(output).not.toContain('Compression was not beneficial');
+ });
+ });
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx
index 929150b5bf..8bbe1ef175 100644
--- a/packages/cli/src/ui/components/messages/CompressionMessage.tsx
+++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx
@@ -4,12 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type React from 'react';
import { Box, Text } from 'ink';
import type { CompressionProps } from '../../types.js';
import Spinner from 'ink-spinner';
import { theme } from '../../semantic-colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
+import { CompressionStatus } from '@google/gemini-cli-core';
export interface CompressionDisplayProps {
compression: CompressionProps;
@@ -19,18 +19,46 @@ export interface CompressionDisplayProps {
* Compression messages appear when the /compress command is run, and show a loading spinner
* while compression is in progress, followed up by some compression stats.
*/
-export const CompressionMessage: React.FC = ({
+export function CompressionMessage({
compression,
-}) => {
- const text = compression.isPending
- ? 'Compressing chat history'
- : `Chat history compressed from ${compression.originalTokenCount ?? 'unknown'}` +
- ` to ${compression.newTokenCount ?? 'unknown'} tokens.`;
+}: CompressionDisplayProps): React.JSX.Element {
+ const { isPending, originalTokenCount, newTokenCount, compressionStatus } =
+ compression;
+
+ const originalTokens = originalTokenCount ?? 0;
+ const newTokens = newTokenCount ?? 0;
+
+ const getCompressionText = () => {
+ if (isPending) {
+ return 'Compressing chat history';
+ }
+
+ switch (compressionStatus) {
+ case CompressionStatus.COMPRESSED:
+ return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`;
+ case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
+ // For smaller histories (< 50k tokens), compression overhead likely exceeds benefits
+ if (originalTokens < 50000) {
+ return 'Compression was not beneficial for this history size.';
+ }
+ // For larger histories where compression should work but didn't,
+ // this suggests an issue with the compression process itself
+ return 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.';
+ case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
+ return 'Could not compress chat history due to a token counting error.';
+ case CompressionStatus.NOOP:
+ return 'Chat history is already compressed.';
+ default:
+ return '';
+ }
+ };
+
+ const text = getCompressionText();
return (
- {compression.isPending ? (
+ {isPending ? (
) : (
✦
@@ -48,4 +76,4 @@ export const CompressionMessage: React.FC = ({
);
-};
+}