feat(ui): restore threshold hint and thin arrow to compression message

This commit is contained in:
Keith Guerin
2026-03-20 12:37:28 -07:00
parent fd8d218911
commit 375afc7199
9 changed files with 128 additions and 20 deletions
@@ -98,6 +98,7 @@ describe('compressCommand', () => {
beforePercentage: 20,
afterPercentage: 10,
isManual: true,
thresholdPercentage: 20,
},
},
expect.any(Number),
@@ -76,8 +76,9 @@ export const compressCommand: SlashCommand = {
isPending: false,
beforePercentage,
afterPercentage,
compressionStatus: compressed.compressionStatus,
compressionStatus: (Number(compressed.compressionStatus) as unknown) as CompressionStatus,
isManual: true,
thresholdPercentage: Math.round(threshold * 100),
},
} as HistoryItemCompression,
Date.now(),
@@ -22,8 +22,13 @@ export interface CompressionDisplayProps {
export function CompressionMessage({
compression,
}: CompressionDisplayProps): React.JSX.Element {
const { isPending, beforePercentage, afterPercentage, compressionStatus } =
compression;
const {
isPending,
beforePercentage,
afterPercentage,
compressionStatus,
thresholdPercentage,
} = compression;
const getCompressionText = () => {
if (isPending) {
@@ -31,8 +36,13 @@ export function CompressionMessage({
}
switch (compressionStatus) {
case CompressionStatus.COMPRESSED:
return `Context compressed (${beforePercentage}% ${afterPercentage}%).`;
case CompressionStatus.COMPRESSED: {
let text = `Context compressed (${beforePercentage}% ${afterPercentage}%).`;
if (thresholdPercentage != null) {
text += ` Adjust threshold (${thresholdPercentage}%) in /settings.`;
}
return text;
}
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
return 'Compression was not beneficial for this history size.';
case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
@@ -2637,6 +2637,7 @@ describe('useGeminiStream', () => {
afterPercentage: 5,
compressionStatus: 'compressed',
isManual: false,
thresholdPercentage: 20,
},
}),
expect.any(Number),
@@ -2696,6 +2697,68 @@ describe('useGeminiStream', () => {
});
});
it('should add informational messages when ChatCompressed event is received with a large prompt even if showContextCompression is false', async () => {
vi.mocked(tokenLimit).mockReturnValue(10000);
vi.mocked(
mockConfig.getContextWindowCompressionThreshold,
).mockReturnValue(0.2); // 20%
vi.mocked(mockConfig.getShowContextCompression).mockReturnValue(false);
// Setup mock to return a stream with ChatCompressed event and a large requestTokenCount (25%)
mockSendMessageStream.mockReturnValue(
(async function* () {
yield {
type: ServerGeminiEventType.ChatCompressed,
value: {
originalTokenCount: 1000,
newTokenCount: 500,
compressionStatus: 'compressed',
requestTokenCount: 2500, // 25% > 20%
},
};
yield {
type: ServerGeminiEventType.Content,
value: 'Response after compression',
};
yield {
type: ServerGeminiEventType.Finished,
value: {
finishReason: 'STOP',
usageMetadata: {
promptTokenCount: 10,
candidatesTokenCount: 20,
totalTokenCount: 30,
},
},
};
})(),
);
const { result } = renderHookWithDefaults();
// Submit a query
await act(async () => {
await result.current.submitQuery('Test large prompt compression');
});
// Check that compression message WAS added despite the setting
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'compression',
compression: expect.objectContaining({
beforePercentage: 10,
afterPercentage: 5,
compressionStatus: 'compressed',
isManual: false,
thresholdPercentage: 20,
}),
}),
expect.any(Number),
);
});
});
it.each([
{
reason: 'STOP',
+12 -2
View File
@@ -38,8 +38,12 @@ import {
GeminiCliOperation,
getPlanModeExitMessage,
isBackgroundExecutionData,
<<<<<<< HEAD
Kind,
ACTIVATE_SKILL_TOOL_NAME,
=======
CompressionStatus,
>>>>>>> 97ff2bea4 (feat(ui): restore threshold hint and thin arrow to compression message)
} from '@google/gemini-cli-core';
import type {
Config,
@@ -1153,7 +1157,12 @@ export const useGeminiStream = (
? Math.round((eventValue.newTokenCount / limit) * 100)
: null;
if (!config.getShowContextCompression()) {
const threshold = config.getContextWindowCompressionThreshold();
const isLargePrompt =
eventValue?.requestTokenCount != null &&
eventValue.requestTokenCount / limit > threshold;
if (!config.getShowContextCompression() && !isLargePrompt) {
return;
}
@@ -1164,8 +1173,9 @@ export const useGeminiStream = (
isPending: false,
beforePercentage,
afterPercentage,
compressionStatus: eventValue?.compressionStatus ?? null,
compressionStatus: eventValue ? ((Number(eventValue.compressionStatus) as unknown) as CompressionStatus) : null,
isManual: false,
thresholdPercentage: Math.round(threshold * 100),
},
timestamp: new Date(userMessageTimestamp),
} as HistoryItemWithoutId,
+2 -1
View File
@@ -141,7 +141,8 @@ export interface CompressionProps {
beforePercentage: number | null;
afterPercentage: number | null;
compressionStatus: CompressionStatus | null;
isManual?: boolean;
isManual: boolean;
thresholdPercentage?: number | null;
}
/**
+8 -1
View File
@@ -248,6 +248,9 @@ describe('Gemini Client (client.ts)', () => {
getEnableHooks: vi.fn().mockReturnValue(false),
getChatCompression: vi.fn().mockReturnValue(undefined),
getCompressionThreshold: vi.fn().mockReturnValue(undefined),
getShowContextWindowWarning: vi.fn().mockReturnValue(false),
getShowContextCompression: vi.fn().mockReturnValue(false),
getContextWindowCompressionThreshold: vi.fn().mockReturnValue(0.2),
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
getShowModelInfoInChat: vi.fn().mockReturnValue(false),
getContinueOnFailedApiCall: vi.fn(),
@@ -1617,6 +1620,7 @@ ${JSON.stringify(
originalTokenCount: initialTokenCount,
newTokenCount: 400,
compressionStatus: CompressionStatus.COMPRESSED,
requestTokenCount: 50, // Added to match updated interface
};
});
@@ -1643,10 +1647,13 @@ ${JSON.stringify(
}),
);
// 2. Should contain compression event
// 2. Should contain compression event with requestTokenCount
expect(events).toContainEqual(
expect.objectContaining({
type: GeminiEventType.ChatCompressed,
value: expect.objectContaining({
requestTokenCount: expect.any(Number),
}),
}),
);
+25 -11
View File
@@ -608,7 +608,19 @@ export class GeminiClient {
// Check for context window overflow
const modelForLimitCheck = this._getActiveModelForCurrentTurn();
const compressed = await this.tryCompressChat(prompt_id, false);
// Estimate tokens. For text-only requests, we estimate based on character length.
// For requests with non-text parts (like images, tools), we use the countTokens API.
const estimatedRequestTokenCount = await calculateRequestTokenCount(
request,
this.getContentGeneratorOrFail(),
modelForLimitCheck,
);
const compressed = await this.tryCompressChat(
prompt_id,
false,
estimatedRequestTokenCount,
);
if (compressed.compressionStatus === CompressionStatus.COMPRESSED) {
yield { type: GeminiEventType.ChatCompressed, value: compressed };
@@ -619,17 +631,13 @@ export class GeminiClient {
await this.tryMaskToolOutputs(this.getHistory());
// Estimate tokens. For text-only requests, we estimate based on character length.
// For requests with non-text parts (like images, tools), we use the countTokens API.
const estimatedRequestTokenCount = await calculateRequestTokenCount(
request,
this.getContentGeneratorOrFail(),
modelForLimitCheck,
);
if (estimatedRequestTokenCount > remainingTokenCount) {
if (!this.config.getShowContextWindowWarning()) {
const forcedCompressed = await this.tryCompressChat(prompt_id, true);
const forcedCompressed = await this.tryCompressChat(
prompt_id,
true,
estimatedRequestTokenCount,
);
if (
forcedCompressed.compressionStatus === CompressionStatus.COMPRESSED
) {
@@ -1175,6 +1183,7 @@ export class GeminiClient {
async tryCompressChat(
prompt_id: string,
force: boolean = false,
requestTokenCount?: number,
): Promise<ChatCompressionInfo> {
// If the model is 'auto', we will use a placeholder model to check.
// Compression occurs before we choose a model, so calling `count_tokens`
@@ -1190,6 +1199,11 @@ export class GeminiClient {
this.hasFailedCompressionAttempt,
);
const resultInfo = {
...info,
requestTokenCount,
};
if (
info.compressionStatus ===
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT
@@ -1225,7 +1239,7 @@ export class GeminiClient {
}
}
return info;
return resultInfo;
}
/**
+1
View File
@@ -188,6 +188,7 @@ export interface ChatCompressionInfo {
originalTokenCount: number;
newTokenCount: number;
compressionStatus: CompressionStatus;
requestTokenCount?: number;
}
export type ServerGeminiChatCompressedEvent = {