From 1be38d8fa09948faad5c848d70d35253f1a6f95d Mon Sep 17 00:00:00 2001 From: Shammi Anand <54147419+ShammiAnand@users.noreply.github.com> Date: Fri, 12 Sep 2025 03:11:08 +0530 Subject: [PATCH] fix(core): Improve compression message clarity for small history cases (#4404) Co-authored-by: Jacob Richman --- .../messages/CompressionMessage.test.tsx | 198 ++++++++++++++++++ .../messages/CompressionMessage.tsx | 46 +++- 2 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 packages/cli/src/ui/components/messages/CompressionMessage.test.tsx 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 = ({ ); -}; +}