feat(cli): lower compression threshold to 0.2 and update UX

- Change default compression threshold to 200k

- Introduce 'ChatCompressing' event to handle UI loading

- Subtly display context optimization in message footer

- Fix associated snapshot tests
This commit is contained in:
Taylor Mullen
2026-02-17 23:12:07 -08:00
parent 884acda2dc
commit 3031aef1a4
10 changed files with 79 additions and 53 deletions
+2
View File
@@ -1061,6 +1061,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells, backgroundShells,
dismissBackgroundShell, dismissBackgroundShell,
retryStatus, retryStatus,
isCompressing,
} = useGeminiStream( } = useGeminiStream(
config.getGeminiClient(), config.getGeminiClient(),
historyManager.history, historyManager.history,
@@ -1613,6 +1614,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
streamingState, streamingState,
shouldShowFocusHint, shouldShowFocusHint,
retryStatus, retryStatus,
customWittyPhrases: isCompressing ? ['Optimizing context...'] : undefined,
}); });
const handleGlobalKeypress = useCallback( const handleGlobalKeypress = useCallback(
@@ -17,7 +17,7 @@ import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js'; import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { CompressionMessage } from './messages/CompressionMessage.js'; import { CompressionMessage } from './messages/CompressionMessage.js';
import { WarningMessage } from './messages/WarningMessage.js'; import { WarningMessage } from './messages/WarningMessage.js';
import { Box } from 'ink'; import { Box, Text } from 'ink';
import { AboutBox } from './AboutBox.js'; import { AboutBox } from './AboutBox.js';
import { StatsDisplay } from './StatsDisplay.js'; import { StatsDisplay } from './StatsDisplay.js';
import { ModelStatsDisplay } from './ModelStatsDisplay.js'; import { ModelStatsDisplay } from './ModelStatsDisplay.js';
@@ -75,6 +75,14 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
{itemForDisplay.type === 'user_shell' && ( {itemForDisplay.type === 'user_shell' && (
<UserShellMessage text={itemForDisplay.text} width={terminalWidth} /> <UserShellMessage text={itemForDisplay.text} width={terminalWidth} />
)} )}
{itemForDisplay.type === 'auto_compression' && (
<Box paddingLeft={2} marginTop={0}>
<Text dimColor>
(Context optimized: {itemForDisplay.compression.originalTokenCount}{' '}
{itemForDisplay.compression.newTokenCount} tokens)
</Text>
</Box>
)}
{itemForDisplay.type === 'gemini' && ( {itemForDisplay.type === 'gemini' && (
<GeminiMessage <GeminiMessage
text={itemForDisplay.text} text={itemForDisplay.text}
@@ -77,39 +77,6 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄" ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`; `;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 > [Pasted Text: 10 lines] 
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 > line1 
 line2 
 line3 
 line4 
 line5 
 line6 
 line7 
 line8 
 line9 
 line10 
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 7`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 > [Pasted Text: 10 lines] 
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = ` exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file > Type your message or @path/to/file
@@ -12,7 +12,6 @@ import { theme } from '../../semantic-colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
import { useUIState } from '../../contexts/UIStateContext.js'; import { useUIState } from '../../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js'; import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
interface GeminiMessageProps { interface GeminiMessageProps {
text: string; text: string;
isPending: boolean; isPending: boolean;
+47 -15
View File
@@ -53,6 +53,7 @@ import { type Part, type PartListUnion, FinishReason } from '@google/genai';
import type { import type {
HistoryItem, HistoryItem,
HistoryItemThinking, HistoryItemThinking,
HistoryItemAutoCompression,
HistoryItemWithoutId, HistoryItemWithoutId,
HistoryItemToolGroup, HistoryItemToolGroup,
IndividualToolCallDisplay, IndividualToolCallDisplay,
@@ -203,6 +204,8 @@ export const useGeminiStream = (
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] = const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
useStateAndRef<HistoryItemWithoutId | null>(null); useStateAndRef<HistoryItemWithoutId | null>(null);
const [isCompressing, setIsCompressing] = useState<boolean>(false);
const [lastGeminiActivityTime, setLastGeminiActivityTime] = const [lastGeminiActivityTime, setLastGeminiActivityTime] =
useState<number>(0); useState<number>(0);
const [pushedToolCallIds, pushedToolCallIdsRef, setPushedToolCallIds] = const [pushedToolCallIds, pushedToolCallIdsRef, setPushedToolCallIds] =
@@ -765,7 +768,10 @@ export const useGeminiStream = (
if (pendingHistoryItemRef.current) { if (pendingHistoryItemRef.current) {
addItem(pendingHistoryItemRef.current, userMessageTimestamp); addItem(pendingHistoryItemRef.current, userMessageTimestamp);
} }
setPendingHistoryItem({ type: 'gemini', text: '' }); setPendingHistoryItem({
type: 'gemini',
text: '',
});
newGeminiMessageBuffer = eventValue; newGeminiMessageBuffer = eventValue;
} }
// Split large messages for better rendering performance. Ideally, // Split large messages for better rendering performance. Ideally,
@@ -773,11 +779,23 @@ export const useGeminiStream = (
const splitPoint = findLastSafeSplitPoint(newGeminiMessageBuffer); const splitPoint = findLastSafeSplitPoint(newGeminiMessageBuffer);
if (splitPoint === newGeminiMessageBuffer.length) { if (splitPoint === newGeminiMessageBuffer.length) {
// Update the existing message with accumulated content // Update the existing message with accumulated content
setPendingHistoryItem((item) => ({ setPendingHistoryItem((item) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion if (!item) return null;
type: item?.type as 'gemini' | 'gemini_content', if (item.type === 'gemini') {
text: newGeminiMessageBuffer, return {
})); ...item,
type: 'gemini',
text: newGeminiMessageBuffer,
};
} else if (item.type === 'gemini_content') {
return {
...item,
type: 'gemini_content',
text: newGeminiMessageBuffer,
};
}
return item;
});
} else { } else {
// This indicates that we need to split up this Gemini Message. // This indicates that we need to split up this Gemini Message.
// Splitting a message is primarily a performance consideration. There is a // Splitting a message is primarily a performance consideration. There is a
@@ -799,7 +817,10 @@ export const useGeminiStream = (
}, },
userMessageTimestamp, userMessageTimestamp,
); );
setPendingHistoryItem({ type: 'gemini_content', text: afterText }); setPendingHistoryItem({
type: 'gemini_content',
text: afterText,
});
newGeminiMessageBuffer = afterText; newGeminiMessageBuffer = afterText;
} }
return newGeminiMessageBuffer; return newGeminiMessageBuffer;
@@ -956,14 +977,16 @@ export const useGeminiStream = (
addItem(pendingHistoryItemRef.current, userMessageTimestamp); addItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null); setPendingHistoryItem(null);
} }
return addItem({
type: 'info', if (eventValue) {
text: addItem(
`IMPORTANT: This conversation exceeded the compress threshold. ` + {
`A compressed context will be sent for future messages (compressed from: ` + type: 'auto_compression',
`${eventValue?.originalTokenCount ?? 'unknown'} to ` + compression: eventValue,
`${eventValue?.newTokenCount ?? 'unknown'} tokens).`, } as HistoryItemAutoCompression,
}); userMessageTimestamp,
);
}
}, },
[addItem, pendingHistoryItemRef, setPendingHistoryItem], [addItem, pendingHistoryItemRef, setPendingHistoryItem],
); );
@@ -1095,6 +1118,10 @@ export const useGeminiStream = (
let geminiMessageBuffer = ''; let geminiMessageBuffer = '';
const toolCallRequests: ToolCallRequestInfo[] = []; const toolCallRequests: ToolCallRequestInfo[] = [];
for await (const event of stream) { for await (const event of stream) {
if (event.type !== ServerGeminiEventType.ChatCompressing) {
setIsCompressing(false);
}
if ( if (
event.type !== ServerGeminiEventType.Thought && event.type !== ServerGeminiEventType.Thought &&
thoughtRef.current !== null thoughtRef.current !== null
@@ -1140,7 +1167,11 @@ export const useGeminiStream = (
event.value.contextCleared, event.value.contextCleared,
); );
break; break;
case ServerGeminiEventType.ChatCompressing:
setIsCompressing(true);
break;
case ServerGeminiEventType.ChatCompressed: case ServerGeminiEventType.ChatCompressed:
setIsCompressing(false);
handleChatCompressionEvent(event.value, userMessageTimestamp); handleChatCompressionEvent(event.value, userMessageTimestamp);
break; break;
case ServerGeminiEventType.ToolCallConfirmation: case ServerGeminiEventType.ToolCallConfirmation:
@@ -1683,6 +1714,7 @@ export const useGeminiStream = (
return { return {
streamingState, streamingState,
isCompressing,
submitQuery, submitQuery,
initError, initError,
pendingHistoryItems, pendingHistoryItems,
+7
View File
@@ -6,6 +6,7 @@
import { import {
type CompressionStatus, type CompressionStatus,
type ChatCompressionInfo,
type GeminiCLIExtension, type GeminiCLIExtension,
type MCPServerConfig, type MCPServerConfig,
type ThoughtSummary, type ThoughtSummary,
@@ -248,6 +249,11 @@ export type HistoryItemThinking = HistoryItemBase & {
thought: ThoughtSummary; thought: ThoughtSummary;
}; };
export type HistoryItemAutoCompression = HistoryItemBase & {
type: 'auto_compression';
compression: ChatCompressionInfo;
};
export type HistoryItemChatList = HistoryItemBase & { export type HistoryItemChatList = HistoryItemBase & {
type: 'chat_list'; type: 'chat_list';
chats: ChatDetail[]; chats: ChatDetail[];
@@ -372,6 +378,7 @@ export type HistoryItemWithoutId =
| HistoryItemMcpStatus | HistoryItemMcpStatus
| HistoryItemChatList | HistoryItemChatList
| HistoryItemThinking | HistoryItemThinking
| HistoryItemAutoCompression
| HistoryItemHooksList; | HistoryItemHooksList;
export type HistoryItem = HistoryItemWithoutId & { id: number }; export type HistoryItem = HistoryItemWithoutId & { id: number };
+6 -2
View File
@@ -1077,6 +1077,7 @@ ${JSON.stringify(
new AbortController().signal, new AbortController().signal,
'test-prompt-id', 'test-prompt-id',
); );
await stream.next(); // Trigger ChatCompressing
await stream.next(); // Trigger the generator await stream.next(); // Trigger the generator
expect(countTokensSpy).toHaveBeenCalledWith( expect(countTokensSpy).toHaveBeenCalledWith(
@@ -1924,8 +1925,10 @@ ${JSON.stringify(
// Assert // Assert
expect(events).toEqual([ expect(events).toEqual([
{ type: GeminiEventType.ChatCompressing },
{ type: GeminiEventType.ModelInfo, value: 'default-routed-model' }, { type: GeminiEventType.ModelInfo, value: 'default-routed-model' },
{ type: GeminiEventType.InvalidStream }, { type: GeminiEventType.InvalidStream },
{ type: GeminiEventType.ChatCompressing },
{ type: GeminiEventType.Content, value: 'Continued content' }, { type: GeminiEventType.Content, value: 'Continued content' },
]); ]);
@@ -1980,6 +1983,7 @@ ${JSON.stringify(
// Assert // Assert
expect(events).toEqual([ expect(events).toEqual([
{ type: GeminiEventType.ChatCompressing },
{ type: GeminiEventType.ModelInfo, value: 'default-routed-model' }, { type: GeminiEventType.ModelInfo, value: 'default-routed-model' },
{ type: GeminiEventType.InvalidStream }, { type: GeminiEventType.InvalidStream },
]); ]);
@@ -2017,8 +2021,8 @@ ${JSON.stringify(
const events = await fromAsync(stream); const events = await fromAsync(stream);
// Assert // Assert
// We expect 3 events (model_info + original + 1 retry) // We expect 5 events (chat_compressing + model_info + original + 1 retry chat_compressing + 1 retry model_info)
expect(events.length).toBe(3); expect(events.length).toBe(5);
expect( expect(
events events
.filter((e) => e.type === GeminiEventType.ModelInfo) .filter((e) => e.type === GeminiEventType.ModelInfo)
+1
View File
@@ -571,6 +571,7 @@ export class GeminiClient {
// Check for context window overflow // Check for context window overflow
const modelForLimitCheck = this._getActiveModelForCurrentTurn(); const modelForLimitCheck = this._getActiveModelForCurrentTurn();
yield { type: GeminiEventType.ChatCompressing };
const compressed = await this.tryCompressChat(prompt_id, false); const compressed = await this.tryCompressChat(prompt_id, false);
if (compressed.compressionStatus === CompressionStatus.COMPRESSED) { if (compressed.compressionStatus === CompressionStatus.COMPRESSED) {
+6
View File
@@ -68,6 +68,7 @@ export enum GeminiEventType {
ModelInfo = 'model_info', ModelInfo = 'model_info',
AgentExecutionStopped = 'agent_execution_stopped', AgentExecutionStopped = 'agent_execution_stopped',
AgentExecutionBlocked = 'agent_execution_blocked', AgentExecutionBlocked = 'agent_execution_blocked',
ChatCompressing = 'chat_compressing',
} }
export type ServerGeminiRetryEvent = { export type ServerGeminiRetryEvent = {
@@ -192,6 +193,10 @@ export type ServerGeminiChatCompressedEvent = {
value: ChatCompressionInfo | null; value: ChatCompressionInfo | null;
}; };
export type ServerGeminiChatCompressingEvent = {
type: GeminiEventType.ChatCompressing;
};
export type ServerGeminiMaxSessionTurnsEvent = { export type ServerGeminiMaxSessionTurnsEvent = {
type: GeminiEventType.MaxSessionTurns; type: GeminiEventType.MaxSessionTurns;
}; };
@@ -213,6 +218,7 @@ export type ServerGeminiCitationEvent = {
// The original union type, now composed of the individual types // The original union type, now composed of the individual types
export type ServerGeminiStreamEvent = export type ServerGeminiStreamEvent =
| ServerGeminiChatCompressedEvent | ServerGeminiChatCompressedEvent
| ServerGeminiChatCompressingEvent
| ServerGeminiCitationEvent | ServerGeminiCitationEvent
| ServerGeminiContentEvent | ServerGeminiContentEvent
| ServerGeminiErrorEvent | ServerGeminiErrorEvent
@@ -36,7 +36,7 @@ import { PreCompressTrigger } from '../hooks/types.js';
* Default threshold for compression token count as a fraction of the model's * Default threshold for compression token count as a fraction of the model's
* token limit. If the chat history exceeds this threshold, it will be compressed. * token limit. If the chat history exceeds this threshold, it will be compressed.
*/ */
export const DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.5; export const DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.2;
/** /**
* The fraction of the latest chat history to keep. A value of 0.3 * The fraction of the latest chat history to keep. A value of 0.3