feat(ui): add showContextCompression setting and simplify message wording

This commit is contained in:
Keith Guerin
2026-03-19 23:23:14 -07:00
parent 39fb7b11a8
commit 252dbeb39a
9 changed files with 87 additions and 20 deletions
+1
View File
@@ -956,6 +956,7 @@ export async function loadCliConfig(
model: resolvedModel,
maxSessionTurns: settings.model?.maxSessionTurns,
showContextWindowWarning: settings.ui?.showContextWindowWarning,
showContextCompression: settings.ui?.showContextCompression,
listExtensions: argv.listExtensions || false,
listSessions: argv.listSessions || false,
@@ -590,6 +590,15 @@ const SETTINGS_SCHEMA = {
'Show a warning message when the context window limit is nearly reached.',
showInDialog: true,
},
showContextCompression: {
type: 'boolean',
label: 'Show Context Compression Events',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Show a message when the chat history is compressed.',
showInDialog: true,
},
footer: {
type: 'object',
label: 'Footer',
@@ -47,8 +47,8 @@ describe('compressCommand', () => {
isPending: true,
beforePercentage: null,
afterPercentage: null,
threshold: null,
compressionStatus: null,
isManual: true,
},
};
await compressCommand.action!(context, '');
@@ -80,7 +80,7 @@ describe('compressCommand', () => {
compressionStatus: null,
beforePercentage: null,
afterPercentage: null,
threshold: null,
isManual: true,
},
});
@@ -97,7 +97,7 @@ describe('compressCommand', () => {
compressionStatus: Core.CompressionStatus.COMPRESSED,
beforePercentage: 20,
afterPercentage: 10,
threshold: 20,
isManual: true,
},
},
expect.any(Number),
@@ -47,8 +47,8 @@ export const compressCommand: SlashCommand = {
isPending: true,
beforePercentage: null,
afterPercentage: null,
threshold: null,
compressionStatus: null,
isManual: true,
},
};
@@ -76,8 +76,8 @@ export const compressCommand: SlashCommand = {
isPending: false,
beforePercentage,
afterPercentage,
threshold,
compressionStatus: compressed.compressionStatus,
isManual: true,
},
} as HistoryItemCompression,
Date.now(),
@@ -22,13 +22,8 @@ export interface CompressionDisplayProps {
export function CompressionMessage({
compression,
}: CompressionDisplayProps): React.JSX.Element {
const {
isPending,
beforePercentage,
afterPercentage,
threshold,
compressionStatus,
} = compression;
const { isPending, beforePercentage, afterPercentage, compressionStatus } =
compression;
const getCompressionText = () => {
if (isPending) {
@@ -37,7 +32,7 @@ export function CompressionMessage({
switch (compressionStatus) {
case CompressionStatus.COMPRESSED:
return `Context compressed (${beforePercentage}% ➔ ${afterPercentage}%). Adjust threshold (${threshold}%) in /settings.`;
return `Context compressed (${beforePercentage}% ➔ ${afterPercentage}%).`;
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
return 'Compression was not beneficial for this history size.';
case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
@@ -332,6 +332,7 @@ describe('useGeminiStream', () => {
getIdeMode: vi.fn(() => false),
getEnableHooks: vi.fn(() => false),
getShowContextWindowWarning: vi.fn(() => false),
getShowContextCompression: vi.fn(() => false),
getContextWindowCompressionThreshold: vi.fn(() => 0.2),
} as unknown as Config;
@@ -2583,11 +2584,12 @@ describe('useGeminiStream', () => {
});
});
it('should add informational messages when ChatCompressed event is received', async () => {
it('should add informational messages when ChatCompressed event is received and showContextCompression is true', async () => {
vi.mocked(tokenLimit).mockReturnValue(10000);
vi.mocked(
mockConfig.getContextWindowCompressionThreshold,
).mockReturnValue(0.2);
vi.mocked(mockConfig.getShowContextCompression).mockReturnValue(true);
// Setup mock to return a stream with ChatCompressed event
mockSendMessageStream.mockReturnValue(
(async function* () {
@@ -2633,8 +2635,8 @@ describe('useGeminiStream', () => {
isPending: false,
beforePercentage: 10,
afterPercentage: 5,
threshold: 20,
compressionStatus: 'compressed',
isManual: false,
},
}),
expect.any(Number),
@@ -2642,6 +2644,58 @@ describe('useGeminiStream', () => {
});
});
it('should NOT add informational messages when ChatCompressed event is received and showContextCompression is false', async () => {
vi.mocked(tokenLimit).mockReturnValue(10000);
vi.mocked(
mockConfig.getContextWindowCompressionThreshold,
).mockReturnValue(0.2);
vi.mocked(mockConfig.getShowContextCompression).mockReturnValue(false);
// Setup mock to return a stream with ChatCompressed event
mockSendMessageStream.mockReturnValue(
(async function* () {
yield {
type: ServerGeminiEventType.ChatCompressed,
value: {
originalTokenCount: 1000,
newTokenCount: 500,
compressionStatus: 'compressed',
},
};
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 compression');
});
// Check that NO compression message was added
await waitFor(() => {
expect(mockAddItem).not.toHaveBeenCalledWith(
expect.objectContaining({
type: 'compression',
}),
);
});
});
it.each([
{
reason: 'STOP',
+5 -4
View File
@@ -1152,9 +1152,10 @@ export const useGeminiStream = (
eventValue?.newTokenCount != null
? Math.round((eventValue.newTokenCount / limit) * 100)
: null;
const threshold = Math.round(
config.getContextWindowCompressionThreshold() * 100,
);
if (!config.getShowContextCompression()) {
return;
}
addItem(
{
@@ -1163,8 +1164,8 @@ export const useGeminiStream = (
isPending: false,
beforePercentage,
afterPercentage,
threshold,
compressionStatus: eventValue?.compressionStatus ?? null,
isManual: false,
},
timestamp: new Date(userMessageTimestamp),
} as HistoryItemWithoutId,
+1 -1
View File
@@ -140,8 +140,8 @@ export interface CompressionProps {
isPending: boolean;
beforePercentage: number | null;
afterPercentage: number | null;
threshold: number | null;
compressionStatus: CompressionStatus | null;
isManual?: boolean;
}
/**
+7
View File
@@ -674,6 +674,7 @@ export interface ConfigParameters {
}>;
enableConseca?: boolean;
showContextWindowWarning?: boolean;
showContextCompression?: boolean;
billing?: {
overageStrategy?: OverageStrategy;
};
@@ -710,6 +711,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly worktreeSettings: WorktreeSettings | undefined;
readonly enableConseca: boolean;
private readonly showContextWindowWarning: boolean;
private readonly showContextCompression: boolean;
private readonly coreTools: string[] | undefined;
private readonly mainAgentTools: string[] | undefined;
@@ -1149,6 +1151,7 @@ export class Config implements McpContext, AgentLoopContext {
this.eventEmitter = params.eventEmitter;
this.enableConseca = params.enableConseca ?? false;
this.showContextWindowWarning = params.showContextWindowWarning ?? false;
this.showContextCompression = params.showContextCompression ?? false;
// Initialize Safety Infrastructure
const contextBuilder = new ContextBuilder(this);
@@ -2060,6 +2063,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.showContextWindowWarning;
}
getShowContextCompression(): boolean {
return this.showContextCompression;
}
getMcpEnablementCallbacks(): McpEnablementCallbacks | undefined {
return this.mcpEnablementCallbacks;
}