mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-25 09:10:59 -07:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d44880ea6 | |||
| 9fbea50ee5 | |||
| a0198269e8 | |||
| fe44afe892 | |||
| 2072d90ab2 | |||
| 174462f4a4 | |||
| 30dc89c8af | |||
| 66b61a13b4 | |||
| cd4bebba9e | |||
| 3876379bf7 | |||
| 7124551aa5 | |||
| b4a63bf73b | |||
| fe98c85535 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Generated
+7
-7
@@ -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
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user