Compare commits

...

13 Commits

Author SHA1 Message Date
gemini-cli-robot 3d44880ea6 chore(release): v0.12.0-preview.7 2025-10-31 15:29:31 +00:00
gemini-cli-robot 9fbea50ee5 fix(patch): cherry-pick 135d981 to release/v0.12.0-preview.6-pr-12299 to patch version v0.12.0-preview.6 and create version 0.12.0-preview.7 (#12368)
Co-authored-by: Jerop Kipruto <jerop@google.com>
2025-10-31 11:14:57 -04:00
gemini-cli-robot a0198269e8 chore(release): v0.12.0-preview.6 2025-10-31 02:11:16 +00:00
gemini-cli-robot fe44afe892 fix(patch): cherry-pick 3332703 to release/v0.12.0-preview.5-pr-12317 to patch version v0.12.0-preview.5 and create version 0.12.0-preview.6 (#12334)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2025-10-30 18:58:03 -07:00
gemini-cli-robot 2072d90ab2 chore(release): v0.12.0-preview.5 2025-10-31 01:25:18 +00:00
gemini-cli-robot 174462f4a4 fix(patch): cherry-pick 643f2c0 to release/v0.12.0-preview.4-pr-12300 to patch version v0.12.0-preview.4 and create version 0.12.0-preview.5 (#12329)
Co-authored-by: Victor May <mayvic@google.com>
2025-10-31 00:31:43 +00:00
gemini-cli-robot 30dc89c8af chore(release): v0.12.0-preview.4 2025-10-30 23:49:24 +00:00
gemini-cli-robot 66b61a13b4 fix(patch): cherry-pick 68afb72 to release/v0.12.0-preview.3-pr-12306 to patch version v0.12.0-preview.3 and create version 0.12.0-preview.4 (#12327)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2025-10-30 23:35:15 +00:00
gemini-cli-robot cd4bebba9e chore(release): v0.12.0-preview.3 2025-10-30 22:44:31 +00:00
gemini-cli-robot 3876379bf7 fix(patch): cherry-pick 82c1042 to release/v0.12.0-preview.2-pr-12231 to patch version v0.12.0-preview.2 and create version 0.12.0-preview.3 (#12320)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2025-10-30 22:28:07 +00:00
gemini-cli-robot 7124551aa5 chore(release): v0.12.0-preview.2 2025-10-30 22:07:18 +00:00
matt korwel b4a63bf73b Merge commit 'b382ae6803ce21ead2a91682fc58126f3786f15b' into HEAD 2025-10-30 14:52:52 -07:00
gemini-cli-robot fe98c85535 chore(release): v0.12.0-preview.0 2025-10-29 00:57:46 +00:00
29 changed files with 332 additions and 276 deletions
+9
View File
@@ -67,3 +67,12 @@ This document lists the available keyboard shortcuts in the Gemini CLI.
| Shortcut | Description |
| -------- | --------------------------------- |
| `Ctrl+G` | See context CLI received from IDE |
## Meta+key combos on mac
On Mac, all Meta+char combos should work normally except for these three which
are mapped to special functionality.
- `meta+b`: "∫" back one word
- `meta+f`: "ƒ" forward one word
- `meta+m`: "µ" toggle markup view
+6 -4
View File
@@ -539,10 +539,6 @@ Measures tool usage and latency.
- `decision` (string: "accept", "reject", "modify", or "auto_accept", if
applicable)
- `tool_type` (string: "mcp" or "native", if applicable)
- `model_added_lines` (Int, optional)
- `model_removed_lines` (Int, optional)
- `user_added_lines` (Int, optional)
- `user_removed_lines` (Int, optional)
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
- **Attributes**:
@@ -586,6 +582,12 @@ Counts file operations with basic context.
- `extension` (string, optional)
- `programming_language` (string, optional)
- `gemini_cli.lines.changed` (Counter, Int): Number of lines changed (from file
diffs).
- **Attributes**:
- `function_name`
- `type` ("added" or "removed")
##### Chat and Streaming
Resilience counters for compression, invalid chunks, and retries.
-15
View File
@@ -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.
+3 -3
View File
@@ -245,13 +245,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.
+25
View File
@@ -161,6 +161,31 @@ export default tseslint.config(
'default-case': 'error',
},
},
{
// Prevent self-imports in packages
files: ['packages/core/src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
name: '@google/gemini-cli-core',
message: 'Please use relative imports within the @google/gemini-cli-core package.',
},
],
},
},
{
files: ['packages/cli/src/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
name: '@google/gemini-cli',
message: 'Please use relative imports within the @google/gemini-cli package.',
},
],
},
},
{
files: ['packages/*/src/**/*.test.{ts,tsx}'],
plugins: {
+7 -7
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"workspaces": [
"packages/*"
],
@@ -17434,7 +17434,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
@@ -17708,7 +17708,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.16.0",
@@ -17828,7 +17828,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -17969,7 +17969,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -17980,7 +17980,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-preview.7"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -127,7 +127,10 @@
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix --max-warnings 0"
"eslint --fix --max-warnings 0 --no-warn-ignored"
],
"eslint.config.js": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-preview.7"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
+6 -10
View File
@@ -1545,7 +1545,7 @@ describe('loadCliConfig with includeDirectories', () => {
});
});
describe('loadCliConfig chatCompression', () => {
describe('loadCliConfig compressionThreshold', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
@@ -1558,28 +1558,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()).toBe(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();
});
});
+1 -1
View File
@@ -684,7 +684,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,
+15 -59
View File
@@ -1104,15 +1104,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(
@@ -1127,15 +1127,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', () => {
@@ -1162,13 +1158,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) => {
@@ -1179,9 +1175,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', () => {
@@ -1191,39 +1185,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(
@@ -1238,9 +1208,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', () => {
@@ -2025,9 +1993,6 @@ describe('Settings Loading and Merging', () => {
},
model: {
name: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.5,
},
},
mcpServers: {
'server-1': {
@@ -2046,9 +2011,6 @@ describe('Settings Loading and Merging', () => {
myTheme: {},
},
model: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.5,
},
mcpServers: {
'server-1': {
command: 'node server.js',
@@ -2088,9 +2050,6 @@ describe('Settings Loading and Merging', () => {
},
model: {
name: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.8,
},
},
context: {
fileName: 'CONTEXT.md',
@@ -2130,9 +2089,6 @@ describe('Settings Loading and Merging', () => {
theme: 'dark',
usageStatisticsEnabled: false,
model: 'gemini-pro',
chatCompression: {
contextPercentageThreshold: 0.8,
},
contextFileName: 'CONTEXT.md',
includeDirectories: ['/src'],
sandbox: true,
+1 -1
View File
@@ -64,7 +64,7 @@ const MIGRATION_MAP: Record<string, string> = {
autoAccept: 'tools.autoAccept',
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
bugCommand: 'advanced.bugCommand',
chatCompression: 'model.chatCompression',
chatCompression: 'model.compressionThreshold',
checkpointing: 'general.checkpointing',
coreTools: 'tools.core',
contextFileName: 'context.fileName',
+7 -7
View File
@@ -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',
@@ -872,99 +872,118 @@ describe('Kitty Sequence Parsing', () => {
vi.useRealTimers();
});
// Terminals to test
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
describe('Cross-terminal Alt key handling (simulating macOS)', () => {
let originalPlatform: NodeJS.Platform;
// Key mappings: letter -> [keycode, accented character]
const keys: Record<string, [number, string]> = {
a: [97, 'å'],
o: [111, 'ø'],
m: [109, 'µ'],
};
it.each(
terminals.flatMap((terminal) =>
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
if (terminal === 'Ghostty') {
// Ghostty uses kitty protocol sequences
return {
terminal,
key,
chunk: `\x1b[${keycode};3u`,
expected: {
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
kittyProtocol: true,
},
};
} else if (terminal === 'MacTerminal') {
// Mac Terminal sends ESC + letter
return {
terminal,
key,
kitty: false,
chunk: `\x1b${key}`,
expected: {
sequence: `\x1b${key}`,
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
},
};
} else {
// iTerm2 and VSCode send accented characters (å, ø, µ)
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
// gets converted to m with meta:true
return {
terminal,
key,
chunk: accentedChar,
expected: {
name: key,
ctrl: false,
meta: true, // Always expect meta:true after conversion
shift: false,
paste: false,
sequence: accentedChar,
},
};
}
}),
),
)(
'should handle Alt+$key in $terminal',
({
chunk,
expected,
kitty = true,
}: {
chunk: string;
expected: Partial<Key>;
kitty?: boolean;
}) => {
const keyHandler = vi.fn();
const testWrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider kittyProtocolEnabled={kitty}>
{children}
</KeypressProvider>
);
const { result } = renderHook(() => useKeypressContext(), {
wrapper: testWrapper,
beforeEach(() => {
originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
});
act(() => result.current.subscribe(keyHandler));
});
act(() => stdin.write(chunk));
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
});
});
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining(expected),
);
},
);
// Terminals to test
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
// Key mappings: letter -> [keycode, accented character]
const keys: Record<string, [number, string]> = {
b: [98, '\u222B'],
f: [102, '\u0192'],
m: [109, '\u00B5'],
};
it.each(
terminals.flatMap((terminal) =>
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
if (terminal === 'Ghostty') {
// Ghostty uses kitty protocol sequences
return {
terminal,
key,
chunk: `\x1b[${keycode};3u`,
expected: {
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
kittyProtocol: true,
},
};
} else if (terminal === 'MacTerminal') {
// Mac Terminal sends ESC + letter
return {
terminal,
key,
kitty: false,
chunk: `\x1b${key}`,
expected: {
sequence: `\x1b${key}`,
name: key,
ctrl: false,
meta: true,
shift: false,
paste: false,
},
};
} else {
// iTerm2 and VSCode send accented characters (å, ø, µ)
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
// gets converted to m with meta:true
return {
terminal,
key,
chunk: accentedChar,
expected: {
name: key,
ctrl: false,
meta: true, // Always expect meta:true after conversion
shift: false,
paste: false,
sequence: accentedChar,
},
};
}
}),
),
)(
'should handle Alt+$key in $terminal',
({
chunk,
expected,
kitty = true,
}: {
chunk: string;
expected: Partial<Key>;
kitty?: boolean;
}) => {
const keyHandler = vi.fn();
const testWrapper = ({ children }: { children: React.ReactNode }) => (
<KeypressProvider kittyProtocolEnabled={kitty}>
{children}
</KeypressProvider>
);
const { result } = renderHook(() => useKeypressContext(), {
wrapper: testWrapper,
});
act(() => result.current.subscribe(keyHandler));
act(() => stdin.write(chunk));
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining(expected),
);
},
);
});
describe('Backslash key handling', () => {
beforeEach(() => {
@@ -48,33 +48,12 @@ export const PASTE_CODE_TIMEOUT_MS = 50; // Flush incomplete paste code after 50
export const SINGLE_QUOTE = "'";
export const DOUBLE_QUOTE = '"';
const ALT_KEY_CHARACTER_MAP: Record<string, string> = {
'\u00E5': 'a',
'\u222B': 'b',
'\u00E7': 'c',
'\u2202': 'd',
'\u00B4': 'e',
'\u0192': 'f',
'\u00A9': 'g',
'\u02D9': 'h',
'\u02C6': 'i',
'\u2206': 'j',
'\u02DA': 'k',
'\u00AC': 'l',
'\u00B5': 'm',
'\u02DC': 'n',
'\u00F8': 'o',
'\u03C0': 'p',
'\u0153': 'q',
'\u00AE': 'r',
'\u00DF': 's',
'\u2020': 't',
'\u00A8': 'u',
'\u221A': 'v',
'\u2211': 'w',
'\u2248': 'x',
'\u00A5': 'y',
'\u03A9': 'z',
// On Mac, hitting alt+char will yield funny characters.
// Remap these three since we listen for them.
const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
'\u222B': 'b', // "∫" back one word
'\u0192': 'f', // "ƒ" forward one word
'\u00B5': 'm', // "µ" toggle markup view
};
/**
@@ -615,8 +594,8 @@ export function KeypressProvider({
return;
}
const mappedLetter = ALT_KEY_CHARACTER_MAP[key.sequence];
if (mappedLetter && !key.meta) {
const mappedLetter = MAC_ALT_KEY_CHARACTER_MAP[key.sequence];
if (process.platform === 'darwin' && mappedLetter && !key.meta) {
broadcast({
name: mappedLetter,
ctrl: false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
+1 -1
View File
@@ -563,7 +563,7 @@ describe('Server Config (config.ts)', () => {
useModelRouter: true,
});
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
expect(config.getUseModelRouter()).toBe(false);
expect(config.getUseModelRouter()).toBe(true);
});
it('should enable model router by default for other auth types', async () => {
+6 -12
View File
@@ -92,10 +92,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;
@@ -357,7 +353,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;
@@ -460,7 +456,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;
@@ -483,9 +479,7 @@ export class Config {
this.useWriteTodos = params.useWriteTodos ?? false;
this.initialUseModelRouter = params.useModelRouter ?? false;
this.useModelRouter = this.initialUseModelRouter;
this.disableModelRouterForAuth = params.disableModelRouterForAuth ?? [
AuthType.LOGIN_WITH_GOOGLE,
];
this.disableModelRouterForAuth = params.disableModelRouterForAuth ?? [];
this.enableMessageBusIntegration =
params.enableMessageBusIntegration ?? false;
this.codebaseInvestigatorSettings = {
@@ -994,8 +988,8 @@ export class Config {
this.fileSystemService = fileSystemService;
}
getChatCompression(): ChatCompressionSettings | undefined {
return this.chatCompression;
getCompressionThreshold(): number | undefined {
return this.compressionThreshold;
}
isInteractiveShellEnabled(): boolean {
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { coreEvents } from '@google/gemini-cli-core';
import { coreEvents } from '../utils/events.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
@@ -34,7 +34,7 @@ vi.mock('../config/storage.js', () => ({
},
}));
vi.mock('@google/gemini-cli-core', () => ({
vi.mock('../utils/events.js', () => ({
coreEvents: {
emitFeedback: vi.fn(),
},
+1 -1
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { coreEvents } from '@google/gemini-cli-core';
import { coreEvents } from '../utils/events.js';
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { Storage } from '../config/storage.js';
@@ -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;
@@ -17,10 +17,10 @@ import { makeChatCompressionEvent } from '../telemetry/types.js';
import { getInitialChatHistory } from '../utils/environmentContext.js';
/**
* 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.
* 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.
*/
export const COMPRESSION_TOKEN_THRESHOLD = 0.7;
export const DEFAULT_COMPRESSION_TOKEN_THRESHOLD = 0.2;
/**
* The fraction of the latest chat history to keep. A value of 0.3
@@ -104,13 +104,10 @@ export class ChatCompressionService {
const originalTokenCount = uiTelemetryService.getLastPromptTokenCount();
const contextPercentageThreshold =
config.getChatCompression()?.contextPercentageThreshold;
// Don't compress if not forced and we are under the limit.
if (!force) {
const threshold =
contextPercentageThreshold ?? COMPRESSION_TOKEN_THRESHOLD;
config.getCompressionThreshold() ?? DEFAULT_COMPRESSION_TOKEN_THRESHOLD;
if (originalTokenCount < threshold * tokenLimit(model)) {
return {
newHistory: null,
+17 -4
View File
@@ -786,12 +786,16 @@ describe('loggers', () => {
const mockMetrics = {
recordToolCallMetrics: vi.fn(),
recordLinesChanged: vi.fn(),
};
beforeEach(() => {
vi.spyOn(metrics, 'recordToolCallMetrics').mockImplementation(
mockMetrics.recordToolCallMetrics,
);
vi.spyOn(metrics, 'recordLinesChanged').mockImplementation(
mockMetrics.recordLinesChanged,
);
mockLogger.emit.mockReset();
});
@@ -888,10 +892,6 @@ describe('loggers', () => {
success: true,
decision: ToolCallDecision.ACCEPT,
tool_type: 'native',
model_added_lines: 1,
model_removed_lines: 2,
user_added_lines: 5,
user_removed_lines: 6,
},
);
@@ -900,6 +900,19 @@ describe('loggers', () => {
'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
});
expect(mockMetrics.recordLinesChanged).toHaveBeenCalledWith(
mockConfig,
1,
'added',
{ function_name: 'test-function' },
);
expect(mockMetrics.recordLinesChanged).toHaveBeenCalledWith(
mockConfig,
2,
'removed',
{ function_name: 'test-function' },
);
});
it('should log a tool call with a reject decision', () => {
const call: ErroredToolCall = {
+16 -8
View File
@@ -63,6 +63,7 @@ import {
recordTokenUsageMetrics,
recordApiResponseMetrics,
recordAgentRunMetrics,
recordLinesChanged,
} from './metrics.js';
import { isTelemetrySdkInitialized } from './sdk.js';
import type { UiEvent } from './uiTelemetry.js';
@@ -118,15 +119,22 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
success: event.success,
decision: event.decision,
tool_type: event.tool_type,
...(event.metadata
? {
model_added_lines: event.metadata['model_added_lines'],
model_removed_lines: event.metadata['model_removed_lines'],
user_added_lines: event.metadata['user_added_lines'],
user_removed_lines: event.metadata['user_removed_lines'],
}
: {}),
});
if (event.metadata) {
const added = event.metadata['model_added_lines'];
if (typeof added === 'number' && added > 0) {
recordLinesChanged(config, added, 'added', {
function_name: event.function_name,
});
}
const removed = event.metadata['model_removed_lines'];
if (typeof removed === 'number' && removed > 0) {
recordLinesChanged(config, removed, 'removed', {
function_name: event.function_name,
});
}
}
}
export function logToolOutputTruncated(
@@ -94,6 +94,7 @@ describe('Telemetry Metrics', () => {
let recordFlickerFrameModule: typeof import('./metrics.js').recordFlickerFrame;
let recordExitFailModule: typeof import('./metrics.js').recordExitFail;
let recordAgentRunMetricsModule: typeof import('./metrics.js').recordAgentRunMetrics;
let recordLinesChangedModule: typeof import('./metrics.js').recordLinesChanged;
beforeEach(async () => {
vi.resetModules();
@@ -136,6 +137,7 @@ describe('Telemetry Metrics', () => {
recordFlickerFrameModule = metricsJsModule.recordFlickerFrame;
recordExitFailModule = metricsJsModule.recordExitFail;
recordAgentRunMetricsModule = metricsJsModule.recordAgentRunMetrics;
recordLinesChangedModule = metricsJsModule.recordLinesChanged;
const otelApiModule = await import('@opentelemetry/api');
@@ -348,6 +350,53 @@ describe('Telemetry Metrics', () => {
});
});
describe('recordLinesChanged metric', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
getTelemetryEnabled: () => true,
} as unknown as Config;
it('should not record lines added/removed if not initialized', () => {
recordLinesChangedModule(mockConfig, 10, 'added', {
function_name: 'fn',
});
recordLinesChangedModule(mockConfig, 5, 'removed', {
function_name: 'fn',
});
expect(mockCounterAddFn).not.toHaveBeenCalled();
});
it('should record lines added with function_name after initialization', () => {
initializeMetricsModule(mockConfig);
mockCounterAddFn.mockClear();
recordLinesChangedModule(mockConfig, 10, 'added', {
function_name: 'my-fn',
});
expect(mockCounterAddFn).toHaveBeenCalledWith(10, {
'session.id': 'test-session-id',
'installation.id': 'test-installation-id',
'user.email': 'test@example.com',
type: 'added',
function_name: 'my-fn',
});
});
it('should record lines removed with function_name after initialization', () => {
initializeMetricsModule(mockConfig);
mockCounterAddFn.mockClear();
recordLinesChangedModule(mockConfig, 7, 'removed', {
function_name: 'my-fn',
});
expect(mockCounterAddFn).toHaveBeenCalledWith(7, {
'session.id': 'test-session-id',
'installation.id': 'test-installation-id',
'user.email': 'test@example.com',
type: 'removed',
function_name: 'my-fn',
});
});
});
describe('recordFileOperationMetric', () => {
const mockConfig = {
getSessionId: () => 'test-session-id',
+26 -5
View File
@@ -24,6 +24,7 @@ const API_REQUEST_LATENCY = 'gemini_cli.api.request.latency';
const TOKEN_USAGE = 'gemini_cli.token.usage';
const SESSION_COUNT = 'gemini_cli.session.count';
const FILE_OPERATION_COUNT = 'gemini_cli.file.operation.count';
const LINES_CHANGED = 'gemini_cli.lines.changed';
const INVALID_CHUNK_COUNT = 'gemini_cli.chat.invalid_chunk.count';
const CONTENT_RETRY_COUNT = 'gemini_cli.chat.content_retry.count';
const CONTENT_RETRY_FAILURE_COUNT =
@@ -72,11 +73,6 @@ const COUNTER_DEFINITIONS = {
success: boolean;
decision?: 'accept' | 'reject' | 'modify' | 'auto_accept';
tool_type?: 'native' | 'mcp';
// Optional diff statistics for file-modifying tools
model_added_lines?: number;
model_removed_lines?: number;
user_added_lines?: number;
user_removed_lines?: number;
},
},
[API_REQUEST_COUNT]: {
@@ -116,6 +112,15 @@ const COUNTER_DEFINITIONS = {
programming_language?: string;
},
},
[LINES_CHANGED]: {
description: 'Number of lines changed (from file diffs).',
valueType: ValueType.INT,
assign: (c: Counter) => (linesChangedCounter = c),
attributes: {} as {
function_name?: string;
type: 'added' | 'removed';
},
},
[INVALID_CHUNK_COUNT]: {
description: 'Counts invalid chunks received from a stream.',
valueType: ValueType.INT,
@@ -454,6 +459,7 @@ let apiRequestLatencyHistogram: Histogram | undefined;
let tokenUsageCounter: Counter | undefined;
let sessionCounter: Counter | undefined;
let fileOperationCounter: Counter | undefined;
let linesChangedCounter: Counter | undefined;
let chatCompressionCounter: Counter | undefined;
let invalidChunkCounter: Counter | undefined;
let contentRetryCounter: Counter | undefined;
@@ -621,6 +627,21 @@ export function recordFileOperationMetric(
});
}
export function recordLinesChanged(
config: Config,
lines: number,
changeType: 'added' | 'removed',
attributes?: { function_name?: string },
): void {
if (!linesChangedCounter || !isMetricsInitialized) return;
if (!Number.isFinite(lines) || lines <= 0) return;
linesChangedCounter.add(lines, {
...baseMetricDefinition.getCommonAttributes(config),
type: changeType,
...(attributes ?? {}),
});
}
// --- New Metric Recording Functions ---
/**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.12.0-nightly.20251022.0542de95",
"version": "0.12.0-preview.7",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {