From 3e004048cf8e8fe77c5cc48b10756ecaa14a846f Mon Sep 17 00:00:00 2001 From: matt korwel Date: Wed, 13 Aug 2025 23:14:25 -0700 Subject: [PATCH 001/277] chore(release): v0.1.21 (#6207) Co-authored-by: gemini-cli-robot --- package-lock.json | 10 +++++----- package.json | 4 ++-- packages/cli/package.json | 4 ++-- packages/core/package.json | 2 +- packages/test-utils/package.json | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9867d148..4677fa3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@google/gemini-cli", - "version": "0.1.19", + "version": "0.1.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@google/gemini-cli", - "version": "0.1.19", + "version": "0.1.21", "workspaces": [ "packages/*" ], @@ -12307,7 +12307,7 @@ }, "packages/cli": { "name": "@google/gemini-cli", - "version": "0.1.19", + "version": "0.1.21", "dependencies": { "@google/gemini-cli-core": "file:../core", "@google/genai": "1.13.0", @@ -12490,7 +12490,7 @@ }, "packages/core": { "name": "@google/gemini-cli-core", - "version": "0.1.19", + "version": "0.1.21", "dependencies": { "@google/genai": "1.13.0", "@modelcontextprotocol/sdk": "^1.11.0", @@ -12596,7 +12596,7 @@ }, "packages/test-utils": { "name": "@google/gemini-cli-test-utils", - "version": "0.1.19", + "version": "0.1.21", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" diff --git a/package.json b/package.json index 8b6d7295..8a40681e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.1.19", + "version": "0.1.21", "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.1.19" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.21" }, "scripts": { "start": "node scripts/start.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 0e087804..a460920e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.1.19", + "version": "0.1.21", "description": "Gemini CLI", "repository": { "type": "git", @@ -25,7 +25,7 @@ "dist" ], "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.19" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.21" }, "dependencies": { "@google/gemini-cli-core": "file:../core", diff --git a/packages/core/package.json b/packages/core/package.json index fac517fd..6f670f2c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-core", - "version": "0.1.19", + "version": "0.1.21", "description": "Gemini CLI Core", "repository": { "type": "git", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index cb93c941..51359c5c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-test-utils", - "version": "0.1.19", + "version": "0.1.21", "private": true, "main": "src/index.ts", "license": "Apache-2.0", From dd55a82a2891fc2e9b197cf491fb8205d1ba9619 Mon Sep 17 00:00:00 2001 From: owenofbrien <86964623+owenofbrien@users.noreply.github.com> Date: Thu, 14 Aug 2025 05:12:26 -0500 Subject: [PATCH 002/277] Log CLI version and git commit hash (v2) (#6176) --- .gitignore | 1 + .../clearcut-logger/clearcut-logger.ts | 9 ++++++++ .../clearcut-logger/event-metadata-key.ts | 6 +++++ scripts/generate-git-commit-info.js | 23 +++++++++++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index af3591bd..fbbb2dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ packages/*/coverage/ # Generated files packages/cli/src/generated/ +packages/core/src/generated/ .integration-tests/ packages/vscode-ide-companion/*.vsix diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 7ccfd440..60a31ae7 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -30,6 +30,7 @@ import { } from '../../utils/user_account.js'; import { getInstallationId } from '../../utils/user_id.js'; import { FixedDeque } from 'mnemonist'; +import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js'; import { DetectedIde, detectIde } from '../../ide/detect-ide.js'; const start_session_event_name = 'start_session'; @@ -374,6 +375,14 @@ export class ClearcutLogger { EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED, value: event.telemetry_log_user_prompts_enabled.toString(), }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_VERSION, + value: CLI_VERSION, + }, + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_GIT_COMMIT_HASH, + value: GIT_COMMIT_INFO, + }, ]; // Flush start event immediately diff --git a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts index cb4172ed..66797caa 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -157,6 +157,12 @@ export enum EventMetadataKey { // Logs the session id GEMINI_CLI_SESSION_ID = 40, + // Logs the Gemini CLI version + GEMINI_CLI_VERSION = 54, + + // Logs the Gemini CLI Git commit hash + GEMINI_CLI_GIT_COMMIT_HASH = 55, + // ========================================================================== // Loop Detected Event Keys // =========================================================================== diff --git a/scripts/generate-git-commit-info.js b/scripts/generate-git-commit-info.js index 7c4871ec..64b3c748 100644 --- a/scripts/generate-git-commit-info.js +++ b/scripts/generate-git-commit-info.js @@ -21,16 +21,24 @@ import { execSync } from 'child_process'; import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { dirname, join, relative } from 'path'; import { fileURLToPath } from 'url'; +import { readPackageUp } from 'read-package-up'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '..'); const scriptPath = relative(root, fileURLToPath(import.meta.url)); -const generatedDir = join(root, 'packages/cli/src/generated'); -const gitCommitFile = join(generatedDir, 'git-commit.ts'); +const generatedCliDir = join(root, 'packages/cli/src/generated'); +const cliGitCommitFile = join(generatedCliDir, 'git-commit.ts'); +const generatedCoreDir = join(root, 'packages/core/src/generated'); +const coreGitCommitFile = join(generatedCoreDir, 'git-commit.ts'); let gitCommitInfo = 'N/A'; +let cliVersion = 'UNKNOWN'; -if (!existsSync(generatedDir)) { - mkdirSync(generatedDir, { recursive: true }); +if (!existsSync(generatedCliDir)) { + mkdirSync(generatedCliDir, { recursive: true }); +} + +if (!existsSync(generatedCoreDir)) { + mkdirSync(generatedCoreDir, { recursive: true }); } try { @@ -40,6 +48,9 @@ try { if (gitHash) { gitCommitInfo = gitHash; } + + const result = await readPackageUp(); + cliVersion = result?.packageJson?.version ?? 'UNKNOWN'; } catch { // ignore } @@ -53,6 +64,8 @@ const fileContent = `/** // This file is auto-generated by the build script (${scriptPath}) // Do not edit this file manually. export const GIT_COMMIT_INFO = '${gitCommitInfo}'; +export const CLI_VERSION = '${cliVersion}'; `; -writeFileSync(gitCommitFile, fileContent); +writeFileSync(cliGitCommitFile, fileContent); +writeFileSync(coreGitCommitFile, fileContent); From e74dc4d0e0c42c93d3c18232f55544adcc084de1 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Thu, 14 Aug 2025 14:30:16 +0000 Subject: [PATCH 003/277] Update versioning script to also bump version for companion extension so they stay in sync (#6075) --- scripts/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/version.js b/scripts/version.js index 741e6e2e..fc993361 100644 --- a/scripts/version.js +++ b/scripts/version.js @@ -35,7 +35,7 @@ if (!versionType) { run(`npm version ${versionType} --no-git-tag-version --allow-same-version`); // 3. Get all workspaces and filter out the one we don't want to version. -const workspacesToExclude = ['gemini-cli-vscode-ide-companion']; +const workspacesToExclude = []; const lsOutput = JSON.parse( execSync('npm ls --workspaces --json --depth=0').toString(), ); From 2fc1ef7d59780bae9a1705cafbd78bd2567f117e Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Thu, 14 Aug 2025 23:46:32 +0900 Subject: [PATCH 004/277] Docs: update overview of Gemini CLI GitHub Actions (#6198) --- README.md | 50 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3fb86d12..ded6f600 100644 --- a/README.md +++ b/README.md @@ -47,25 +47,32 @@ brew install gemini-cli ## 📋 Key Features -With Gemini CLI you can: +### Code Understanding & Generation -- **Code Understanding & Generation** - - Query and edit large codebases - - Generate new apps from PDFs, images, or sketches using multimodal capabilities - - Debug issues and troubleshoot with natural language -- **Automation & Integration** - - Automate operational tasks like querying pull requests or handling complex rebases - - Use MCP servers to connect new capabilities, including [media generation with Imagen, Veo or Lyria](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio/tree/main/experiments/mcp-genmedia) - - Run non-interactively in scripts for workflow automation -- **Advanced Capabilities** - - Ground your queries with built-in [Google Search](https://ai.google.dev/gemini-api/docs/grounding) for real-time information - - Conversation checkpointing to save and resume complex sessions - - Custom context files (GEMINI.md) to tailor behavior for your projects +- Query and edit large codebases +- Generate new apps from PDFs, images, or sketches using multimodal capabilities +- Debug issues and troubleshoot with natural language -- **🔗 GitHub Integration** - - Use the Gemini CLI GitHub Action for automated PR reviews - - Automated issue triage and on-demand AI assistance directly in your repositories - - Seamless integration with your GitHub workflows +### Automation & Integration + +- Automate operational tasks like querying pull requests or handling complex rebases +- Use MCP servers to connect new capabilities, including [media generation with Imagen, Veo or Lyria](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio/tree/main/experiments/mcp-genmedia) +- Run non-interactively in scripts for workflow automation + +### Advanced Capabilities + +- Ground your queries with built-in [Google Search](https://ai.google.dev/gemini-api/docs/grounding) for real-time information +- Conversation checkpointing to save and resume complex sessions +- Custom context files (GEMINI.md) to tailor behavior for your projects + +### GitHub Integration + +Integrate Gemini CLI directly into your GitHub workflows with [**Gemini CLI GitHub Action**](https://github.com/google-github-actions/run-gemini-cli): + +- **Pull Request Reviews**: Automated code review with contextual feedback and suggestions +- **Issue Triage**: Automated labeling and prioritization of GitHub issues based on content analysis +- **On-demand Assistance**: Mention `@gemini-cli` in issues and pull requests for help with debugging, explanations, or task delegation +- **Custom Workflows**: Build automated, scheduled and on-demand workflows tailored to your team's needs ## 🔐 Authentication Options @@ -176,15 +183,6 @@ gemini > Give me a summary of all of the changes that went in yesterday ```` -## 🔗 GitHub Integration - -Integrate Gemini CLI directly into your GitHub workflows with the [**Gemini CLI GitHub Action**](https://github.com/google-github-actions/run-gemini-cli). Key features include: - -- **Pull Request Reviews**: Automatically review pull requests when they're opened. -- **Issue Triage**: Automatically triage and label GitHub issues. -- **On-demand Collaboration**: Mention `@gemini-cli` in issues and pull requests for assistance and task delegation. -- **Custom Workflows**: Set up your own scheduled tasks and event-driven automations. - ## 📚 Documentation ### Getting Started From d6403c67ee18971829736b78cca354f088f0aeee Mon Sep 17 00:00:00 2001 From: christine betts Date: Thu, 14 Aug 2025 14:57:36 +0000 Subject: [PATCH 005/277] [ide-mode] Suggest the extension name in the installation messages (#6182) --- packages/cli/src/ui/commands/ideCommand.ts | 3 ++- packages/core/src/ide/constants.ts | 7 +++++++ packages/core/src/ide/ide-installer.ts | 5 +++-- packages/core/src/index.ts | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/ide/constants.ts diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts index 2dfad33c..b7cbea3d 100644 --- a/packages/cli/src/ui/commands/ideCommand.ts +++ b/packages/cli/src/ui/commands/ideCommand.ts @@ -7,6 +7,7 @@ import { Config, DetectedIde, + GEMINI_CLI_COMPANION_EXTENSION_NAME, IDEConnectionStatus, getIdeInfo, getIdeInstaller, @@ -170,7 +171,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => { context.ui.addItem( { type: 'error', - text: `No installer is available for ${ideClient.getDetectedIdeDisplayName()}. Please install the IDE companion manually from its marketplace.`, + text: `No installer is available for ${ideClient.getDetectedIdeDisplayName()}. Please install the '${GEMINI_CLI_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`, }, Date.now(), ); diff --git a/packages/core/src/ide/constants.ts b/packages/core/src/ide/constants.ts new file mode 100644 index 00000000..f1f066c4 --- /dev/null +++ b/packages/core/src/ide/constants.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const GEMINI_CLI_COMPANION_EXTENSION_NAME = 'Gemini CLI Companion'; diff --git a/packages/core/src/ide/ide-installer.ts b/packages/core/src/ide/ide-installer.ts index 121e0089..7b9ead77 100644 --- a/packages/core/src/ide/ide-installer.ts +++ b/packages/core/src/ide/ide-installer.ts @@ -10,6 +10,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import { DetectedIde } from './detect-ide.js'; +import { GEMINI_CLI_COMPANION_EXTENSION_NAME } from './constants.js'; const VSCODE_COMMAND = process.platform === 'win32' ? 'code.cmd' : 'code'; @@ -96,7 +97,7 @@ class VsCodeInstaller implements IdeInstaller { if (!commandPath) { return { success: false, - message: `VS Code CLI not found. Please ensure 'code' is in your system's PATH. For help, see https://code.visualstudio.com/docs/configure/command-line#_code-is-not-recognized-as-an-internal-or-external-command. You can also install the companion extension manually from the VS Code marketplace.`, + message: `VS Code CLI not found. Please ensure 'code' is in your system's PATH. For help, see https://code.visualstudio.com/docs/configure/command-line#_code-is-not-recognized-as-an-internal-or-external-command. You can also install the '${GEMINI_CLI_COMPANION_EXTENSION_NAME}' extension manually from the VS Code marketplace.`, }; } @@ -111,7 +112,7 @@ class VsCodeInstaller implements IdeInstaller { } catch (_error) { return { success: false, - message: `Failed to install VS Code companion extension. Please try installing it manually from the VS Code marketplace.`, + message: `Failed to install VS Code companion extension. Please try installing '${GEMINI_CLI_COMPANION_EXTENSION_NAME}' manually from the VS Code extension marketplace.`, }; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a24cddbe..e2cefddd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -52,6 +52,7 @@ export * from './ide/ide-client.js'; export * from './ide/ideContext.js'; export * from './ide/ide-installer.js'; export { getIdeInfo, DetectedIde, IdeInfo } from './ide/detect-ide.js'; +export * from './ide/constants.js'; // Export Shell Execution Service export * from './services/shellExecutionService.js'; From 8bebaedad4c82c50f570dc65c13feabcbb8444ef Mon Sep 17 00:00:00 2001 From: Richie Foreman Date: Thu, 14 Aug 2025 12:14:02 -0400 Subject: [PATCH 006/277] chore(vscode): Add eslint as a recommended extension (#6196) --- .vscode/extensions.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 183356b0..cbdbfd17 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["vitest.explorer", "esbenp.prettier-vscode"] + "recommendations": [ + "vitest.explorer", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint" + ] } From 4973e7e1e04004547021986a0e2dc5eeb4a8bf9d Mon Sep 17 00:00:00 2001 From: Kamal Raj Sekar Date: Thu, 14 Aug 2025 22:00:30 +0530 Subject: [PATCH 007/277] /chat save command saves empty conversations with only system context (#6121) Co-authored-by: Jacob Richman --- .../cli/src/ui/commands/chatCommand.test.ts | 44 ++++++++++--------- packages/cli/src/ui/commands/chatCommand.ts | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/ui/commands/chatCommand.test.ts b/packages/cli/src/ui/commands/chatCommand.test.ts index ccdfd4b2..c7299883 100644 --- a/packages/cli/src/ui/commands/chatCommand.test.ts +++ b/packages/cli/src/ui/commands/chatCommand.test.ts @@ -185,30 +185,32 @@ describe('chatCommand', () => { }); }); - it('should inform if conversation history is empty', async () => { + it('should inform if conversation history is empty or only contains system context', async () => { mockGetHistory.mockReturnValue([]); - const result = await saveCommand?.action?.(mockContext, tag); + let result = await saveCommand?.action?.(mockContext, tag); expect(result).toEqual({ type: 'message', messageType: 'info', content: 'No conversation found to save.', }); - }); - it('should save the conversation if checkpoint does not exist', async () => { - const history: HistoryItemWithoutId[] = [ - { - type: 'user', - text: 'hello', - }, - ]; - mockGetHistory.mockReturnValue(history); - mockCheckpointExists.mockResolvedValue(false); + mockGetHistory.mockReturnValue([ + { role: 'user', parts: [{ text: 'context for our chat' }] }, + { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] }, + ]); + result = await saveCommand?.action?.(mockContext, tag); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: 'No conversation found to save.', + }); - const result = await saveCommand?.action?.(mockContext, tag); - - expect(mockCheckpointExists).toHaveBeenCalledWith(tag); - expect(mockSaveCheckpoint).toHaveBeenCalledWith(history, tag); + mockGetHistory.mockReturnValue([ + { role: 'user', parts: [{ text: 'context for our chat' }] }, + { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] }, + { role: 'user', parts: [{ text: 'Hello, how are you?' }] }, + ]); + result = await saveCommand?.action?.(mockContext, tag); expect(result).toEqual({ type: 'message', messageType: 'info', @@ -237,11 +239,11 @@ describe('chatCommand', () => { }); it('should save the conversation if overwrite is confirmed', async () => { - const history: HistoryItemWithoutId[] = [ - { - type: 'user', - text: 'hello', - }, + const history: Content[] = [ + { role: 'user', parts: [{ text: 'context for our chat' }] }, + { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] }, + { role: 'user', parts: [{ text: 'hello' }] }, + { role: 'model', parts: [{ text: 'Hi there!' }] }, ]; mockGetHistory.mockReturnValue(history); mockContext.overwriteConfirmed = true; diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 56eebe1a..14714df3 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -142,7 +142,7 @@ const saveCommand: SlashCommand = { } const history = chat.getHistory(); - if (history.length > 0) { + if (history.length > 2) { await logger.saveCheckpoint(history, tag); return { type: 'message', From ef54f720de0ed9e94677504b21d437a79fec89fd Mon Sep 17 00:00:00 2001 From: joshualitt Date: Thu, 14 Aug 2025 10:35:34 -0700 Subject: [PATCH 008/277] bug(cli): Exclude only specific tests. (#6244) --- packages/cli/src/test-utils/customMatchers.ts | 10 +-- .../cli/src/test-utils/mockCommandContext.ts | 3 +- packages/cli/tsconfig.json | 69 ++++++++++++++++++- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index c0b4df6b..26eac07b 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -12,15 +12,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { expect } from 'vitest'; +import { Assertion, expect } from 'vitest'; import type { TextBuffer } from '../ui/components/shared/text-buffer.js'; // RegExp to detect invalid characters: backspace, and ANSI escape codes // eslint-disable-next-line no-control-regex const invalidCharsRegex = /[\b\x1b]/; -function toHaveOnlyValidCharacters(this: vi.Assertion, buffer: TextBuffer) { - const { isNot } = this; +function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { isNot } = this as any; let pass = true; const invalidLines: Array<{ line: number; content: string }> = []; @@ -50,7 +51,8 @@ function toHaveOnlyValidCharacters(this: vi.Assertion, buffer: TextBuffer) { expect.extend({ toHaveOnlyValidCharacters, -}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as any); // Extend Vitest's `expect` interface with the custom matcher's type definition. declare module 'vitest' { diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 4137dbff..12b8d096 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -54,7 +54,8 @@ export const createMockCommandContext = ( loadHistory: vi.fn(), toggleCorgiMode: vi.fn(), toggleVimEnabled: vi.fn(), - }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, session: { sessionShellAllowlist: new Set(), stats: { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 55be9a03..65324f37 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -16,9 +16,72 @@ "exclude": [ "node_modules", "dist", - "src/**/*.test.ts", - "src/**/*.test.tsx", - "src/test-utils" + // TODO(5691): Fix type errors and remove excludes. + "src/commands/mcp.test.ts", + "src/commands/mcp/add.test.ts", + "src/commands/mcp/list.test.ts", + "src/commands/mcp/remove.test.ts", + "src/config/config.integration.test.ts", + "src/config/config.test.ts", + "src/config/extension.test.ts", + "src/config/settings.test.ts", + "src/nonInteractiveCli.test.ts", + "src/services/FileCommandLoader.test.ts", + "src/services/prompt-processors/argumentProcessor.test.ts", + "src/utils/cleanup.test.ts", + "src/utils/handleAutoUpdate.test.ts", + "src/utils/startupWarnings.test.ts", + "src/ui/App.test.tsx", + "src/ui/commands/aboutCommand.test.ts", + "src/ui/commands/authCommand.test.ts", + "src/ui/commands/bugCommand.test.ts", + "src/ui/commands/clearCommand.test.ts", + "src/ui/commands/compressCommand.test.ts", + "src/ui/commands/copyCommand.test.ts", + "src/ui/commands/corgiCommand.test.ts", + "src/ui/commands/docsCommand.test.ts", + "src/ui/commands/editorCommand.test.ts", + "src/ui/commands/extensionsCommand.test.ts", + "src/ui/commands/helpCommand.test.ts", + "src/ui/commands/restoreCommand.test.ts", + "src/ui/commands/settingsCommand.test.ts", + "src/ui/commands/themeCommand.test.ts", + "src/ui/commands/chatCommand.test.ts", + "src/ui/commands/directoryCommand.test.tsx", + "src/ui/commands/ideCommand.test.ts", + "src/ui/commands/initCommand.test.ts", + "src/ui/commands/privacyCommand.test.ts", + "src/ui/commands/quitCommand.test.ts", + "src/ui/commands/mcpCommand.test.ts", + "src/ui/commands/memoryCommand.test.ts", + "src/ui/commands/statsCommand.test.ts", + "src/ui/commands/terminalSetupCommand.test.ts", + "src/ui/commands/toolsCommand.test.ts", + "src/ui/components/ContextSummaryDisplay.test.tsx", + "src/ui/components/Footer.test.tsx", + "src/ui/components/InputPrompt.test.tsx", + "src/ui/components/ModelStatsDisplay.test.tsx", + "src/ui/components/SessionSummaryDisplay.test.tsx", + "src/ui/components/shared/text-buffer.test.ts", + "src/ui/components/shared/vim-buffer-actions.test.ts", + "src/ui/components/StatsDisplay.test.tsx", + "src/ui/components/ToolStatsDisplay.test.tsx", + "src/ui/contexts/SessionContext.test.tsx", + "src/ui/hooks/slashCommandProcessor.test.ts", + "src/ui/hooks/useAtCompletion.test.ts", + "src/ui/hooks/useConsoleMessages.test.ts", + "src/ui/hooks/useCommandCompletion.test.ts", + "src/ui/hooks/useFocus.test.ts", + "src/ui/hooks/useFolderTrust.test.ts", + "src/ui/hooks/useGeminiStream.test.tsx", + "src/ui/hooks/useKeypress.test.ts", + "src/ui/hooks/usePhraseCycler.test.ts", + "src/ui/hooks/useToolScheduler.test.ts", + "src/ui/hooks/vim.test.ts", + "src/ui/utils/computeStats.test.ts", + "src/ui/themes/theme.test.ts", + "src/validateNonInterActiveAuth.test.ts", + "src/services/prompt-processors/shellProcessor.test.ts" ], "references": [{ "path": "../core" }] } From 2416a80e9c1d12718e5cf02db8582cff4c9a5942 Mon Sep 17 00:00:00 2001 From: christine betts Date: Thu, 14 Aug 2025 17:47:36 +0000 Subject: [PATCH 009/277] [ide-mode] Add docs on running the vscode companion extension locally (#6145) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Shreya Co-authored-by: Shreya Keshive --- packages/vscode-ide-companion/README.md | 2 +- packages/vscode-ide-companion/development.md | 30 ++++++++++++++++++++ packages/vscode-ide-companion/package.json | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 packages/vscode-ide-companion/development.md diff --git a/packages/vscode-ide-companion/README.md b/packages/vscode-ide-companion/README.md index ebc751e6..6e6db38d 100644 --- a/packages/vscode-ide-companion/README.md +++ b/packages/vscode-ide-companion/README.md @@ -1,6 +1,6 @@ # Gemini CLI Companion -The Gemini CLI Companion extension seamlessly integrates [Gemini CLI](https://github.com/google-gemini/gemini-cli) into your IDE. +The Gemini CLI Companion extension pairs with [Gemini CLI](https://github.com/google-gemini/gemini-cli). This extension is compatible with both VS Code and VS Code forks. # Features diff --git a/packages/vscode-ide-companion/development.md b/packages/vscode-ide-companion/development.md new file mode 100644 index 00000000..0fdc93f2 --- /dev/null +++ b/packages/vscode-ide-companion/development.md @@ -0,0 +1,30 @@ +# Local Development + +## Running the Extension + +To run the extension locally for development: + +1. From the root of the repository, install dependencies: + ```bash + npm install + ``` +2. Open this directory (`packages/vscode-ide-companion`) in VS Code. +3. Compile the extension: + ```bash + npm run compile + ``` +4. Press `F5` (fn+f5 on mac) to open a new Extension Development Host window with the extension running. + +To watch for changes and have the extension rebuild automatically, run: + +```bash +npm run watch +``` + +## Running Tests + +To run the automated tests, run: + +```bash +npm run test +``` diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 4cbf9bb1..187da3e8 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -1,7 +1,7 @@ { "name": "gemini-cli-vscode-ide-companion", "displayName": "Gemini CLI Companion", - "description": "Enable Gemini CLI with direct access to your VS Code workspace.", + "description": "Enable Gemini CLI with direct access to your IDE workspace.", "version": "0.1.19", "publisher": "google", "icon": "assets/icon.png", From 798c4d1311dc9e415caf422f5eb6a7d7e65f0a7a Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Thu, 14 Aug 2025 17:50:20 +0000 Subject: [PATCH 010/277] Update IDE integration context toggle shortcut to ctrl+G (#6245) --- docs/keyboard-shortcuts.md | 6 ++++++ packages/cli/src/config/keyBindings.ts | 4 ++-- packages/cli/src/ui/App.test.tsx | 6 +++--- .../cli/src/ui/components/ContextSummaryDisplay.test.tsx | 6 +++--- packages/cli/src/ui/components/ContextSummaryDisplay.tsx | 2 +- .../__snapshots__/IDEContextDetailDisplay.test.tsx.snap | 4 ++-- packages/cli/src/ui/keyMatchers.test.ts | 6 +++--- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/keyboard-shortcuts.md b/docs/keyboard-shortcuts.md index 37e47045..f9720982 100644 --- a/docs/keyboard-shortcuts.md +++ b/docs/keyboard-shortcuts.md @@ -60,3 +60,9 @@ This document lists the available keyboard shortcuts in the Gemini CLI. | `Up Arrow` / `k` | Move selection up. | | `1-9` | Select an item by its number. | | (multi-digit) | For items with numbers greater than 9, press the digits in quick succession to select the corresponding item. | + +## IDE Integration + +| Shortcut | Description | +| -------- | --------------------------------- | +| `Ctrl+G` | See context CLI received from IDE | diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index 640bf9de..8060cbba 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -163,8 +163,8 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }], // Original: key.ctrl && key.name === 't' [Command.TOGGLE_TOOL_DESCRIPTIONS]: [{ key: 't', ctrl: true }], - // Original: key.ctrl && key.name === 'e' - [Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'e', ctrl: true }], + // Original: key.ctrl && key.name === 'g' + [Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }], // Original: key.ctrl && (key.name === 'c' || key.name === 'C') [Command.QUIT]: [{ key: 'c', ctrl: true }], // Original: key.ctrl && (key.name === 'd' || key.name === 'D') diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 3636823b..c797b778 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -528,7 +528,7 @@ describe('App UI', () => { ); currentUnmount = unmount; await Promise.resolve(); - expect(lastFrame()).toContain('1 open file (ctrl+e to view)'); + expect(lastFrame()).toContain('1 open file (ctrl+g to view)'); }); it('should not display any files when not available', async () => { @@ -583,7 +583,7 @@ describe('App UI', () => { ); currentUnmount = unmount; await Promise.resolve(); - expect(lastFrame()).toContain('3 open files (ctrl+e to view)'); + expect(lastFrame()).toContain('3 open files (ctrl+g to view)'); }); it('should display active file and other context', async () => { @@ -612,7 +612,7 @@ describe('App UI', () => { currentUnmount = unmount; await Promise.resolve(); expect(lastFrame()).toContain( - 'Using: 1 open file (ctrl+e to view) | 1 GEMINI.md file', + 'Using: 1 open file (ctrl+g to view) | 1 GEMINI.md file', ); }); diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx index d70bb4ca..b356e796 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx @@ -41,7 +41,7 @@ describe('', () => { const { lastFrame } = renderWithWidth(120, baseProps); const output = lastFrame(); expect(output).toContain( - 'Using: 1 open file (ctrl+e to view) | 1 GEMINI.md file | 1 MCP server (ctrl+t to view)', + 'Using: 1 open file (ctrl+g to view) | 1 GEMINI.md file | 1 MCP server (ctrl+t to view)', ); // Check for absence of newlines expect(output.includes('\n')).toBe(false); @@ -52,7 +52,7 @@ describe('', () => { const output = lastFrame(); const expectedLines = [ 'Using:', - ' - 1 open file (ctrl+e to view)', + ' - 1 open file (ctrl+g to view)', ' - 1 GEMINI.md file', ' - 1 MCP server (ctrl+t to view)', ]; @@ -78,7 +78,7 @@ describe('', () => { mcpServers: {}, }; const { lastFrame } = renderWithWidth(60, props); - const expectedLines = ['Using:', ' - 1 open file (ctrl+e to view)']; + const expectedLines = ['Using:', ' - 1 open file (ctrl+g to view)']; const actualLines = lastFrame().split('\n'); expect(actualLines).toEqual(expectedLines); }); diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx index 99406bd6..0c946385 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx @@ -49,7 +49,7 @@ export const ContextSummaryDisplay: React.FC = ({ } return `${openFileCount} open file${ openFileCount > 1 ? 's' : '' - } (ctrl+e to view)`; + } (ctrl+g to view)`; })(); const geminiMdText = (() => { diff --git a/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap index 8b84e1f3..dd659db0 100644 --- a/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap @@ -3,7 +3,7 @@ exports[`IDEContextDetailDisplay > handles duplicate basenames by showing path hints 1`] = ` " ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ VS Code Context (ctrl+e to toggle) │ +│ VS Code Context (ctrl+g to toggle) │ │ │ │ Open files: │ │ - bar.txt (/foo) (active) │ @@ -15,7 +15,7 @@ exports[`IDEContextDetailDisplay > handles duplicate basenames by showing path h exports[`IDEContextDetailDisplay > renders a list of open files with active status 1`] = ` " ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ VS Code Context (ctrl+e to toggle) │ +│ VS Code Context (ctrl+g to toggle) │ │ │ │ Open files: │ │ - bar.txt (active) │ diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index 9c72f2dd..9e2963b9 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -51,7 +51,7 @@ describe('keyMatchers', () => { [Command.TOGGLE_TOOL_DESCRIPTIONS]: (key: Key) => key.ctrl && key.name === 't', [Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) => - key.ctrl && key.name === 'e', + key.ctrl && key.name === 'g', [Command.QUIT]: (key: Key) => key.ctrl && key.name === 'c', [Command.EXIT]: (key: Key) => key.ctrl && key.name === 'd', [Command.SHOW_MORE_LINES]: (key: Key) => key.ctrl && key.name === 's', @@ -207,8 +207,8 @@ describe('keyMatchers', () => { }, { command: Command.TOGGLE_IDE_CONTEXT_DETAIL, - positive: [createKey('e', { ctrl: true })], - negative: [createKey('e'), createKey('t', { ctrl: true })], + positive: [createKey('g', { ctrl: true })], + negative: [createKey('g'), createKey('t', { ctrl: true })], }, { command: Command.QUIT, From ec7b84191f539c1a3f890fc994dd62b68d3232fd Mon Sep 17 00:00:00 2001 From: Wietse Venema <356014+wietsevenema@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:08:59 +0200 Subject: [PATCH 011/277] feat: Allow combining -p and stdin for prompt input (#4406) Co-authored-by: Allen Hutchison --- packages/cli/src/gemini.tsx | 7 +++++-- packages/cli/src/utils/readStdin.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 54e58f72..565625d8 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -297,8 +297,11 @@ export async function main() { } // If not a TTY, read from stdin // This is for cases where the user pipes input directly into the command - if (!process.stdin.isTTY && !input) { - input += await readStdin(); + if (!process.stdin.isTTY) { + const stdinData = await readStdin(); + if (stdinData) { + input = `${stdinData}\n\n${input}`; + } } if (!input) { console.error('No input provided via stdin.'); diff --git a/packages/cli/src/utils/readStdin.ts b/packages/cli/src/utils/readStdin.ts index 2e005526..f309c53e 100644 --- a/packages/cli/src/utils/readStdin.ts +++ b/packages/cli/src/utils/readStdin.ts @@ -5,14 +5,26 @@ */ export async function readStdin(): Promise { + const MAX_STDIN_SIZE = 8 * 1024 * 1024; // 8MB return new Promise((resolve, reject) => { let data = ''; + let totalSize = 0; process.stdin.setEncoding('utf8'); const onReadable = () => { let chunk; while ((chunk = process.stdin.read()) !== null) { + if (totalSize + chunk.length > MAX_STDIN_SIZE) { + const remainingSize = MAX_STDIN_SIZE - totalSize; + data += chunk.slice(0, remainingSize); + console.warn( + `Warning: stdin input truncated to ${MAX_STDIN_SIZE} bytes.`, + ); + process.stdin.destroy(); // Stop reading further + break; + } data += chunk; + totalSize += chunk.length; } }; From af93a10a92423184b30c8ef0f5a8e9e70abff167 Mon Sep 17 00:00:00 2001 From: christine betts Date: Thu, 14 Aug 2025 18:09:19 +0000 Subject: [PATCH 012/277] [ide-mode] Write port to file in ide-server (#5811) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- integration-tests/ide-client.test.ts | 139 ++++++++++++++++++ packages/core/src/ide/ide-client.ts | 56 +++++-- packages/core/src/ide/process-utils.ts | 62 ++++++++ .../vscode-ide-companion/src/ide-server.ts | 17 +++ 4 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 integration-tests/ide-client.test.ts create mode 100644 packages/core/src/ide/process-utils.ts diff --git a/integration-tests/ide-client.test.ts b/integration-tests/ide-client.test.ts new file mode 100644 index 00000000..186320b3 --- /dev/null +++ b/integration-tests/ide-client.test.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import * as net from 'node:net'; +import { IdeClient } from '../packages/core/src/ide/ide-client.js'; +import { getIdeProcessId } from '../packages/core/src/ide/process-utils.js'; +import { spawn, ChildProcess } from 'child_process'; + +describe('IdeClient', () => { + it('reads port from file and connects', async () => { + const port = 12345; + const pid = await getIdeProcessId(); + const portFile = path.join(os.tmpdir(), `gemini-ide-server-${pid}.json`); + fs.writeFileSync(portFile, JSON.stringify({ port })); + + const ideClient = IdeClient.getInstance(); + await ideClient.connect(); + + expect(ideClient.getConnectionStatus().status).not.toBe('disconnected'); + + fs.unlinkSync(portFile); + }); +}); + +const getFreePort = (): Promise => { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + server.close(() => { + resolve(port); + }); + }); + }); +}; + +describe('IdeClient fallback connection logic', () => { + let server: net.Server; + let envPort: number; + let pid: number; + let portFile: string; + + beforeEach(async () => { + pid = await getIdeProcessId(); + portFile = path.join(os.tmpdir(), `gemini-ide-server-${pid}.json`); + envPort = await getFreePort(); + server = net.createServer().listen(envPort); + process.env['GEMINI_CLI_IDE_SERVER_PORT'] = String(envPort); + process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'] = process.cwd(); + // Reset instance + IdeClient.instance = undefined; + }); + + afterEach(() => { + server.close(); + delete process.env['GEMINI_CLI_IDE_SERVER_PORT']; + delete process.env['GEMINI_CLI_IDE_WORKSPACE_PATH']; + if (fs.existsSync(portFile)) { + fs.unlinkSync(portFile); + } + }); + + it('connects using env var when port file does not exist', async () => { + // Ensure port file doesn't exist + if (fs.existsSync(portFile)) { + fs.unlinkSync(portFile); + } + + const ideClient = IdeClient.getInstance(); + await ideClient.connect(); + + expect(ideClient.getConnectionStatus().status).toBe('connected'); + }); + + it('falls back to env var when connection with port from file fails', async () => { + const filePort = await getFreePort(); + // Write port file with a port that is not listening + fs.writeFileSync(portFile, JSON.stringify({ port: filePort })); + + const ideClient = IdeClient.getInstance(); + await ideClient.connect(); + + expect(ideClient.getConnectionStatus().status).toBe('connected'); + }); +}); + +describe('getIdeProcessId', () => { + let child: ChildProcess; + + afterEach(() => { + if (child) { + child.kill(); + } + }); + + it('should return the pid of the parent process', async () => { + // We need to spawn a child process that will run the test + // so that we can check that getIdeProcessId returns the pid of the parent + const parentPid = process.pid; + const output = await new Promise((resolve, reject) => { + child = spawn( + 'node', + [ + '-e', + ` + const { getIdeProcessId } = require('../packages/core/src/ide/process-utils.js'); + getIdeProcessId().then(pid => console.log(pid)); + `, + ], + { + stdio: ['pipe', 'pipe', 'pipe'], + }, + ); + + let out = ''; + child.stdout?.on('data', (data) => { + out += data.toString(); + }); + + child.on('close', (code) => { + if (code === 0) { + resolve(out.trim()); + } else { + reject(new Error(`Child process exited with code ${code}`)); + } + }); + }); + + expect(parseInt(output, 10)).toBe(parentPid); + }, 10000); +}); diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts index fe605eb2..94107f21 100644 --- a/packages/core/src/ide/ide-client.ts +++ b/packages/core/src/ide/ide-client.ts @@ -5,7 +5,6 @@ */ import * as fs from 'node:fs'; -import * as path from 'node:path'; import { detectIde, DetectedIde, getIdeInfo } from '../ide/detect-ide.js'; import { ideContext, @@ -15,8 +14,11 @@ import { CloseDiffResponseSchema, DiffUpdateResult, } from '../ide/ideContext.js'; +import { getIdeProcessId } from './process-utils.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import * as os from 'node:os'; +import * as path from 'node:path'; const logger = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -95,12 +97,27 @@ export class IdeClient { return; } - const port = this.getPortFromEnv(); - if (!port) { - return; + const portFromFile = await this.getPortFromFile(); + if (portFromFile) { + const connected = await this.establishConnection(portFromFile); + if (connected) { + return; + } } - await this.establishConnection(port); + const portFromEnv = this.getPortFromEnv(); + if (portFromEnv) { + const connected = await this.establishConnection(portFromEnv); + if (connected) { + return; + } + } + + this.setState( + IDEConnectionStatus.Disconnected, + `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.`, + true, + ); } /** @@ -264,16 +281,26 @@ export class IdeClient { private getPortFromEnv(): string | undefined { const port = process.env['GEMINI_CLI_IDE_SERVER_PORT']; if (!port) { - this.setState( - IDEConnectionStatus.Disconnected, - `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.`, - true, - ); return undefined; } return port; } + private async getPortFromFile(): Promise { + try { + const ideProcessId = await getIdeProcessId(); + const portFile = path.join( + os.tmpdir(), + `gemini-ide-server-${ideProcessId}.json`, + ); + const portFileContents = await fs.promises.readFile(portFile, 'utf8'); + const port = JSON.parse(portFileContents).port; + return port.toString(); + } catch (_) { + return undefined; + } + } + private registerClientHandlers() { if (!this.client) { return; @@ -328,7 +355,7 @@ export class IdeClient { ); } - private async establishConnection(port: string) { + private async establishConnection(port: string): Promise { let transport: StreamableHTTPClientTransport | undefined; try { this.client = new Client({ @@ -342,12 +369,8 @@ export class IdeClient { await this.client.connect(transport); this.registerClientHandlers(); this.setState(IDEConnectionStatus.Connected); + return true; } catch (_error) { - this.setState( - IDEConnectionStatus.Disconnected, - `Failed to connect to IDE companion extension for ${this.currentIdeDisplayName}. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.`, - true, - ); if (transport) { try { await transport.close(); @@ -355,6 +378,7 @@ export class IdeClient { logger.debug('Failed to close transport:', closeError); } } + return false; } } } diff --git a/packages/core/src/ide/process-utils.ts b/packages/core/src/ide/process-utils.ts new file mode 100644 index 00000000..40e16a73 --- /dev/null +++ b/packages/core/src/ide/process-utils.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; +import os from 'os'; + +const execAsync = promisify(exec); + +/** + * Traverses up the process tree from the current process to find the top-level ancestor process ID. + * This is useful for identifying the main application process that spawned the current script, + * such as the main VS Code window process. + * + * @returns A promise that resolves to the numeric PID of the top-level process. + * @throws Will throw an error if the underlying shell commands fail unexpectedly. + */ +export async function getIdeProcessId(): Promise { + const platform = os.platform(); + let currentPid = process.pid; + + // Loop upwards through the process tree, with a depth limit to prevent infinite loops. + const MAX_TRAVERSAL_DEPTH = 32; + for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) { + let parentPid: number; + + try { + // Use wmic for Windows + if (platform === 'win32') { + const command = `wmic process where "ProcessId=${currentPid}" get ParentProcessId /value`; + const { stdout } = await execAsync(command); + const match = stdout.match(/ParentProcessId=(\d+)/); + parentPid = match ? parseInt(match[1], 10) : 0; // Top of the tree is 0 + } + // Use ps for macOS, Linux, and other Unix-like systems + else { + const command = `ps -o ppid= -p ${currentPid}`; + const { stdout } = await execAsync(command); + const ppid = parseInt(stdout.trim(), 10); + parentPid = isNaN(ppid) ? 1 : ppid; // Top of the tree is 1 + } + } catch (_) { + // This can happen if a process in the chain dies during execution. + // We'll break the loop and return the last valid PID we found. + break; + } + + // Define the root PID for the current OS + const rootPid = platform === 'win32' ? 0 : 1; + + // If the parent is the root process or invalid, we've found our target. + if (parentPid === rootPid || parentPid <= 0) { + break; + } + // Move one level up the tree for the next iteration. + currentPid = parentPid; + } + return currentPid; +} diff --git a/packages/vscode-ide-companion/src/ide-server.ts b/packages/vscode-ide-companion/src/ide-server.ts index eec99cb3..ee77bdb8 100644 --- a/packages/vscode-ide-companion/src/ide-server.ts +++ b/packages/vscode-ide-companion/src/ide-server.ts @@ -12,6 +12,9 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import express, { type Request, type Response } from 'express'; import { randomUUID } from 'node:crypto'; import { type Server as HTTPServer } from 'node:http'; +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; import { z } from 'zod'; import { DiffManager } from './diff-manager.js'; import { OpenFilesManager } from './open-files-manager.js'; @@ -46,11 +49,16 @@ export class IDEServer { private server: HTTPServer | undefined; private context: vscode.ExtensionContext | undefined; private log: (message: string) => void; + private portFile: string; diffManager: DiffManager; constructor(log: (message: string) => void, diffManager: DiffManager) { this.log = log; this.diffManager = diffManager; + this.portFile = path.join( + os.tmpdir(), + `gemini-ide-server-${process.ppid}.json`, + ); } async start(context: vscode.ExtensionContext) { @@ -197,6 +205,10 @@ export class IDEServer { port.toString(), ); this.log(`IDE server listening on port ${port}`); + fs.writeFile(this.portFile, JSON.stringify({ port })).catch((err) => { + this.log(`Failed to write port to file: ${err}`); + }); + this.log(this.portFile); } }); } @@ -219,6 +231,11 @@ export class IDEServer { if (this.context) { this.context.environmentVariableCollection.clear(); } + try { + await fs.unlink(this.portFile); + } catch (_err) { + // Ignore errors if the file doesn't exist. + } } } From 69d666cfafe97e49a6cacb306df9a737d4aa9f20 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Thu, 14 Aug 2025 18:13:13 +0000 Subject: [PATCH 013/277] Fix release notes generation (#6233) --- .github/workflows/release.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8079e5c3..6c5ed376 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -183,6 +183,23 @@ jobs: --workspace="@google/gemini-cli" \ --tag="${NPM_TAG}" + - name: 'Get previous release tag' + id: 'previous_release' + if: |- + ${{ steps.vars.outputs.is_dry_run == 'false' }} + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}' + run: |- + if [[ "${IS_NIGHTLY}" == "true" ]]; then + echo "Finding latest nightly release..." + PREVIOUS_TAG=$(gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | contains("nightly"))] | .[0].tagName') + else + echo "Finding latest STABLE release (excluding pre-releases)..." + PREVIOUS_TAG=$(gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | contains("nightly") | not)] | .[0].tagName') + fi + echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "$GITHUB_OUTPUT" + - name: 'Create GitHub Release and Tag' if: |- ${{ steps.vars.outputs.is_dry_run == 'false' }} @@ -190,11 +207,13 @@ jobs: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}' RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' + PREVIOUS_TAG: '${{ steps.previous_release.outputs.PREVIOUS_TAG }}' run: |- gh release create "${RELEASE_TAG}" \ bundle/gemini.js \ --target "$RELEASE_BRANCH" \ --title "Release ${RELEASE_TAG}" \ + --notes-start-tag "$PREVIOUS_TAG" \ --generate-notes - name: 'Create Issue on Failure' From 69c55827239b5c937c177eef4b4fbcc2758ef23e Mon Sep 17 00:00:00 2001 From: shrutip90 Date: Thu, 14 Aug 2025 11:15:48 -0700 Subject: [PATCH 014/277] feat: Show untrusted status in the Footer (#6210) Co-authored-by: Jacob Richman --- packages/cli/src/config/config.test.ts | 9 +- packages/cli/src/config/config.ts | 2 +- .../cli/src/config/trustedFolders.test.ts | 21 +++-- packages/cli/src/config/trustedFolders.ts | 11 ++- packages/cli/src/ui/App.test.tsx | 41 +++++++++ packages/cli/src/ui/App.tsx | 6 +- .../cli/src/ui/components/Footer.test.tsx | 53 ++++++++++++ packages/cli/src/ui/components/Footer.tsx | 8 +- .../cli/src/ui/hooks/useFolderTrust.test.ts | 44 ++++++---- packages/cli/src/ui/hooks/useFolderTrust.ts | 86 ++++++++++++------- 10 files changed, 221 insertions(+), 60 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 69985867..e4535fca 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1809,7 +1809,14 @@ describe('loadCliConfig trustedFolder', () => { description, } of testCases) { it(`should be correct for: ${description}`, async () => { - (isWorkspaceTrusted as vi.Mock).mockReturnValue(mockTrustValue); + (isWorkspaceTrusted as vi.Mock).mockImplementation( + (settings: Settings) => { + const featureIsEnabled = + (settings.folderTrustFeature ?? false) && + (settings.folderTrust ?? true); + return featureIsEnabled ? mockTrustValue : true; + }, + ); const argv = await parseArguments(); const settings: Settings = { folderTrustFeature, folderTrust }; const config = await loadCliConfig(settings, [], 'test-session', argv); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 296d140d..f50cafd4 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -321,7 +321,7 @@ export async function loadCliConfig( const folderTrustFeature = settings.folderTrustFeature ?? false; const folderTrustSetting = settings.folderTrust ?? true; const folderTrust = folderTrustFeature && folderTrustSetting; - const trustedFolder = folderTrust ? isWorkspaceTrusted() : true; + const trustedFolder = isWorkspaceTrusted(settings); const allExtensions = annotateActiveExtensions( extensions, diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index 67bf9cfc..5f613e63 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -35,6 +35,7 @@ import { TrustLevel, isWorkspaceTrusted, } from './trustedFolders.js'; +import { Settings } from './settings.js'; vi.mock('fs', async (importOriginal) => { const actualFs = await importOriginal(); @@ -130,6 +131,10 @@ describe('Trusted Folders Loading', () => { describe('isWorkspaceTrusted', () => { let mockCwd: string; const mockRules: Record = {}; + const mockSettings: Settings = { + folderTrustFeature: true, + folderTrust: true, + }; beforeEach(() => { vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd); @@ -153,51 +158,51 @@ describe('isWorkspaceTrusted', () => { it('should return true for a directly trusted folder', () => { mockCwd = '/home/user/projectA'; mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER; - expect(isWorkspaceTrusted()).toBe(true); + expect(isWorkspaceTrusted(mockSettings)).toBe(true); }); it('should return true for a child of a trusted folder', () => { mockCwd = '/home/user/projectA/src'; mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER; - expect(isWorkspaceTrusted()).toBe(true); + expect(isWorkspaceTrusted(mockSettings)).toBe(true); }); it('should return true for a child of a trusted parent folder', () => { mockCwd = '/home/user/projectB'; mockRules['/home/user/projectB/somefile.txt'] = TrustLevel.TRUST_PARENT; - expect(isWorkspaceTrusted()).toBe(true); + expect(isWorkspaceTrusted(mockSettings)).toBe(true); }); it('should return false for a directly untrusted folder', () => { mockCwd = '/home/user/untrusted'; mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST; - expect(isWorkspaceTrusted()).toBe(false); + expect(isWorkspaceTrusted(mockSettings)).toBe(false); }); it('should return undefined for a child of an untrusted folder', () => { mockCwd = '/home/user/untrusted/src'; mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST; - expect(isWorkspaceTrusted()).toBeUndefined(); + expect(isWorkspaceTrusted(mockSettings)).toBeUndefined(); }); it('should return undefined when no rules match', () => { mockCwd = '/home/user/other'; mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER; mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST; - expect(isWorkspaceTrusted()).toBeUndefined(); + expect(isWorkspaceTrusted(mockSettings)).toBeUndefined(); }); it('should prioritize trust over distrust', () => { mockCwd = '/home/user/projectA/untrusted'; mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER; mockRules['/home/user/projectA/untrusted'] = TrustLevel.DO_NOT_TRUST; - expect(isWorkspaceTrusted()).toBe(true); + expect(isWorkspaceTrusted(mockSettings)).toBe(true); }); it('should handle path normalization', () => { mockCwd = '/home/user/projectA'; mockRules[`/home/user/../user/${path.basename('/home/user/projectA')}`] = TrustLevel.TRUST_FOLDER; - expect(isWorkspaceTrusted()).toBe(true); + expect(isWorkspaceTrusted(mockSettings)).toBe(true); }); }); diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 9da27c80..876c2eb3 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { homedir } from 'os'; import { getErrorMessage, isWithinRoot } from '@google/gemini-cli-core'; +import { Settings } from './settings.js'; import stripJsonComments from 'strip-json-comments'; export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json'; @@ -109,7 +110,15 @@ export function saveTrustedFolders( } } -export function isWorkspaceTrusted(): boolean | undefined { +export function isWorkspaceTrusted(settings: Settings): boolean | undefined { + const folderTrustFeature = settings.folderTrustFeature ?? false; + const folderTrustSetting = settings.folderTrust ?? true; + const folderTrustEnabled = folderTrustFeature && folderTrustSetting; + + if (!folderTrustEnabled) { + return true; + } + const { rules, errors } = loadTrustedFolders(); if (errors.length > 0) { diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index c797b778..64cd5842 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -163,6 +163,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { getCurrentIde: vi.fn(() => 'vscode'), getDetectedIdeDisplayName: vi.fn(() => 'VSCode'), })), + isTrustedFolder: vi.fn(() => true), }; }); @@ -1118,5 +1119,45 @@ describe('App UI', () => { await Promise.resolve(); expect(lastFrame()).toContain('Do you trust this folder?'); }); + + it('should display the folder trust dialog when the feature is enabled but the folder is not trusted', async () => { + const { useFolderTrust } = await import('./hooks/useFolderTrust.js'); + vi.mocked(useFolderTrust).mockReturnValue({ + isFolderTrustDialogOpen: true, + handleFolderTrustSelect: vi.fn(), + }); + mockConfig.isTrustedFolder.mockReturnValue(false); + + const { lastFrame, unmount } = render( + , + ); + currentUnmount = unmount; + await Promise.resolve(); + expect(lastFrame()).toContain('Do you trust this folder?'); + }); + + it('should not display the folder trust dialog when the feature is disabled', async () => { + const { useFolderTrust } = await import('./hooks/useFolderTrust.js'); + vi.mocked(useFolderTrust).mockReturnValue({ + isFolderTrustDialogOpen: false, + handleFolderTrustSelect: vi.fn(), + }); + mockConfig.isTrustedFolder.mockReturnValue(false); + + const { lastFrame, unmount } = render( + , + ); + currentUnmount = unmount; + await Promise.resolve(); + expect(lastFrame()).not.toContain('Do you trust this folder?'); + }); }); }); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 5d4643e5..2c17315f 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -171,6 +171,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const [editorError, setEditorError] = useState(null); const [footerHeight, setFooterHeight] = useState(0); const [corgiMode, setCorgiMode] = useState(false); + const [isTrustedFolderState, setIsTrustedFolder] = useState( + config.isTrustedFolder(), + ); const [currentModel, setCurrentModel] = useState(config.getModel()); const [shellModeActive, setShellModeActive] = useState(false); const [showErrorDetails, setShowErrorDetails] = useState(false); @@ -254,7 +257,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust( settings, - config, + setIsTrustedFolder, ); const { @@ -1198,6 +1201,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { promptTokenCount={sessionStats.lastPromptTokenCount} nightly={nightly} vimMode={vimModeEnabled ? vimMode : undefined} + isTrustedFolder={isTrustedFolderState} /> diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 5e79eea4..e3673dfe 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -103,4 +103,57 @@ describe('