Compare commits

...

8 Commits

Author SHA1 Message Date
gemini-cli-robot 72e48451c6 chore(release): v0.13.0 2025-11-07 01:59:55 +00:00
gemini-cli-robot 230056cc41 chore(release): v0.13.0-preview.3 2025-11-07 00:44:02 +00:00
gemini-cli-robot be36bf6193 fix(patch): cherry-pick 36feb73 to release/v0.13.0-preview.2-pr-12658 to patch version v0.13.0-preview.2 and create version 0.13.0-preview.3 (#12663)
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
2025-11-06 16:25:23 -08:00
gemini-cli-robot 37670fe6e6 chore(release): v0.13.0-preview.2 2025-11-05 18:45:46 +00:00
gemini-cli-robot ece06155cc fix(patch): cherry-pick 1611364 to release/v0.13.0-preview.1-pr-12587 to patch version v0.13.0-preview.1 and create version 0.13.0-preview.2 (#12601)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
Co-authored-by: LayorX <yor31117@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-05 17:54:04 +00:00
gemini-cli-robot 13b443af4f chore(release): v0.13.0-preview.1 2025-11-05 17:26:20 +00:00
gemini-cli-robot fd885a3e50 fix(patch): cherry-pick f51d745 to release/v0.13.0-preview.0-pr-12586 to patch version v0.13.0-preview.0 and create version 0.13.0-preview.1 (#12595)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2025-11-05 17:04:36 +00:00
gemini-cli-robot 25d7a8037f chore(release): v0.13.0-preview.0 2025-11-04 23:49:33 +00:00
22 changed files with 214 additions and 74 deletions
+1 -1
View File
@@ -467,7 +467,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`useWriteTodos`** (boolean):
- **Description:** Enable the write_todos_list tool.
- **Default:** `true`
- **Default:** `false`
#### `security`
+3 -4
View File
@@ -6,8 +6,7 @@ This document describes the `write_todos` tool for the Gemini CLI.
The `write_todos` tool allows the Gemini agent to create and manage a list of
subtasks for complex user requests. This provides you, the user, with greater
visibility into the agent's plan and its current progress. It also helps with
alignment where the agent is less likely to lose track of its current goal.
visibility into the agent's plan and its current progress.
### Arguments
@@ -50,8 +49,8 @@ write_todos({
## Important notes
- **Enabling:** This tool is enabled by default. You can disable it in your
`settings.json` file by setting `"useWriteTodos": false`.
- **Enabling:** This tool is disabled by default. To use it, you must enable it
in your `settings.json` file by setting `"useWriteTodos": true`.
- **Intended Use:** This tool is primarily used by the agent for complex,
multi-turn tasks. It is generally not used for simple, single-turn questions.
+7 -7
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"workspaces": [
"packages/*"
],
@@ -16959,7 +16959,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
@@ -17249,7 +17249,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.16.0",
@@ -17349,7 +17349,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -17493,7 +17493,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -17504,7 +17504,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"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.13.0-nightly.20251031.c89bc30d"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0-nightly.20251031.c89bc30d"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.13.0"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
+1 -1
View File
@@ -1047,7 +1047,7 @@ const SETTINGS_SCHEMA = {
label: 'Use Write Todos',
category: 'Advanced',
requiresRestart: false,
default: true,
default: false,
description: 'Enable the write_todos_list tool.',
showInDialog: false,
},
+37
View File
@@ -1709,4 +1709,41 @@ describe('AppContainer State Management', () => {
unmount();
});
});
describe('Shell Interaction', () => {
it('should not crash if resizing the pty fails', async () => {
const resizePtySpy = vi
.spyOn(ShellExecutionService, 'resizePty')
.mockImplementation(() => {
throw new Error('Cannot resize a pty that has already exited');
});
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
activePtyId: 'some-pty-id', // Make sure activePtyId is set
});
// The main assertion is that the render does not throw.
const { unmount } = render(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(resizePtySpy).toHaveBeenCalled();
unmount();
});
});
});
+21 -5
View File
@@ -805,11 +805,27 @@ Logging in with Google... Please restart Gemini CLI to continue.
useEffect(() => {
if (activePtyId) {
ShellExecutionService.resizePty(
activePtyId,
Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
Math.max(Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), 1),
);
try {
ShellExecutionService.resizePty(
activePtyId,
Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
Math.max(
Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING),
1,
),
);
} catch (e) {
// This can happen in a race condition where the pty exits
// right before we try to resize it.
if (
!(
e instanceof Error &&
e.message.includes('Cannot resize a pty that has already exited')
)
) {
throw e;
}
}
}
}, [terminalWidth, availableTerminalHeight, activePtyId]);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
+1 -1
View File
@@ -494,7 +494,7 @@ export class Config {
params.truncateToolOutputLines ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES;
this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true;
this.useSmartEdit = params.useSmartEdit ?? true;
this.useWriteTodos = params.useWriteTodos ?? true;
this.useWriteTodos = params.useWriteTodos ?? false;
this.initialUseModelRouter = params.useModelRouter ?? false;
this.useModelRouter = this.initialUseModelRouter;
this.disableModelRouterForAuth = params.disableModelRouterForAuth ?? [];
@@ -1553,7 +1553,7 @@ describe('CoreToolScheduler request queueing', () => {
expect(statusUpdates).toContain('awaiting_approval');
expect(executeFn).not.toHaveBeenCalled();
expect(onAllToolCallsComplete).not.toHaveBeenCalled();
});
}, 20000);
it('should handle two synchronous calls to schedule', async () => {
const executeFn = vi.fn().mockResolvedValue({
@@ -351,6 +351,23 @@ describe('ShellExecutionService', () => {
expect(mockHeadlessTerminal.scrollLines).toHaveBeenCalledWith(10);
});
it('should not throw when resizing a pty that has already exited (Windows)', () => {
const resizeError = new Error(
'Cannot resize a pty that has already exited',
);
mockPtyProcess.resize.mockImplementation(() => {
throw resizeError;
});
// This should catch the specific error and not re-throw it.
expect(() => {
ShellExecutionService.resizePty(mockPtyProcess.pid, 100, 40);
}).not.toThrow();
expect(mockPtyProcess.resize).toHaveBeenCalledWith(100, 40);
expect(mockHeadlessTerminal.resize).not.toHaveBeenCalled();
});
});
describe('Failed Execution', () => {
@@ -753,7 +770,7 @@ describe('ShellExecutionService child_process fallback', () => {
expect(onOutputEventMock).not.toHaveBeenCalled();
});
it('should truncate stdout using a sliding window and show a warning', async () => {
it.skip('should truncate stdout using a sliding window and show a warning', async () => {
const MAX_SIZE = 16 * 1024 * 1024;
const chunk1 = 'a'.repeat(MAX_SIZE / 2 - 5);
const chunk2 = 'b'.repeat(MAX_SIZE / 2 - 5);
@@ -781,7 +798,7 @@ describe('ShellExecutionService child_process fallback', () => {
outputWithoutMessage.startsWith(expectedStart.substring(0, 10)),
).toBe(true);
expect(outputWithoutMessage.endsWith('c'.repeat(20))).toBe(true);
}, 20000);
}, 120000);
});
describe('Failed Execution', () => {
@@ -771,9 +771,11 @@ export class ShellExecutionService {
if (
e instanceof Error &&
(('code' in e && e.code === 'ESRCH') ||
e.message === 'Cannot resize a pty that has already exited')
e.message.includes('Cannot resize a pty that has already exited'))
) {
// ignore
// On Unix, we get an ESRCH error.
// On Windows, we get a message-based error.
// In both cases, it's safe to ignore.
} else {
throw e;
}
+28 -24
View File
@@ -232,30 +232,34 @@ describe('ShellTool', () => {
);
});
itWindowsOnly('should not wrap command on windows', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const invocation = shellTool.build({ command: 'dir' });
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({
rawOutput: Buffer.from(''),
output: '',
exitCode: 0,
signal: null,
error: null,
aborted: false,
pid: 12345,
executionMethod: 'child_process',
});
await promise;
expect(mockShellExecutionService).toHaveBeenCalledWith(
'dir',
'/test/dir',
expect.any(Function),
mockAbortSignal,
false,
{},
);
});
itWindowsOnly(
'should not wrap command on windows',
async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const invocation = shellTool.build({ command: 'dir' });
const promise = invocation.execute(mockAbortSignal);
resolveShellExecution({
rawOutput: Buffer.from(''),
output: '',
exitCode: 0,
signal: null,
error: null,
aborted: false,
pid: 12345,
executionMethod: 'child_process',
});
await promise;
expect(mockShellExecutionService).toHaveBeenCalledWith(
'dir',
'/test/dir',
expect.any(Function),
mockAbortSignal,
false,
{},
);
},
20000,
);
it('should format error messages correctly', async () => {
const error = new Error('wrapped command failed');
@@ -25,6 +25,44 @@ describe('classifyGoogleError', () => {
expect(result).toBe(regularError);
});
it('should return RetryableQuotaError when message contains "Please retry in Xs"', () => {
const complexError = {
error: {
message:
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s."}]}}',
code: 429,
status: 'Too Many Requests',
},
};
const rawError = new Error(JSON.stringify(complexError));
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
const result = classifyGoogleError(rawError);
expect(result).toBeInstanceOf(RetryableQuotaError);
expect((result as RetryableQuotaError).retryDelayMs).toBe(44097.740004);
expect((result as RetryableQuotaError).message).toBe(rawError.message);
});
it('should return RetryableQuotaError when error is a string and message contains "Please retry in Xms"', () => {
const complexErrorString = JSON.stringify({
error: {
message:
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms."}]}}',
code: 429,
status: 'Too Many Requests',
},
});
const rawError = new Error(complexErrorString);
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
const result = classifyGoogleError(rawError);
expect(result).toBeInstanceOf(RetryableQuotaError);
expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(900.2);
expect((result as RetryableQuotaError).message).toBe(rawError.message);
});
it('should return original error if code is not 429', () => {
const apiError: GoogleApiError = {
code: 500,
+28 -5
View File
@@ -43,16 +43,20 @@ export class RetryableQuotaError extends Error {
}
/**
* Parses a duration string (e.g., "34.074824224s", "60s") and returns the time in seconds.
* Parses a duration string (e.g., "34.074824224s", "60s", "900ms") and returns the time in seconds.
* @param duration The duration string to parse.
* @returns The duration in seconds, or null if parsing fails.
*/
function parseDurationInSeconds(duration: string): number | null {
if (!duration.endsWith('s')) {
return null;
if (duration.endsWith('ms')) {
const milliseconds = parseFloat(duration.slice(0, -2));
return isNaN(milliseconds) ? null : milliseconds / 1000;
}
const seconds = parseFloat(duration.slice(0, -1));
return isNaN(seconds) ? null : seconds;
if (duration.endsWith('s')) {
const seconds = parseFloat(duration.slice(0, -1));
return isNaN(seconds) ? null : seconds;
}
return null;
}
/**
@@ -64,6 +68,7 @@ function parseDurationInSeconds(duration: string): number | null {
* - If the error suggests a retry delay of more than 2 minutes, it's a `TerminalQuotaError`.
* - If the error suggests a retry delay of 2 minutes or less, it's a `RetryableQuotaError`.
* - If the error indicates a per-minute limit, it's a `RetryableQuotaError`.
* - If the error message contains the phrase "Please retry in X[s|ms]", it's a `RetryableQuotaError`.
*
* @param error The error to classify.
* @returns A `TerminalQuotaError`, `RetryableQuotaError`, or the original `unknown` error.
@@ -72,6 +77,24 @@ export function classifyGoogleError(error: unknown): unknown {
const googleApiError = parseGoogleApiError(error);
if (!googleApiError || googleApiError.code !== 429) {
// Fallback: try to parse the error message for a retry delay
const errorMessage = error instanceof Error ? error.message : String(error);
const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
if (match?.[1]) {
const retryDelaySeconds = parseDurationInSeconds(match[1]);
if (retryDelaySeconds !== null) {
return new RetryableQuotaError(
errorMessage,
googleApiError ?? {
code: 429,
message: errorMessage,
details: [],
},
retryDelaySeconds,
);
}
}
return error; // Not a 429 error we can handle.
}
@@ -83,18 +83,21 @@ describe('WorkspaceContext with real filesystem', () => {
expect(directories).toHaveLength(2);
});
it('should handle symbolic links correctly', () => {
const realDir = path.join(tempDir, 'real');
fs.mkdirSync(realDir, { recursive: true });
const symlinkDir = path.join(tempDir, 'symlink-to-real');
fs.symlinkSync(realDir, symlinkDir, 'dir');
const workspaceContext = new WorkspaceContext(cwd);
workspaceContext.addDirectory(symlinkDir);
it.skipIf(os.platform() === 'win32')(
'should handle symbolic links correctly',
() => {
const realDir = path.join(tempDir, 'real');
fs.mkdirSync(realDir, { recursive: true });
const symlinkDir = path.join(tempDir, 'symlink-to-real');
fs.symlinkSync(realDir, symlinkDir, 'dir');
const workspaceContext = new WorkspaceContext(cwd);
workspaceContext.addDirectory(symlinkDir);
const directories = workspaceContext.getDirectories();
const directories = workspaceContext.getDirectories();
expect(directories).toEqual([cwd, realDir]);
});
expect(directories).toEqual([cwd, realDir]);
},
);
});
describe('path validation', () => {
@@ -158,7 +161,7 @@ describe('WorkspaceContext with real filesystem', () => {
);
});
describe('with symbolic link', () => {
describe.skipIf(os.platform() === 'win32')('with symbolic link', () => {
describe('in the workspace', () => {
let realDir: string;
let symlinkDir: string;
+1
View File
@@ -9,6 +9,7 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: ['default', 'junit'],
timeout: 30000,
silent: true,
setupFiles: ['./test-setup.ts'],
outputFile: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"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.13.0-nightly.20251031.c89bc30d",
"version": "0.13.0",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
+2 -2
View File
@@ -675,8 +675,8 @@
"useWriteTodos": {
"title": "Use Write Todos",
"description": "Enable the write_todos_list tool.",
"markdownDescription": "Enable the write_todos_list tool.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"markdownDescription": "Enable the write_todos_list tool.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"security": {