From ce40a6534f090df633a6a8f33e9dcf9cbba85554 Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Thu, 30 Oct 2025 16:03:58 -0700 Subject: [PATCH] Make compression threshold editable in the UI. (#12317) --- docs/get-started/configuration-v1.md | 15 ---- docs/get-started/configuration.md | 6 +- packages/cli/src/config/config.test.ts | 16 ++-- packages/cli/src/config/config.ts | 2 +- packages/cli/src/config/settings.test.ts | 74 ++++--------------- packages/cli/src/config/settings.ts | 2 +- packages/cli/src/config/settingsSchema.ts | 14 ++-- packages/core/src/config/config.ts | 14 ++-- .../services/chatCompressionService.test.ts | 4 +- .../src/services/chatCompressionService.ts | 3 +- 10 files changed, 41 insertions(+), 109 deletions(-) diff --git a/docs/get-started/configuration-v1.md b/docs/get-started/configuration-v1.md index 4c00b00f4d..866028a975 100644 --- a/docs/get-started/configuration-v1.md +++ b/docs/get-started/configuration-v1.md @@ -473,21 +473,6 @@ a few things you can try in order of recommendation: "loadMemoryFromIncludeDirectories": true ``` -- **`chatCompression`** (object): - - **Description:** Controls the settings for chat history compression, both - automatic and when manually invoked through the /compress command. - - **Properties:** - - **`contextPercentageThreshold`** (number): A value between 0 and 1 that - specifies the token threshold for compression as a percentage of the - model's total token limit. For example, a value of `0.6` will trigger - compression when the chat history exceeds 60% of the token limit. - - **Example:** - ```json - "chatCompression": { - "contextPercentageThreshold": 0.6 - } - ``` - - **`showLineNumbers`** (boolean): - **Description:** Controls whether line numbers are displayed in code blocks in the CLI output. diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 45353b9034..632f28000c 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -244,13 +244,13 @@ their corresponding top-level category object in your `settings.json` file. example `{"run_shell_command": {"tokenBudget": 2000}}` - **Default:** `undefined` -- **`model.chatCompression.contextPercentageThreshold`** (number): +- **`model.compressionThreshold`** (number): - **Description:** Sets the threshold for chat history compression as a - percentage of the model's total token limit. This is a value between 0 and 1 + fraction of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. - - **Default:** `0.7` + - **Default:** `0.2` - **`model.skipNextSpeakerCheck`** (boolean): - **Description:** Skip the next speaker check. diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 5490afb678..880462e728 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1572,7 +1572,7 @@ describe('loadCliConfig with includeDirectories', () => { }); }); -describe('loadCliConfig chatCompression', () => { +describe('loadCliConfig compressionThreshold', () => { const originalArgv = process.argv; beforeEach(() => { @@ -1587,28 +1587,24 @@ describe('loadCliConfig chatCompression', () => { vi.restoreAllMocks(); }); - it('should pass chatCompression settings to the core config', async () => { + it('should pass settings to the core config', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { model: { - chatCompression: { - contextPercentageThreshold: 0.5, - }, + compressionThreshold: 0.5, }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); - expect(config.getChatCompression()).toEqual({ - contextPercentageThreshold: 0.5, - }); + expect(config.getCompressionThreshold()).toEqual(0.5); }); - it('should have undefined chatCompression if not in settings', async () => { + it('should have undefined compressionThreshold if not in settings', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); - expect(config.getChatCompression()).toBeUndefined(); + expect(config.getCompressionThreshold()).toBeUndefined(); }); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 1bb1a3b70d..318f674170 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -610,7 +610,7 @@ export async function loadCliConfig( noBrowser: !!process.env['NO_BROWSER'], summarizeToolOutput: settings.model?.summarizeToolOutput, ideMode, - chatCompression: settings.model?.chatCompression, + compressionThreshold: settings.model?.compressionThreshold, folderTrust, interactive, trustedFolder, diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 672f42bd5b..12687922e9 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -1051,15 +1051,15 @@ describe('Settings Loading and Merging', () => { }); }); - it('should merge chatCompression settings, with workspace taking precedence', () => { + it('should merge compressionThreshold settings, with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { general: {}, - model: { chatCompression: { contextPercentageThreshold: 0.5 } }, + model: { compressionThreshold: 0.5 }, }; const workspaceSettingsContent = { general: {}, - model: { chatCompression: { contextPercentageThreshold: 0.8 } }, + model: { compressionThreshold: 0.8 }, }; (fs.readFileSync as Mock).mockImplementation( @@ -1074,15 +1074,11 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.model?.chatCompression).toEqual({ - contextPercentageThreshold: 0.5, - }); - expect(settings.workspace.settings.model?.chatCompression).toEqual({ - contextPercentageThreshold: 0.8, - }); - expect(settings.merged.model?.chatCompression).toEqual({ - contextPercentageThreshold: 0.8, - }); + expect(settings.user.settings.model?.compressionThreshold).toEqual(0.5); + expect(settings.workspace.settings.model?.compressionThreshold).toEqual( + 0.8, + ); + expect(settings.merged.model?.compressionThreshold).toEqual(0.8); }); it('should merge output format settings, with workspace taking precedence', () => { @@ -1109,13 +1105,13 @@ describe('Settings Loading and Merging', () => { expect(settings.merged.output?.format).toBe('json'); }); - it('should handle chatCompression when only in user settings', () => { + it('should handle compressionThreshold when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { general: {}, - model: { chatCompression: { contextPercentageThreshold: 0.5 } }, + model: { compressionThreshold: 0.5 }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -1126,9 +1122,7 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.model?.chatCompression).toEqual({ - contextPercentageThreshold: 0.5, - }); + expect(settings.merged.model?.compressionThreshold).toEqual(0.5); }); it('should have model as undefined if not in any settings file', () => { @@ -1138,39 +1132,15 @@ describe('Settings Loading and Merging', () => { expect(settings.merged.model).toBeUndefined(); }); - it('should ignore chatCompression if contextPercentageThreshold is invalid', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - (mockFsExistsSync as Mock).mockImplementation( - (p: fs.PathLike) => p === USER_SETTINGS_PATH, - ); - const userSettingsContent = { - general: {}, - model: { chatCompression: { contextPercentageThreshold: 1.5 } }, - }; - (fs.readFileSync as Mock).mockImplementation( - (p: fs.PathOrFileDescriptor) => { - if (p === USER_SETTINGS_PATH) - return JSON.stringify(userSettingsContent); - return '{}'; - }, - ); - - const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.model?.chatCompression).toEqual({ - contextPercentageThreshold: 1.5, - }); - warnSpy.mockRestore(); - }); - - it('should deep merge chatCompression settings', () => { + it('should use user compressionThreshold if workspace does not define it', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { general: {}, - model: { chatCompression: { contextPercentageThreshold: 0.5 } }, + model: { compressionThreshold: 0.5 }, }; const workspaceSettingsContent = { general: {}, - model: { chatCompression: {} }, + model: {}, }; (fs.readFileSync as Mock).mockImplementation( @@ -1185,9 +1155,7 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.model?.chatCompression).toEqual({ - contextPercentageThreshold: 0.5, - }); + expect(settings.merged.model?.compressionThreshold).toEqual(0.5); }); it('should merge includeDirectories from all scopes', () => { @@ -1972,9 +1940,6 @@ describe('Settings Loading and Merging', () => { }, model: { name: 'gemini-pro', - chatCompression: { - contextPercentageThreshold: 0.5, - }, }, mcpServers: { 'server-1': { @@ -1993,9 +1958,6 @@ describe('Settings Loading and Merging', () => { myTheme: {}, }, model: 'gemini-pro', - chatCompression: { - contextPercentageThreshold: 0.5, - }, mcpServers: { 'server-1': { command: 'node server.js', @@ -2035,9 +1997,6 @@ describe('Settings Loading and Merging', () => { }, model: { name: 'gemini-pro', - chatCompression: { - contextPercentageThreshold: 0.8, - }, }, context: { fileName: 'CONTEXT.md', @@ -2077,9 +2036,6 @@ describe('Settings Loading and Merging', () => { theme: 'dark', usageStatisticsEnabled: false, model: 'gemini-pro', - chatCompression: { - contextPercentageThreshold: 0.8, - }, contextFileName: 'CONTEXT.md', includeDirectories: ['/src'], sandbox: true, diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 0835cdd178..51c67b8b76 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -64,7 +64,7 @@ const MIGRATION_MAP: Record = { autoAccept: 'tools.autoAccept', autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory', bugCommand: 'advanced.bugCommand', - chatCompression: 'model.chatCompression', + chatCompression: 'model.compressionThreshold', checkpointing: 'general.checkpointing', coreTools: 'tools.core', contextFileName: 'context.fileName', diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c01e691f44..7cdc36f860 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -14,7 +14,6 @@ import type { BugCommandSettings, TelemetrySettings, AuthType, - ChatCompressionSettings, } from '@google/gemini-cli-core'; import { DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, @@ -578,14 +577,15 @@ const SETTINGS_SCHEMA = { description: 'Settings for summarizing tool output.', showInDialog: false, }, - chatCompression: { - type: 'object', - label: 'Chat Compression', + compressionThreshold: { + type: 'number', + label: 'Compression Threshold', category: 'Model', requiresRestart: false, - default: undefined as ChatCompressionSettings | undefined, - description: 'Chat compression settings.', - showInDialog: false, + default: 0.2 as number, + description: + 'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).', + showInDialog: true, }, skipNextSpeakerCheck: { type: 'boolean', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 8e267965f2..4d677b2aa5 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -96,10 +96,6 @@ export interface BugCommandSettings { urlTemplate: string; } -export interface ChatCompressionSettings { - contextPercentageThreshold?: number; -} - export interface SummarizeToolOutputSettings { tokenBudget?: number; } @@ -261,7 +257,7 @@ export interface ConfigParameters { folderTrust?: boolean; ideMode?: boolean; loadMemoryFromIncludeDirectories?: boolean; - chatCompression?: ChatCompressionSettings; + compressionThreshold?: number; interactive?: boolean; trustedFolder?: boolean; useRipgrep?: boolean; @@ -354,7 +350,7 @@ export class Config { | undefined; private readonly experimentalZedIntegration: boolean = false; private readonly loadMemoryFromIncludeDirectories: boolean = false; - private readonly chatCompression: ChatCompressionSettings | undefined; + private readonly compressionThreshold: number | undefined; private readonly interactive: boolean; private readonly ptyInfo: string; private readonly trustedFolder: boolean | undefined; @@ -453,7 +449,7 @@ export class Config { this.ideMode = params.ideMode ?? false; this.loadMemoryFromIncludeDirectories = params.loadMemoryFromIncludeDirectories ?? false; - this.chatCompression = params.chatCompression; + this.compressionThreshold = params.compressionThreshold; this.interactive = params.interactive ?? false; this.ptyInfo = params.ptyInfo ?? 'child_process'; this.trustedFolder = params.trustedFolder; @@ -977,8 +973,8 @@ export class Config { this.fileSystemService = fileSystemService; } - getChatCompression(): ChatCompressionSettings | undefined { - return this.chatCompression; + getCompressionThreshold(): number | undefined { + return this.compressionThreshold; } isInteractiveShellEnabled(): boolean { diff --git a/packages/core/src/services/chatCompressionService.test.ts b/packages/core/src/services/chatCompressionService.test.ts index ba5688b458..b07f4f9acf 100644 --- a/packages/core/src/services/chatCompressionService.test.ts +++ b/packages/core/src/services/chatCompressionService.test.ts @@ -72,7 +72,7 @@ describe('findCompressSplitPoint', () => { expect(findCompressSplitPoint(history, 0.8)).toBe(4); }); - it('should return earlier splitpoint if no valid ones are after threshhold', () => { + it('should return earlier splitpoint if no valid ones are after threshold', () => { const history: Content[] = [ { role: 'user', parts: [{ text: 'This is the first message.' }] }, { role: 'model', parts: [{ text: 'This is the second message.' }] }, @@ -116,7 +116,7 @@ describe('ChatCompressionService', () => { getHistory: vi.fn(), } as unknown as GeminiChat; mockConfig = { - getChatCompression: vi.fn(), + getCompressionThreshold: vi.fn(), getContentGenerator: vi.fn(), } as unknown as Config; diff --git a/packages/core/src/services/chatCompressionService.ts b/packages/core/src/services/chatCompressionService.ts index 0c94130398..4adff5d32e 100644 --- a/packages/core/src/services/chatCompressionService.ts +++ b/packages/core/src/services/chatCompressionService.ts @@ -107,8 +107,7 @@ export class ChatCompressionService { // Don't compress if not forced and we are under the limit. if (!force) { const threshold = - config.getChatCompression()?.contextPercentageThreshold ?? - DEFAULT_COMPRESSION_TOKEN_THRESHOLD; + config.getCompressionThreshold() ?? DEFAULT_COMPRESSION_TOKEN_THRESHOLD; if (originalTokenCount < threshold * tokenLimit(model)) { return { newHistory: null,