diff --git a/.gemini/settings.json b/.gemini/settings.json
new file mode 100644
index 0000000000..f84c17e60a
--- /dev/null
+++ b/.gemini/settings.json
@@ -0,0 +1,7 @@
+{
+ "experimental": {
+ "toolOutputMasking": {
+ "enabled": true
+ }
+ }
+}
diff --git a/.github/workflows/verify-release.yml b/.github/workflows/verify-release.yml
index 2a2f545498..edf0995ddd 100644
--- a/.github/workflows/verify-release.yml
+++ b/.github/workflows/verify-release.yml
@@ -29,7 +29,11 @@ on:
jobs:
verify-release:
environment: "${{ github.event.inputs.environment || 'prod' }}"
- runs-on: 'ubuntu-latest'
+ strategy:
+ fail-fast: false
+ matrix:
+ os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
+ runs-on: '${{ matrix.os }}'
permissions:
contents: 'read'
packages: 'write'
diff --git a/docs/cli/commands.md b/docs/cli/commands.md
index 5dec6fb5db..6e563cda11 100644
--- a/docs/cli/commands.md
+++ b/docs/cli/commands.md
@@ -113,10 +113,14 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Lists all active extensions in the current Gemini CLI
session. See [Gemini CLI Extensions](../extensions/index.md).
-- **`/help`** (or **`/?`**)
+- **`/help`**
- **Description:** Display help information about Gemini CLI, including
available commands and their usage.
+- **`/shortcuts`**
+ - **Description:** Toggle the shortcuts panel above the input.
+ - **Shortcut:** Press `?` when the prompt is empty.
+
- **`/hooks`**
- **Description:** Manage hooks, which allow you to intercept and customize
Gemini CLI behavior at specific lifecycle events.
diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md
index a1a28665b9..f6cd545438 100644
--- a/docs/cli/keyboard-shortcuts.md
+++ b/docs/cli/keyboard-shortcuts.md
@@ -106,16 +106,17 @@ available combinations.
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`
`Ctrl + S` |
-| Ctrl+B | `Ctrl + B` |
-| Ctrl+L | `Ctrl + L` |
-| Ctrl+K | `Ctrl + K` |
-| Enter | `Enter` |
-| Esc | `Esc` |
-| Shift+Tab | `Shift + Tab` |
-| Tab | `Tab (no Shift)` |
-| Tab | `Tab (no Shift)` |
-| Focus the shell input from the gemini input. | `Tab (no Shift)` |
-| Focus the Gemini input from the shell input. | `Tab` |
+| Toggle current background shell visibility. | `Ctrl + B` |
+| Toggle background shell list. | `Ctrl + L` |
+| Kill the active background shell. | `Ctrl + K` |
+| Confirm selection in background shell list. | `Enter` |
+| Dismiss background shell list. | `Esc` |
+| Move focus from background shell to Gemini. | `Shift + Tab` |
+| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
+| Show warning when trying to unfocus background shell via Tab. | `Tab (no Shift)` |
+| Show warning when trying to unfocus shell input via Tab. | `Tab (no Shift)` |
+| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
+| Move focus from the shell back to Gemini. | `Shift + Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Suspend the application (not yet implemented). | `Ctrl + Z` |
@@ -127,6 +128,9 @@ available combinations.
- `Option+B/F/M` (macOS only): Are interpreted as `Cmd+B/F/M` even if your
terminal isn't configured to send Meta with Option.
- `!` on an empty prompt: Enter or exit shell mode.
+- `?` on an empty prompt: Toggle the shortcuts panel above the input. Press
+ `Esc`, `Backspace`, or any printable key to close it. Press `?` again to close
+ the panel and insert a `?` into the prompt.
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
diff --git a/docs/cli/settings.md b/docs/cli/settings.md
index e925c49482..9a60f89a53 100644
--- a/docs/cli/settings.md
+++ b/docs/cli/settings.md
@@ -22,14 +22,13 @@ they appear in the UI.
### General
-| UI Label | Setting | Description | Default |
-| ------------------------------- | ---------------------------------- | ------------------------------------------------------------- | ------- |
-| Preview Features (e.g., models) | `general.previewFeatures` | Enable preview features (e.g., preview models). | `false` |
-| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
-| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
-| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
-| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
-| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
+| UI Label | Setting | Description | Default |
+| ------------------------ | ---------------------------------- | ------------------------------------------------------------- | ------- |
+| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
+| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
+| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
+| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
+| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
### Output
@@ -102,9 +101,7 @@ they appear in the UI.
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Approval Mode | `tools.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
-| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
-| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` |
-| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` |
+| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation. | `40000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
### Security
diff --git a/docs/cli/telemetry.md b/docs/cli/telemetry.md
index 9bf662b2a1..407ba101f2 100644
--- a/docs/cli/telemetry.md
+++ b/docs/cli/telemetry.md
@@ -320,6 +320,8 @@ Captures startup configuration and user prompt submissions.
Tracks changes and duration of approval modes.
+##### Lifecycle
+
- `approval_mode_switch`: Approval mode was changed.
- **Attributes**:
- `from_mode` (string)
@@ -330,6 +332,15 @@ Tracks changes and duration of approval modes.
- `mode` (string)
- `duration_ms` (int)
+##### Execution
+
+These events track the execution of an approval mode, such as Plan Mode.
+
+- `plan_execution`: A plan was executed and the session switched from plan mode
+ to active execution.
+ - **Attributes**:
+ - `approval_mode` (string)
+
#### Tools
Captures tool executions, output truncation, and Edit behavior.
@@ -710,6 +721,17 @@ Agent lifecycle metrics: runs, durations, and turns.
- **Attributes**:
- `agent_name` (string)
+##### Approval Mode
+
+###### Execution
+
+These metrics track the adoption and usage of specific approval workflows, such
+as Plan Mode.
+
+- `gemini_cli.plan.execution.count` (Counter, Int): Counts plan executions.
+ - **Attributes**:
+ - `approval_mode` (string)
+
##### UI
UI stability signals such as flicker count.
diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md
index 9fb5a5006c..3b1d3899ae 100644
--- a/docs/get-started/configuration.md
+++ b/docs/get-started/configuration.md
@@ -98,10 +98,6 @@ their corresponding top-level category object in your `settings.json` file.
#### `general`
-- **`general.previewFeatures`** (boolean):
- - **Description:** Enable preview features (e.g., preview models).
- - **Default:** `false`
-
- **`general.preferredEditor`** (string):
- **Description:** The preferred editor to open files in.
- **Default:** `undefined`
@@ -720,20 +716,10 @@ their corresponding top-level category object in your `settings.json` file.
implementation. Provides faster search performance.
- **Default:** `true`
-- **`tools.enableToolOutputTruncation`** (boolean):
- - **Description:** Enable truncation of large tool outputs.
- - **Default:** `true`
- - **Requires restart:** Yes
-
- **`tools.truncateToolOutputThreshold`** (number):
- - **Description:** Truncate tool output if it is larger than this many
- characters. Set to -1 to disable.
- - **Default:** `4000000`
- - **Requires restart:** Yes
-
-- **`tools.truncateToolOutputLines`** (number):
- - **Description:** The number of lines to keep when truncating tool output.
- - **Default:** `1000`
+ - **Description:** Maximum characters to show when truncating large tool
+ outputs. Set to 0 or negative to disable truncation.
+ - **Default:** `40000`
- **Requires restart:** Yes
- **`tools.disableLLMCorrection`** (boolean):
@@ -866,7 +852,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.extensionConfig`** (boolean):
- **Description:** Enable requesting and fetching of extension settings.
- - **Default:** `false`
+ - **Default:** `true`
- **Requires restart:** Yes
- **`experimental.enableEventDrivenScheduler`** (boolean):
diff --git a/evals/plan_mode.eval.ts b/evals/plan_mode.eval.ts
new file mode 100644
index 0000000000..197d3c84db
--- /dev/null
+++ b/evals/plan_mode.eval.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, expect } from 'vitest';
+import { ApprovalMode } from '@google/gemini-cli-core';
+import { evalTest } from './test-helper.js';
+import {
+ assertModelHasOutput,
+ checkModelOutputContent,
+} from './test-helper.js';
+
+describe('plan_mode', () => {
+ const TEST_PREFIX = 'Plan Mode: ';
+ const settings = {
+ experimental: { plan: true },
+ };
+
+ evalTest('USUALLY_PASSES', {
+ name: 'should refuse file modification when in plan mode',
+ approvalMode: ApprovalMode.PLAN,
+ params: {
+ settings,
+ },
+ files: {
+ 'README.md': '# Original Content',
+ },
+ prompt: 'Please overwrite README.md with the text "Hello World"',
+ assert: async (rig, result) => {
+ await rig.waitForTelemetryReady();
+ const toolLogs = rig.readToolLogs();
+
+ const writeTargets = toolLogs
+ .filter((log) =>
+ ['write_file', 'replace'].includes(log.toolRequest.name),
+ )
+ .map((log) => {
+ try {
+ return JSON.parse(log.toolRequest.args).file_path;
+ } catch {
+ return null;
+ }
+ });
+
+ expect(
+ writeTargets,
+ 'Should not attempt to modify README.md in plan mode',
+ ).not.toContain('README.md');
+
+ assertModelHasOutput(result);
+ checkModelOutputContent(result, {
+ expectedContent: [/plan mode|read-only|cannot modify|refuse|exiting/i],
+ testName: `${TEST_PREFIX}should refuse file modification`,
+ });
+ },
+ });
+
+ evalTest('USUALLY_PASSES', {
+ name: 'should enter plan mode when asked to create a plan',
+ approvalMode: ApprovalMode.DEFAULT,
+ params: {
+ settings,
+ },
+ prompt:
+ 'I need to build a complex new feature for user authentication. Please create a detailed implementation plan.',
+ assert: async (rig, result) => {
+ const wasToolCalled = await rig.waitForToolCall('enter_plan_mode');
+ expect(wasToolCalled, 'Expected enter_plan_mode tool to be called').toBe(
+ true,
+ );
+ assertModelHasOutput(result);
+ },
+ });
+
+ evalTest('USUALLY_PASSES', {
+ name: 'should exit plan mode when plan is complete and implementation is requested',
+ approvalMode: ApprovalMode.PLAN,
+ params: {
+ settings,
+ },
+ files: {
+ 'plans/my-plan.md':
+ '# My Implementation Plan\n\n1. Step one\n2. Step two',
+ },
+ prompt:
+ 'The plan in plans/my-plan.md is solid. Please proceed with the implementation.',
+ assert: async (rig, result) => {
+ const wasToolCalled = await rig.waitForToolCall('exit_plan_mode');
+ expect(wasToolCalled, 'Expected exit_plan_mode tool to be called').toBe(
+ true,
+ );
+ assertModelHasOutput(result);
+ },
+ });
+
+ evalTest('USUALLY_PASSES', {
+ name: 'should allow file modification in plans directory when in plan mode',
+ approvalMode: ApprovalMode.PLAN,
+ params: {
+ settings,
+ },
+ prompt: 'Create a plan for a new login feature.',
+ assert: async (rig, result) => {
+ await rig.waitForTelemetryReady();
+ const toolLogs = rig.readToolLogs();
+
+ const writeCall = toolLogs.find(
+ (log) => log.toolRequest.name === 'write_file',
+ );
+
+ expect(
+ writeCall,
+ 'Should attempt to modify a file in the plans directory when in plan mode',
+ ).toBeDefined();
+
+ if (writeCall) {
+ const args = JSON.parse(writeCall.toolRequest.args);
+ expect(args.file_path).toContain('.gemini/tmp');
+ expect(args.file_path).toContain('/plans/');
+ expect(args.file_path).toMatch(/\.md$/);
+ }
+
+ assertModelHasOutput(result);
+ },
+ });
+});
diff --git a/integration-tests/resume_repro.responses b/integration-tests/resume_repro.responses
new file mode 100644
index 0000000000..682f3fc9ff
--- /dev/null
+++ b/integration-tests/resume_repro.responses
@@ -0,0 +1 @@
+{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Session started."}],"role":"model"},"finishReason":"STOP","index":0}]}]}
diff --git a/integration-tests/resume_repro.test.ts b/integration-tests/resume_repro.test.ts
new file mode 100644
index 0000000000..6d4f849886
--- /dev/null
+++ b/integration-tests/resume_repro.test.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { TestRig } from './test-helper.js';
+import * as path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('resume-repro', () => {
+ let rig: TestRig;
+
+ beforeEach(() => {
+ rig = new TestRig();
+ });
+
+ afterEach(async () => await rig.cleanup());
+
+ it('should be able to resume a session without "Storage must be initialized before use"', async () => {
+ const responsesPath = path.join(__dirname, 'resume_repro.responses');
+ await rig.setup('should be able to resume a session', {
+ fakeResponsesPath: responsesPath,
+ });
+
+ // 1. First run to create a session
+ await rig.run({
+ args: 'hello',
+ });
+
+ // 2. Second run with --resume latest
+ // This should NOT fail with "Storage must be initialized before use"
+ const result = await rig.run({
+ args: ['--resume', 'latest', 'continue'],
+ });
+
+ expect(result).toContain('Session started');
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 6d48124df7..012115c83d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.8",
"latest-version": "^9.0.0",
+ "proper-lockfile": "^4.1.2",
"simple-git": "^3.28.0"
},
"bin": {
@@ -26,6 +27,7 @@
"@types/minimatch": "^5.1.2",
"@types/mock-fs": "^4.13.4",
"@types/prompts": "^2.4.9",
+ "@types/proper-lockfile": "^4.1.4",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/shell-quote": "^1.7.5",
@@ -4108,6 +4110,16 @@
"kleur": "^3.0.3"
}
},
+ "node_modules/@types/proper-lockfile": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz",
+ "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "*"
+ }
+ },
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -4203,6 +4215,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/@types/retry": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz",
+ "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/sarif": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz",
@@ -14052,6 +14071,32 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
diff --git a/package.json b/package.json
index ab9c20fe84..71bc3884fd 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"docs:settings": "tsx ./scripts/generate-settings-doc.ts",
"docs:keybindings": "tsx ./scripts/generate-keybindings-doc.ts",
"build": "node scripts/build.js",
- "build-and-start": "npm run build && npm run start",
+ "build-and-start": "npm run build && npm run start --",
"build:vscode": "node scripts/build_vscode_companion.js",
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
@@ -86,6 +86,7 @@
"@types/minimatch": "^5.1.2",
"@types/mock-fs": "^4.13.4",
"@types/prompts": "^2.4.9",
+ "@types/proper-lockfile": "^4.1.4",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/shell-quote": "^1.7.5",
@@ -126,6 +127,7 @@
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.8",
"latest-version": "^9.0.0",
+ "proper-lockfile": "^4.1.2",
"simple-git": "^3.28.0"
},
"optionalDependencies": {
diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts
index 5b8793d15e..91c23d7910 100644
--- a/packages/a2a-server/src/config/config.ts
+++ b/packages/a2a-server/src/config/config.ts
@@ -18,7 +18,6 @@ import {
loadServerHierarchicalMemory,
GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
- DEFAULT_GEMINI_MODEL,
type ExtensionLoader,
startupProfiler,
PREVIEW_GEMINI_MODEL,
@@ -60,9 +59,7 @@ export async function loadConfig(
const configParams: ConfigParameters = {
sessionId: taskId,
- model: settings.general?.previewFeatures
- ? PREVIEW_GEMINI_MODEL
- : DEFAULT_GEMINI_MODEL,
+ model: PREVIEW_GEMINI_MODEL,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
sandbox: undefined, // Sandbox might not be relevant for a server-side agent
targetDir: workspaceDir, // Or a specific directory the agent operates on
@@ -104,7 +101,6 @@ export async function loadConfig(
trustedFolder: true,
extensionLoader,
checkpointing,
- previewFeatures: settings.general?.previewFeatures,
interactive: true,
enableInteractiveShell: true,
ptyInfo: 'auto',
diff --git a/packages/a2a-server/src/config/settings.test.ts b/packages/a2a-server/src/config/settings.test.ts
index b5788b0fb6..7c51950535 100644
--- a/packages/a2a-server/src/config/settings.test.ts
+++ b/packages/a2a-server/src/config/settings.test.ts
@@ -89,67 +89,6 @@ describe('loadSettings', () => {
vi.restoreAllMocks();
});
- it('should load nested previewFeatures from user settings', () => {
- const settings = {
- general: {
- previewFeatures: true,
- },
- };
- fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
-
- const result = loadSettings(mockWorkspaceDir);
- expect(result.general?.previewFeatures).toBe(true);
- });
-
- it('should load nested previewFeatures from workspace settings', () => {
- const settings = {
- general: {
- previewFeatures: true,
- },
- };
- const workspaceSettingsPath = path.join(
- mockGeminiWorkspaceDir,
- 'settings.json',
- );
- fs.writeFileSync(workspaceSettingsPath, JSON.stringify(settings));
-
- const result = loadSettings(mockWorkspaceDir);
- expect(result.general?.previewFeatures).toBe(true);
- });
-
- it('should prioritize workspace settings over user settings', () => {
- const userSettings = {
- general: {
- previewFeatures: false,
- },
- };
- fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(userSettings));
-
- const workspaceSettings = {
- general: {
- previewFeatures: true,
- },
- };
- const workspaceSettingsPath = path.join(
- mockGeminiWorkspaceDir,
- 'settings.json',
- );
- fs.writeFileSync(workspaceSettingsPath, JSON.stringify(workspaceSettings));
-
- const result = loadSettings(mockWorkspaceDir);
- expect(result.general?.previewFeatures).toBe(true);
- });
-
- it('should handle missing previewFeatures', () => {
- const settings = {
- general: {},
- };
- fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
-
- const result = loadSettings(mockWorkspaceDir);
- expect(result.general?.previewFeatures).toBeUndefined();
- });
-
it('should load other top-level settings correctly', () => {
const settings = {
showMemoryUsage: true,
diff --git a/packages/a2a-server/src/config/settings.ts b/packages/a2a-server/src/config/settings.ts
index f57e177681..5538576dc7 100644
--- a/packages/a2a-server/src/config/settings.ts
+++ b/packages/a2a-server/src/config/settings.ts
@@ -31,9 +31,6 @@ export interface Settings {
showMemoryUsage?: boolean;
checkpointing?: CheckpointingSettings;
folderTrust?: boolean;
- general?: {
- previewFeatures?: boolean;
- };
// Git-aware file filtering settings
fileFiltering?: {
diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts
index 87c7315f82..36880fda79 100644
--- a/packages/a2a-server/src/utils/testing_utils.ts
+++ b/packages/a2a-server/src/utils/testing_utils.ts
@@ -12,7 +12,6 @@ import type {
import {
ApprovalMode,
DEFAULT_GEMINI_MODEL,
- DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
GeminiClient,
HookSystem,
@@ -47,7 +46,6 @@ export function createMockConfig(
} as Storage,
getTruncateToolOutputThreshold: () =>
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
- getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL),
getDebugMode: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi.fn().mockReturnValue({ model: 'gemini-pro' }),
diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts
index 30d88af995..60912c51f5 100644
--- a/packages/cli/src/commands/mcp/list.test.ts
+++ b/packages/cli/src/commands/mcp/list.test.ts
@@ -32,6 +32,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...original,
createTransport: vi.fn(),
+
MCPServerStatus: {
CONNECTED: 'CONNECTED',
CONNECTING: 'CONNECTING',
@@ -223,4 +224,46 @@ describe('mcp list command', () => {
),
);
});
+
+ it('should filter servers based on admin allowlist passed in settings', async () => {
+ const settingsWithAllowlist = mergeSettings({}, {}, {}, {}, true);
+ settingsWithAllowlist.admin = {
+ secureModeEnabled: false,
+ extensions: { enabled: true },
+ skills: { enabled: true },
+ mcp: {
+ enabled: true,
+ config: {
+ 'allowed-server': { url: 'http://allowed' },
+ },
+ },
+ };
+
+ settingsWithAllowlist.mcpServers = {
+ 'allowed-server': { command: 'cmd1' },
+ 'forbidden-server': { command: 'cmd2' },
+ };
+
+ mockedLoadSettings.mockReturnValue({
+ merged: settingsWithAllowlist,
+ });
+
+ mockClient.connect.mockResolvedValue(undefined);
+ mockClient.ping.mockResolvedValue(undefined);
+
+ await listMcpServers(settingsWithAllowlist);
+
+ expect(debugLogger.log).toHaveBeenCalledWith(
+ expect.stringContaining('allowed-server'),
+ );
+ expect(debugLogger.log).not.toHaveBeenCalledWith(
+ expect.stringContaining('forbidden-server'),
+ );
+ expect(mockedCreateTransport).toHaveBeenCalledWith(
+ 'allowed-server',
+ expect.objectContaining({ url: 'http://allowed' }), // Should use admin config
+ false,
+ expect.anything(),
+ );
+ });
});
diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts
index 50fc222f71..d51093fbfa 100644
--- a/packages/cli/src/commands/mcp/list.ts
+++ b/packages/cli/src/commands/mcp/list.ts
@@ -6,12 +6,14 @@
// File for 'gemini mcp list' command
import type { CommandModule } from 'yargs';
-import { loadSettings } from '../../config/settings.js';
+import { type MergedSettings, loadSettings } from '../../config/settings.js';
import type { MCPServerConfig } from '@google/gemini-cli-core';
import {
MCPServerStatus,
createTransport,
debugLogger,
+ applyAdminAllowlist,
+ getAdminBlockedMcpServersMessage,
} from '@google/gemini-cli-core';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ExtensionManager } from '../../config/extension-manager.js';
@@ -24,18 +26,24 @@ const COLOR_YELLOW = '\u001b[33m';
const COLOR_RED = '\u001b[31m';
const RESET_COLOR = '\u001b[0m';
-export async function getMcpServersFromConfig(): Promise<
- Record
-> {
- const settings = loadSettings();
+export async function getMcpServersFromConfig(
+ settings?: MergedSettings,
+): Promise<{
+ mcpServers: Record;
+ blockedServerNames: string[];
+}> {
+ if (!settings) {
+ settings = loadSettings().merged;
+ }
+
const extensionManager = new ExtensionManager({
- settings: settings.merged,
+ settings,
workspaceDir: process.cwd(),
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
});
const extensions = await extensionManager.loadExtensions();
- const mcpServers = { ...settings.merged.mcpServers };
+ const mcpServers = { ...settings.mcpServers };
for (const extension of extensions) {
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
if (mcpServers[key]) {
@@ -47,7 +55,11 @@ export async function getMcpServersFromConfig(): Promise<
};
});
}
- return mcpServers;
+
+ const adminAllowlist = settings.admin?.mcp?.config;
+ const filteredResult = applyAdminAllowlist(mcpServers, adminAllowlist);
+
+ return filteredResult;
}
async function testMCPConnection(
@@ -103,12 +115,23 @@ async function getServerStatus(
return testMCPConnection(serverName, server);
}
-export async function listMcpServers(): Promise {
- const mcpServers = await getMcpServersFromConfig();
+export async function listMcpServers(settings?: MergedSettings): Promise {
+ const { mcpServers, blockedServerNames } =
+ await getMcpServersFromConfig(settings);
const serverNames = Object.keys(mcpServers);
+ if (blockedServerNames.length > 0) {
+ const message = getAdminBlockedMcpServersMessage(
+ blockedServerNames,
+ undefined,
+ );
+ debugLogger.log(COLOR_YELLOW + message + RESET_COLOR + '\n');
+ }
+
if (serverNames.length === 0) {
- debugLogger.log('No MCP servers configured.');
+ if (blockedServerNames.length === 0) {
+ debugLogger.log('No MCP servers configured.');
+ }
return;
}
@@ -154,11 +177,15 @@ export async function listMcpServers(): Promise {
}
}
-export const listCommand: CommandModule = {
+interface ListArgs {
+ settings?: MergedSettings;
+}
+
+export const listCommand: CommandModule