Compare commits

..

106 Commits

Author SHA1 Message Date
Gaurav Ghosh 7d32f2bf88 fix: suppress MCP server stderr from corrupting alternate buffer UI
Pipe stderr from npx chrome-devtools-mcp instead of inheriting it.
The server's banner warnings were leaking into the terminal and
corrupting the Ink-based UI in alternate buffer mode. Piped output
is forwarded to debugLogger so it remains visible with --debug.
2026-02-24 02:05:53 -08:00
Gaurav Ghosh c991e5b3dc fix: address PR #19284 review comments
- Remove redundant Promise.race in McpToolInvocation.execute (event listener leak)
- Propagate AbortSignal to all press_key calls (submitKey + typeCharByChar)
- Call this.close() on connectMcp failure (zombie process leak)
- Set showInDialog: false for all browser settings
- Remove debug log truncation in analyzeScreenshot
- Fix misleading --experimental-vision error message
- Replace any casts with typed TestableConfirmation interface in tests
- Update license year to 2026 in all browser agent files
- Merge duplicate imports in mcpToolWrapper
- Add sync comment to BrowserAgentCustomConfig
- Update subagents.md Chrome requirement wording
- Regenerate settings docs
2026-02-24 02:05:53 -08:00
Gaurav Ghosh c1560d99fd fix: update browser agent description to encourage full task delegation
Updated the browser_agent description from a primitive-focused listing
(navigating, filling, clicking) to a goal-oriented description that
emphasizes autonomy, multi-step reasoning, and dynamic feedback
interpretation. This encourages the parent agent to delegate entire
tasks in a single call rather than micromanaging individual browser
actions.
2026-02-23 13:29:39 -08:00
Gaurav Ghosh 377186d831 feat(browser): default persistent profile to ~/.gemini/cli-browser-profile 2026-02-23 12:25:46 -08:00
Gaurav Ghosh 64853dbfde refactor: Introduce dedicated browser agent configuration with session mode, headless, profile path, and visual model settings. 2026-02-23 12:06:23 -08:00
Gaurav Ghosh 52d9271e63 chore: regenerate settings schema and docs 2026-02-23 11:52:50 -08:00
Gaurav Ghosh 6732115859 fix: Add LlmRole.UTILITY_TOOL to analyzeScreenshot function calls. 2026-02-23 11:52:50 -08:00
Gaurav Ghosh 7718709f01 fix(browser): exclude visual prompt section when vision is disabled
The system prompt always included the VISUAL IDENTIFICATION section
telling the model about analyze_screenshot, even when visualModel was
not configured. This caused the model to attempt calling the tool
despite it not being registered.

- Convert BROWSER_SYSTEM_PROMPT to buildBrowserSystemPrompt(visionEnabled)
- Pass vision state from factory to definition builder
- Remove analyze_screenshot reference from click_at tool description
- Add tests for conditional prompt inclusion/exclusion
- Fix misleading test comment about tool count
2026-02-23 11:52:49 -08:00
Gaurav Ghosh 4e2856c4dd feat(browser): add submitKey param to type_text and improve connection errors
- Add submitKey parameter to type_text tool for pressing Enter/Tab/etc
  after typing, eliminating a separate model round-trip per value entry
- Update system prompt and tool hints to guide model toward type_text
  with submitKey instead of per-character press_key calls
- Refactor connection error handling into createConnectionError() with
  session-mode-aware remediation messages for profile locks, timeouts,
  and generic failures
- Update terminal failure prompts to pass through error remediation
  verbatim instead of hardcoding instructions
- Add tests for profile-lock, timeout, and generic connection errors
2026-02-23 11:52:49 -08:00
Gaurav Ghosh c7ec983d31 feat: document the experimental browser agent, its configuration, session modes, and security. 2026-02-23 11:52:49 -08:00
Gaurav Ghosh 067d0ecab3 fix: update chrome-devtools-mcp dependency, and add transport error handling. 2026-02-23 11:52:49 -08:00
Gaurav Ghosh fb1b2891cc feat(browser): gate vision on visualModel setting
Vision (screenshot analysis + coordinate-based interactions) is now
disabled by default. Set visualModel in browser_agent customConfig
to enable it, e.g. visualModel: 'gemini-2.5-computer-use-preview-10-2025'.
2026-02-23 11:52:48 -08:00
Gaurav Ghosh 2bc2945d14 feat(browser-agent): add type_text composite tool and improve prompt
- Add custom type_text tool that types a full string by internally
  calling press_key for each character, turning N model round-trips
  into 1. Dramatically speeds up text input in complex web apps.

- Move tool-specific usage rules from system prompt to individual
  tool descriptions via augmentToolDescription() for better
  organization and token efficiency.

- Add terminal failure handling instructions to system prompt
  (Chrome connection errors, browser crashes, repeated errors)
  with specific remediation steps.

- Add complex web app guidance (spreadsheets, rich editors) to
  system prompt, recommending type_text + keyboard navigation.

- Fix augmentToolDescription key ordering so more-specific keys
  (fill_form, click_at) match before shorter keys (fill, click).

- Remove non-existent tool references (scroll, type_text as MCP tool)
  and add click_at hint for vision tool.
2026-02-23 11:52:48 -08:00
Gaurav Ghosh 1c8a37379b fix(browser): correct session mode CLI flags and add connection validation
Fix chrome-devtools-mcp CLI flags:
- --existing (invalid) → --autoConnect for existing session mode
- --profile-path (invalid) → --userDataDir for custom profile path
- Default session mode changed from 'isolated' to 'persistent'

Add 'persistent' session mode (new default) which uses a persistent
Chrome profile at ~/.cache/chrome-devtools-mcp/chrome-profile.

Add connection timeout and actionable error for 'existing' mode when
Chrome remote debugging is not enabled.
2026-02-23 11:52:47 -08:00
Gaurav Ghosh 1620c7d82f feat(browser): implement visual agent for coordinate-based interactions
Implement the visual agent using the LocalAgentDefinition pattern:
- VisualAgentDefinition: Agent metadata for coordinate-based visual tasks
- delegateToVisualAgent.ts: Tool for semantic agent to delegate visual tasks
- Uses gemini-2.5-computer-use-preview-10-2025 model for Computer Use capability

The visual agent handles tasks requiring visual identification or precise
coordinate-based actions that cannot be done via the accessibility tree.
2026-02-23 11:52:47 -08:00
Gaurav Ghosh f4100baf6b feat(browser): implement browser agent as LocalAgentDefinition
Implement the browser agent using the LocalAgentDefinition pattern:
- BrowserAgentDefinition: Agent metadata and prompt configuration
- BrowserAgentInvocation: Handles individual browser agent invocations
- BrowserAgentFactory: Creates agent definitions with dynamic MCP tools
- BrowserManager: Manages chrome-devtools-mcp connection lifecycle

Uses getBrowserAgentConfig() to read settings from agents.overrides.browser_agent
2026-02-23 11:52:47 -08:00
Gaurav Ghosh 0b93c868e9 feat(browser): add browser agent settings schema
Add extensible browser agent configuration using the agents.overrides pattern:
- Extended AgentOverride interface with customConfig field for agent-specific settings
- Added BrowserAgentCustomConfig type for browser-specific configuration
- Added getAgentOverride() and getBrowserAgentConfig() methods to Config class
- Settings configured via agents.overrides.browser_agent.customConfig
- Updated settings schema with customConfig in AgentOverride definition

This establishes the foundational pattern for configuring the browser agent
through the standard agents.overrides infrastructure.
2026-02-23 11:52:46 -08:00
Aishanee Shah 7cfbb6fb71 feat(core): optimize tool descriptions and schemas for Gemini 3 (#19643) 2026-02-23 19:27:35 +00:00
Mehmet Gok a105768de8 docs(CONTRIBUTING): update React DevTools version to 6 (#20014)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-02-23 19:17:04 +00:00
Jerop Kipruto 347f3fe7e4 feat(policy): Support MCP Server Wildcards in Policy Engine (#20024) 2026-02-23 19:07:06 +00:00
Sandy Tao 25803e05fd fix(bundling): copy devtools package to bundle for runtime resolution (#19766) 2026-02-23 18:40:41 +00:00
Himanshu Soni 774ae220be fix(core): prevent state corruption in McpClientManager during collis (#19782) 2026-02-23 18:35:31 +00:00
Tommaso Sciortino 813e0c18ac Allow ask headers longer than 16 chars (#20041) 2026-02-23 18:26:59 +00:00
Gal Zahavi 3f6cec22e6 chore: restrict gemini-automted-issue-triage to only allow echo (#20047) 2026-02-23 18:24:34 +00:00
Sri Pasumarthi 3966f3c053 feat: Map tool kinds to explicit ACP.ToolKind values and update test … (#19547) 2026-02-23 18:22:05 +00:00
sinisterchill 2e3cbd6363 fix(core): prevent OAuth server crash on unexpected requests (#19668) 2026-02-23 18:03:31 +00:00
Adib234 8b1dc15182 fix(plan): allow plan mode writes on Windows and fix prompt paths (#19658) 2026-02-23 17:48:50 +00:00
owenofbrien fa9aee2bf0 Fix for silent failures in non-interactive mode (#19905) 2026-02-23 17:35:13 +00:00
Sam Roberts 6628cbb39d Updates command reference and /stats command. (#19794) 2026-02-23 17:13:24 +00:00
Sehoon Shon aa9163da60 feat(core): add policy chain support for Gemini 3.1 (#19991) 2026-02-23 15:13:48 +00:00
Sehoon Shon ec0f23ae03 fix(core): increase default retry attempts and add quota error backoff (#19949) 2026-02-23 15:13:34 +00:00
nityam ac04c388e0 Fix: Persist manual model selection on restart #19864 (#19891) 2026-02-23 03:44:00 +00:00
Abhi 621ddbe744 refactor(core): move session conversion logic to core (#19972) 2026-02-23 01:18:07 +00:00
Sehoon Shon c537fd5aec refactor(config): remove enablePromptCompletion from settings (#19974) 2026-02-22 19:10:20 -05:00
Shivangi Sharma a91bc60e18 fix(core): add uniqueness guard to edit tool (#19890)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-02-22 20:24:58 +00:00
Nick Salerni faa1ec3044 fix(core): prevent omission placeholder deletions in replace/write_file (#19870)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-02-22 19:58:31 +00:00
Bryan Morgan d96bd05d36 fix(core): allow any preview model in quota access check (#19867) 2026-02-22 12:53:24 +00:00
Adib234 84666e1bbc fix(plan): time share by approval mode dashboard reporting negative time shares (#19847) 2026-02-22 00:32:57 +00:00
N. Taylor Mullen a7d851146a feat(core): remove unnecessary login verbiage from Code Assist auth (#19861) 2026-02-21 21:55:11 +00:00
Abhi acb7f577de chore(lint): fix lint errors seen when running npm run lint (#19844) 2026-02-21 18:33:25 +00:00
Abhi d2d345f41a fix(cli): filter subagent sessions from resume history (#19698) 2026-02-21 17:41:27 +00:00
Christian Gunderman dfd7721e69 Disallow unsafe returns. (#19767) 2026-02-21 01:12:56 +00:00
matt korwel 09218572d0 refactor(core): remove unsafe type assertions in error utils (Phase 1.1) (#19750) 2026-02-21 01:00:57 +00:00
Christian Gunderman 5d98ed5820 Utilize pipelining of grep_search -> read_file to eliminate turns (#19574) 2026-02-21 00:36:10 +00:00
Jarrod Whelan 727f9b67b1 feat(cli): improve CTRL+O experience for both standard and alternate screen buffer (ASB) modes (#19010)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-02-21 00:26:11 +00:00
Adam Weidman 547f5d45f5 feat(core): migrate read_file to 1-based start_line/end_line parameters (#19526) 2026-02-20 22:59:18 +00:00
Christian Gunderman 58d637f919 Disallow and suppress unsafe assignment (#19736) 2026-02-20 22:28:55 +00:00
Sehoon Shon b746524a1b fix(cli): re-enable CLI banner (#19741) 2026-02-20 22:21:26 +00:00
Abhijit Balaji c5baf39dbd feat(policy): repurpose "Always Allow" persistence to workspace level (#19707) 2026-02-20 22:07:20 +00:00
Sehoon Shon b48970da15 fix(cli): use getDisplayString for manual model selection in dialog (#19726) 2026-02-20 22:03:32 +00:00
Jacob Richman 9a8e5d3940 fix(cli): extensions dialog UX polish (#19685) 2026-02-20 21:08:24 +00:00
Jacob Richman 089aec8b8d feat(cli): make JetBrains warning more specific (#19687) 2026-02-20 21:06:35 +00:00
Christian Gunderman b7555ab1e1 Fix unsafe assertions in code_assist folder. (#19706)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 20:44:23 +00:00
Emily Hedlund c04602f209 fix(core): restore auth consent in headless mode and add unit tests (#19689) 2026-02-20 20:31:43 +00:00
Emily Hedlund a01d7e9a05 security: implement deceptive URL detection and disclosure in tool confirmations (#19288) 2026-02-20 20:21:31 +00:00
Emily Hedlund 49b2e76ee1 Revert "feat(ui): add source indicators to slash commands" (#19695) 2026-02-20 20:08:49 +00:00
Emily Hedlund aed348a99c security: strip deceptive Unicode characters from terminal output (#19026)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 20:04:32 +00:00
Christian Gunderman 7cf4c05c66 Fixes 'input.on' is not a function error in Gemini CLI (#19691) 2026-02-20 20:03:57 +00:00
Sam Roberts cdf157e52a Update sidebar.json for to allow top nav tabs. (#19595) 2026-02-20 19:54:26 +00:00
Sehoon Shon 723f269df6 fix(core): treat 503 Service Unavailable as retryable quota error (#19642) 2026-02-20 19:51:53 +00:00
Spencer 239aa0909c fix(cli): allow perfect match @-path completions to submit on Enter (#19562) 2026-02-20 19:46:48 +00:00
matt korwel 6cfd29ef9b feat(plan): enforce read-only constraints in Plan Mode (#19433)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jerop Kipruto <jerop@google.com>
2026-02-20 19:33:04 +00:00
Sehoon Shon f97b04cc9a feat(models): support Gemini 3.1 Pro Preview and fixes (#19676) 2026-02-20 19:19:21 +00:00
Christian Gunderman 788a40c445 Send accepted/removed lines with ACCEPT_FILE telemetry. (#19670) 2026-02-20 19:07:43 +00:00
Adam Weidman ce03156c9f feat(a2a): Add API key authentication provider (#19548) 2026-02-20 18:55:36 +00:00
Spencer fe428936d5 feat(ui): improve startup warnings UX with dismissal and show-count limits (#19584) 2026-02-20 18:22:45 +00:00
Gal Zahavi d24f10b087 feat(cli): enhance folder trust with configuration discovery and security warnings (#19492)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 18:21:03 +00:00
Tommaso Sciortino d54702185b feat(cli): add support for numpad SS3 sequences (#19659) 2026-02-20 18:09:10 +00:00
Alisa 27b7fc04de Search updates (#19482)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 17:54:28 +00:00
╯‵Д′)╯彡┻━┻ (☕1e6) be03e0619f fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) (#19038) 2026-02-20 17:48:42 +00:00
christine betts 2bb7aaecd0 Add initial implementation of /extensions explore command (#19029) 2026-02-20 17:30:49 +00:00
matt korwel 0f855fc0c4 fix(security): rate limit web_fetch tool to mitigate DDoS via prompt injection (#19567)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 17:18:07 +00:00
Jenna Inouye c7e309efc9 Docs: Update release information regarding Gemini 3.1 (#19568) 2026-02-20 16:37:41 +00:00
Jerop Kipruto 429932c663 docs: refine Plan Mode documentation structure and workflow (#19644) 2026-02-20 15:56:49 +00:00
Emily Hedlund d08b1efc72 feat(ui): add source indicators to slash commands (#18839) 2026-02-20 15:54:59 +00:00
matt korwel c3b52b8206 chore: resolve build warnings and update dependencies (#18880) 2026-02-20 03:25:56 +00:00
Adib234 5fd557347e fix(plan): exclude EnterPlanMode tool from YOLO mode (#19570) 2026-02-20 01:53:12 +00:00
gemini-cli-robot cbfb2a4e26 Changelog for v0.30.0-preview.3 (#19585)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
Co-authored-by: Sam Roberts <158088236+g-samroberts@users.noreply.github.com>
2026-02-20 01:10:05 +00:00
Sandy Tao fb1b1b451d feat(core): refine Edit and WriteFile tool schemas for Gemini 3 (#19476) 2026-02-20 01:03:10 +00:00
Bryan Morgan 99fa700231 fix(ci): add fallback JSON extraction to issue triage workflow (#19593) 2026-02-19 20:19:01 -05:00
Michael Bleigh f1c0a695f8 refactor(sdk): introduce session-based architecture (#19180)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-20 00:47:35 +00:00
joshualitt 6351352e54 feat(core): Implement parallel FC for read only tools. (#18791) 2026-02-20 00:38:22 +00:00
Michael Bleigh 2ac39b6acc chore(skills): adds pr-address-comments skill to work on PR feedback (#19576) 2026-02-20 00:38:07 +00:00
Abhijit Balaji d8b24e6983 feat(policy): implement project-level policy support (#18682) 2026-02-20 00:16:03 +00:00
Sam Roberts d25c469f77 Migrate files to resource or references folder. (#19503) 2026-02-19 23:47:39 +00:00
Jerop Kipruto 537e56ffae feat(plan): support configuring custom plans storage directory (#19577) 2026-02-19 22:47:08 +00:00
Jack Wotherspoon 2cba2ab37a fix: remove extra padding in Composer (#19529) 2026-02-19 21:31:09 +00:00
Yuna Seol 8064973899 fix(core): improve error type extraction for telemetry (#19565)
Co-authored-by: Yuna Seol <yunaseol@google.com>
2026-02-19 21:19:19 +00:00
christine betts ddc5458451 Revert "Add generic searchable list to back settings and extensions (… (#19434)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-02-19 21:06:37 +00:00
Adam Weidman a468407098 chore(core): improve encapsulation and remove unused exports (#19556) 2026-02-19 20:19:32 +00:00
Adib234 264c7aceaa fix(core): resolve crash in ClearcutLogger when os.cpus() is empty (#19555) 2026-02-19 20:13:28 +00:00
Abhijit Balaji 3408542a66 fix(core): prevent duplicate tool approval entries in auto-saved.toml (#19487)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-19 20:03:52 +00:00
Christian Gunderman c276d0c7b6 Fix message too large issue. (#19499) 2026-02-19 19:06:36 +00:00
Sam Roberts a00eb3b8e6 Update skill to adjust for generated results. (#19500) 2026-02-19 18:49:27 +00:00
Dmitry Lyalin 372f41eab8 feat(cli): replace loading phrases boolean with enum setting (#19347) 2026-02-19 18:43:12 +00:00
Tommaso Sciortino 09b623fbd7 feat(cli): add experimental.useOSC52Copy setting (#19488) 2026-02-19 18:22:11 +00:00
Tommaso Sciortino ba3e327ba1 format md file (#19474) 2026-02-19 09:43:53 -08:00
薄明色の忘れ路 5d235952ba Fix: Avoid tool confirmation timeout when no UI listeners are present (#17955) 2026-02-19 17:28:06 +00:00
Jacob Richman 082f41f54d Deflake windows tests. (#19511) 2026-02-19 16:17:34 +00:00
Emily Hedlund 880af43b02 fix(core): robust workspace-based IDE connection discovery (#18443) 2026-02-19 15:59:33 +00:00
Valery Teplyakov 966eef14ee feat(acp): support set_mode interface (#18890) (#18891)
Co-authored-by: Mervap <megavaprold@gmail.com>
2026-02-19 11:07:46 -05:00
Yuvraj Angad Singh b79e5ce56d fix(core): add error logging for IDE fetch failures (#17981) 2026-02-19 15:54:49 +00:00
Valery Teplyakov 1a8d77329e fix(acp): Initialize config (#18897) (#18898)
Co-authored-by: Mervap <megavaprold@gmail.com>
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-02-19 15:08:09 +00:00
abhiasap ad9c49a604 fix(core): ensure user rejections update tool outcome for telemetry (#18982) 2026-02-19 13:14:02 +00:00
Jacob Richman 70b427c7dd fix spacing (#19494) 2026-02-19 06:39:54 +00:00
Jacob Richman a804f99fe0 Speculative fixes to try to fix react error. (#19508) 2026-02-19 06:39:32 +00:00
463 changed files with 19293 additions and 3736 deletions
+28 -10
View File
@@ -109,23 +109,41 @@ detailed **highlights** section for the release-specific page.
- **Target File**: `docs/changelogs/latest.md`
- Perform the following edits on the target file:
1. Update the version in the main header.
2. Update the "Released:" date.
1. Update the version in the main header. The line should read,
`# Latest stable release: {{version}}`
2. Update the rease date. The line should read,
`Released: {{release_date_month_dd_yyyy}}`
3. **Prepend** the processed "What's Changed" list from the temporary file
to the existing "What's Changed" list in the file.
4. In the "Full Changelog" URL, replace only the trailing version with the
new patch version.
to the existing "What's Changed" list in `latest.md`. Do not change or
replace the existing list, **only add** to the beginning of it.
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
last part of the URL that looks like `...{previous_version}` and update
it to be `...{version}`.
Example: assume the patch version is `v0.29.1`. Change
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0`
to
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.1`
### B.2: Preview Patch (e.g., `v0.29.0-preview.3`)
- **Target File**: `docs/changelogs/preview.md`
- Perform the following edits on the target file:
1. Update the version in the main header.
2. Update the "Released:" date.
1. Update the version in the main header. The line should read,
`# Preview release: {{version}}`
2. Update the rease date. The line should read,
`Released: {{release_date_month_dd_yyyy}}`
3. **Prepend** the processed "What's Changed" list from the temporary file
to the existing "What's Changed" list in the file.
4. In the "Full Changelog" URL, replace only the trailing version with the
new patch version.
to the existing "What's Changed" list in `preview.md`. Do not change or
replace the existing list, **only add** to the beginning of it.
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
last part of the URL that looks like `...{previous_version}` and update
it to be `...{version}`.
Example: assume the patch version is `v0.29.0-preview.1`. Change
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.0`
to
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.1`
---
@@ -0,0 +1,13 @@
---
name: pr-address-comments
description: Use this skill if the user asks you to help them address GitHub PR comments for their current branch of the Gemini CLI. Requires `gh` CLI tool.
---
You are helping the user address comments on their Pull Request. These comments may have come from an automated review agent or a team member.
OBJECTIVE: Help the user review and address comments on their PR.
# Comment Review Procedure
1. Run the `scripts/fetch-pr-info.js` script to get PR info and state. MAKE SURE you read the entire output of the command, even if it gets truncated.
2. Summarize the review status by analyzing the diff, commit log, and comments to see which still need to be addressed. Pay attention to the current user's comments. For resolved threads, summarize as a single line with a ✅. For open threads, provide a reference number e.g. [1] and the comment content.
3. Present your summary of the feedback and current state and allow the user to guide you as to what to fix/address/skip. DO NOT begin fixing issues automatically.
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-env node */
/* global console, process */
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
async function run(cmd) {
try {
const { stdout } = await execAsync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
return stdout.trim();
} catch (_e) {
// eslint-disable-line @typescript-eslint/no-unused-vars
return null;
}
}
const IGNORE_MESSAGES = [
'thank you so much for your contribution to Gemini CLI!',
"I'm currently reviewing this pull request and will post my feedback shortly.",
'This pull request is being closed because it is not currently linked to an issue.',
];
const shouldIgnore = (body) => {
if (!body) return false;
return IGNORE_MESSAGES.some((msg) => body.includes(msg));
};
async function main() {
const branch = await run('git branch --show-current');
if (!branch) {
console.error('❌ Could not determine current git branch.');
process.exit(1);
}
const gqlQuery = `query($branch:String!){repository(name:"gemini-cli",owner:"google-gemini"){pullRequests(headRefName:$branch,first:100){nodes{id,number,state,comments(first:100){nodes{createdAt,isMinimized,minimizedReason,author{login},body,url,authorAssociation}},reviews(first:100){nodes{id,author{login},createdAt,isMinimized,minimizedReason,body,state,comments(first:30){nodes{id,replyTo{id},author{login},createdAt,body,isMinimized,minimizedReason,path,line,startLine,originalLine,originalStartLine}}}}}}}}`;
const [authInfo, diff, commits, rawJson] = await Promise.all([
run('gh auth status -a'),
run('gh pr diff'),
run(
'git fetch && git log origin/main..origin/$(git branch --show-current)',
),
run(`gh api graphql -F branch="${branch}" -f query='${gqlQuery}'`),
]);
if (!diff) {
console.error(`⚠️ No active PR found for branch: ${branch}`);
process.exit(1);
}
console.log(`\n# Current GitHub user info:\n\n${authInfo}\n`);
console.log(`\n# PR diff for current branch: ${branch}\n\n\`\`\``);
console.log(diff);
console.log('```');
console.log(
`\n# Commit history (origin/main..origin/${branch})\n\n${commits}`,
);
const data = JSON.parse(rawJson || '{}');
const prs = data?.data?.repository?.pullRequests?.nodes || [];
// Sort PRs by number descending so we check the newest one first
prs.sort((a, b) => b.number - a.number);
const pr = prs.find((p) => p.state === 'OPEN') || prs[0];
if (!pr) {
console.error('❌ No PR data found.');
process.exit(1);
}
console.log('\n# PR Feedback\n');
// 1. General PR Comments
const general = pr.comments.nodes.filter((c) => !shouldIgnore(c.body));
if (general.length > 0) {
console.log('\n💬 GENERAL COMMENTS:');
general.forEach((c) => {
const minimized = c.isMinimized
? ` (Minimized: ${c.minimizedReason})`
: '';
console.log(
`[${c.createdAt}] [${c.author.login}]${minimized}: ${c.body}\n`,
);
});
}
// 2. Process ALL Review Comments into a single Thread Map
const allInlineComments = pr.reviews.nodes.flatMap((r) => r.comments.nodes);
const filteredInlines = allInlineComments.filter(
(c) => !shouldIgnore(c.body),
);
console.log('🔍 CODE REVIEWS & INLINE THREADS:');
// Print Review Summaries First
pr.reviews.nodes.forEach((review) => {
if (review.body && !shouldIgnore(review.body)) {
const icon = review.state === 'APPROVED' ? '✅' : '💬';
const minimized = review.isMinimized
? ` (Minimized: ${review.minimizedReason})`
: '';
console.log(
`\n${icon} ${review.state} by ${review.author.login} at ${review.createdAt}${minimized}: "${review.body}"`,
);
}
});
// Build and Print Threads
const topLevelThreads = filteredInlines.filter((c) => !c.replyTo);
const printThread = (parentId, depth = 1) => {
const indent = ' '.repeat(depth);
filteredInlines
.filter((c) => c.replyTo?.id === parentId)
.forEach((reply) => {
const minimized = reply.isMinimized
? ` (Minimized: ${reply.minimizedReason})`
: '';
console.log(
`${indent}↳ [${reply.createdAt}] ${reply.author.login}${minimized}: ${reply.body}`,
);
printThread(reply.id, depth + 1);
});
};
topLevelThreads.forEach((c) => {
const start = c.startLine || c.originalStartLine;
const end = c.line || c.originalLine;
const range = start && end && start !== end ? `${start}-${end}` : end || '';
const fileInfo = c.path
? `(${c.path}${range ? `:${range}` : ''}) `
: range
? `(Line ${range}) `
: '';
const minimized = c.isMinimized ? ` (Minimized: ${c.minimizedReason})` : '';
console.log(
`\n💬 ${minimized}${c.author.login} | ${c.createdAt} ${fileInfo}\n${c.body}`,
);
printThread(c.id);
});
console.log('\n');
}
main().catch((err) => {
console.error('❌ Unexpected error:', err);
process.exit(1);
});
@@ -155,7 +155,10 @@ jobs:
"telemetry": {
"enabled": true,
"target": "gcp"
}
},
"coreTools": [
"run_shell_command(echo)"
],
}
prompt: |-
## Role
@@ -284,8 +287,21 @@ jobs:
return;
}
} else {
core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawOutput}`);
return;
// If no markdown block, try to find a raw JSON object in the output.
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
// before the actual JSON response.
const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
if (jsonObjectMatch) {
try {
parsedLabels = JSON.parse(jsonObjectMatch[0]);
} catch (extractError) {
core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
return;
}
} else {
core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawOutput}`);
return;
}
}
}
+6 -7
View File
@@ -372,8 +372,7 @@ specific debug settings.
### React DevTools
To debug the CLI's React-based UI, you can use React DevTools. Ink, the library
used for the CLI's interface, is compatible with React DevTools version 4.x.
To debug the CLI's React-based UI, you can use React DevTools.
1. **Start the Gemini CLI in development mode:**
@@ -381,20 +380,20 @@ used for the CLI's interface, is compatible with React DevTools version 4.x.
DEV=true npm start
```
2. **Install and run React DevTools version 4.28.5 (or the latest compatible
4.x version):**
2. **Install and run React DevTools version 6 (which matches the CLI's
`react-devtools-core`):**
You can either install it globally:
```bash
npm install -g react-devtools@4.28.5
npm install -g react-devtools@6
react-devtools
```
Or run it directly using npx:
```bash
npx react-devtools@4.28.5
npx react-devtools@6
```
Your running CLI application should then connect to React DevTools.
@@ -546,7 +545,7 @@ Before submitting your documentation pull request, please:
If you have questions about contributing documentation:
- Check our [FAQ](/docs/faq.md).
- Check our [FAQ](/docs/resources/faq.md).
- Review existing documentation for examples.
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
your proposed changes.
+2 -1
View File
@@ -295,7 +295,8 @@ on GitHub.
- **Experimental permission improvements:** We are now experimenting with a new
policy engine in Gemini CLI. This allows users and administrators to create
fine-grained policy for tool calls. Currently behind a flag. See
[policy engine documentation](../core/policy-engine.md) for more information.
[policy engine documentation](../reference/policy-engine.md) for more
information.
- Blog:
[https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/](https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/)
- **Gemini 3 support for paid:** Gemini 3 support has been rolled out to all API
+2 -2
View File
@@ -1,4 +1,4 @@
# Preview release: v0.30.0-preview.1
# Preview release: v0.30.0-preview.3
Released: February 19, 2026
@@ -311,4 +311,4 @@ npm install -g @google/gemini-cli@preview
[#19008](https://github.com/google-gemini/gemini-cli/pull/19008)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.1
https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.3
+23 -23
View File
@@ -26,29 +26,29 @@ and parameters.
## CLI Options
| Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
| `--version` | `-v` | - | - | Show CLI version number and exit |
| `--help` | `-h` | - | - | Show help information |
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../core/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
| Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
| `--version` | `-v` | - | - | Show CLI version number and exit |
| `--help` | `-h` | - | - | Show help information |
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../reference/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
## Model selection
+7 -7
View File
@@ -20,7 +20,7 @@ The most powerful tools for enterprise administration are the system-wide
settings files. These files allow you to define a baseline configuration
(`system-defaults.json`) and a set of overrides (`settings.json`) that apply to
all users on a machine. For a complete overview of configuration options, see
the [Configuration documentation](../get-started/configuration.md).
the [Configuration documentation](../reference/configuration.md).
Settings are merged from four files. The precedence order for single-value
settings (like `theme`) is:
@@ -224,8 +224,8 @@ gemini
You can significantly enhance security by controlling which tools the Gemini
model can use. This is achieved through the `tools.core` setting and the
[Policy Engine](../core/policy-engine.md). For a list of available tools, see
the [Tools documentation](../tools/index.md).
[Policy Engine](../reference/policy-engine.md). For a list of available tools,
see the [Tools documentation](../tools/index.md).
### Allowlisting with `coreTools`
@@ -245,8 +245,8 @@ on the approved list.
### Blocklisting with `excludeTools` (Deprecated)
> **Deprecated:** Use the [Policy Engine](../core/policy-engine.md) for more
> robust control.
> **Deprecated:** Use the [Policy Engine](../reference/policy-engine.md) for
> more robust control.
Alternatively, you can add specific tools that are considered dangerous in your
environment to a blocklist.
@@ -289,8 +289,8 @@ unintended tool execution.
## Managing custom tools (MCP servers)
If your organization uses custom tools via
[Model-Context Protocol (MCP) servers](../core/tools-api.md), it is crucial to
understand how server configurations are managed to apply security policies
[Model-Context Protocol (MCP) servers](../reference/tools-api.md), it is crucial
to understand how server configurations are managed to apply security policies
effectively.
### How MCP server configurations are merged
+1 -1
View File
@@ -88,7 +88,7 @@ More content here.
@../shared/style-guide.md
```
For more details, see the [Memory Import Processor](../core/memport.md)
For more details, see the [Memory Import Processor](../reference/memport.md)
documentation.
## Customize the context file name
+1 -1
View File
@@ -39,7 +39,7 @@ To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
You can also use the `--model` flag to specify a particular Gemini model on
startup. For more details, refer to the
[configuration documentation](../get-started/configuration.md).
[configuration documentation](../reference/configuration.md).
Changes to these settings will be applied to all subsequent interactions with
Gemini CLI.
+129 -74
View File
@@ -1,50 +1,61 @@
# Plan Mode (experimental)
Plan Mode is a safe, read-only mode for researching and designing complex
changes. It prevents modifications while you research, design and plan an
implementation strategy.
Plan Mode is a read-only environment for architecting robust solutions before
implementation. It allows you to:
> **Note: Plan Mode is currently an experimental feature.**
>
> Experimental features are subject to change. To use Plan Mode, enable it via
> `/settings` (search for `Plan`) or add the following to your `settings.json`:
>
> ```json
> {
> "experimental": {
> "plan": true
> }
> }
> ```
>
> Your feedback is invaluable as we refine this feature. If you have ideas,
- **Research:** Explore the project in a read-only state to prevent accidental
changes.
- **Design:** Understand problems, evaluate trade-offs, and choose a solution.
- **Plan:** Align on an execution strategy before any code is modified.
> **Note:** This is a preview feature currently under active development. Your
> feedback is invaluable as we refine this feature. If you have ideas,
> suggestions, or encounter issues:
>
> - Use the `/bug` command within the CLI to file an issue.
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues) on
> GitHub.
> - Use the **/bug** command within Gemini CLI to file an issue.
- [Starting in Plan Mode](#starting-in-plan-mode)
- [Enabling Plan Mode](#enabling-plan-mode)
- [How to use Plan Mode](#how-to-use-plan-mode)
- [Entering Plan Mode](#entering-plan-mode)
- [The Planning Workflow](#the-planning-workflow)
- [Planning Workflow](#planning-workflow)
- [Exiting Plan Mode](#exiting-plan-mode)
- [Tool Restrictions](#tool-restrictions)
- [Customizing Planning with Skills](#customizing-planning-with-skills)
- [Customizing Policies](#customizing-policies)
- [Example: Allow git commands in Plan Mode](#example-allow-git-commands-in-plan-mode)
- [Example: Enable research subagents in Plan Mode](#example-enable-research-subagents-in-plan-mode)
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
## Starting in Plan Mode
## Enabling Plan Mode
You can configure Gemini CLI to start directly in Plan Mode by default:
To use Plan Mode, enable it via **/settings** (search for **Plan**) or add the
following to your `settings.json`:
1. Type `/settings` in the CLI.
2. Search for `Default Approval Mode`.
3. Set the value to `Plan`.
```json
{
"experimental": {
"plan": true
}
}
```
Other ways to start in Plan Mode:
## How to use Plan Mode
- **CLI Flag:** `gemini --approval-mode=plan`
- **Manual Settings:** Manually update your `settings.json`:
### Entering Plan Mode
You can configure Gemini CLI to start in Plan Mode by default or enter it
manually during a session.
- **Configuration:** Configure Gemini CLI to start directly in Plan Mode by
default:
1. Type `/settings` in the CLI.
2. Search for **Default Approval Mode**.
3. Set the value to **Plan**.
Alternatively, use the `gemini --approval-mode=plan` CLI flag or manually
update:
```json
{
@@ -54,42 +65,44 @@ Other ways to start in Plan Mode:
}
```
## How to use Plan Mode
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`).
### Entering Plan Mode
> **Note:** Plan Mode is automatically removed from the rotation when Gemini
> CLI is actively processing or showing confirmation dialogs.
You can enter Plan Mode in three ways:
- **Command:** Type `/plan` in the input box.
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`).
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI then
calls the [`enter_plan_mode`] tool to switch modes.
> **Note:** This tool is not available when Gemini CLI is in [YOLO mode].
> **Note:** Plan Mode is automatically removed from the rotation when the
> agent is actively processing or showing confirmation dialogs.
### Planning Workflow
2. **Command:** Type `/plan` in the input box.
3. **Natural Language:** Ask the agent to "start a plan for...". The agent will
then call the [`enter_plan_mode`] tool to switch modes.
### The Planning Workflow
1. **Requirements:** The agent clarifies goals using [`ask_user`].
2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map
the codebase and validate assumptions.
3. **Design:** The agent proposes alternative approaches with a recommended
solution for you to choose from.
4. **Planning:** A detailed plan is written to a temporary Markdown file.
5. **Review:** You review the plan.
- **Approve:** Exit Plan Mode and start implementation (switching to
Auto-Edit or Default approval mode).
1. **Explore & Analyze:** Analyze requirements and use read-only tools to map
the codebase and validate assumptions. For complex tasks, identify at least
two viable implementation approaches.
2. **Consult:** Present a summary of the identified approaches via [`ask_user`]
to obtain a selection. For simple or canonical tasks, this step may be
skipped.
3. **Draft:** Once an approach is selected, write a detailed implementation
plan to the plans directory.
4. **Review & Approval:** Use the [`exit_plan_mode`] tool to present the plan
and formally request approval.
- **Approve:** Exit Plan Mode and start implementation.
- **Iterate:** Provide feedback to refine the plan.
For more complex or specialized planning tasks, you can
[customize the planning workflow with skills](#customizing-planning-with-skills).
### Exiting Plan Mode
To exit Plan Mode:
To exit Plan Mode, you can:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
2. **Tool:** The agent calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
- **Tool:** Gemini CLI calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
## Tool Restrictions
@@ -102,30 +115,31 @@ These are the only allowed tools:
- **Interaction:** [`ask_user`]
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory.
- **Planning (Write):** [`write_file`] and [`replace`] only allowed for `.md`
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory or your
[custom plans directory](#custom-plan-directory-and-policies).
- **Skills:** [`activate_skill`] (allows loading specialized instructions and
resources in a read-only manner)
### Customizing Planning with Skills
You can leverage [Agent Skills](./skills.md) to customize how Gemini CLI
approaches planning for specific types of tasks. When a skill is activated
during Plan Mode, its specialized instructions and procedural workflows will
guide the research and design phases.
You can use [Agent Skills](./skills.md) to customize how Gemini CLI approaches
planning for specific types of tasks. When a skill is activated during Plan
Mode, its specialized instructions and procedural workflows will guide the
research, design and planning phases.
For example:
- A **"Database Migration"** skill could ensure the plan includes data safety
checks and rollback strategies.
- A **"Security Audit"** skill could prompt the agent to look for specific
- A **"Security Audit"** skill could prompt Gemini CLI to look for specific
vulnerabilities during codebase exploration.
- A **"Frontend Design"** skill could guide the agent to use specific UI
- A **"Frontend Design"** skill could guide Gemini CLI to use specific UI
components and accessibility standards in its proposal.
To use a skill in Plan Mode, you can explicitly ask the agent to "use the
[skill-name] skill to plan..." or the agent may autonomously activate it based
on the task description.
To use a skill in Plan Mode, you can explicitly ask Gemini CLI to "use the
`<skill-name>` skill to plan..." or Gemini CLI may autonomously activate it
based on the task description.
### Customizing Policies
@@ -137,7 +151,7 @@ Because user policies (Tier 2) have a higher base priority than built-in
policies (Tier 1), you can override Plan Mode's default restrictions by creating
a rule in your `~/.gemini/policies/` directory.
#### Example: Allow `git status` and `git diff` in Plan Mode
#### Example: Allow git commands in Plan Mode
This rule allows you to check the repository status and see changes while in
Plan Mode.
@@ -153,10 +167,10 @@ priority = 100
modes = ["plan"]
```
#### Example: Enable research sub-agents in Plan Mode
#### Example: Enable research subagents in Plan Mode
You can enable [experimental research sub-agents] like `codebase_investigator`
to help gather architecture details during the planning phase.
You can enable experimental research [subagents] like `codebase_investigator` to
help gather architecture details during the planning phase.
`~/.gemini/policies/research-subagents.toml`
@@ -168,11 +182,51 @@ priority = 100
modes = ["plan"]
```
Tell the agent it can use these tools in your prompt, for example: _"You can
Tell Gemini CLI it can use these tools in your prompt, for example: _"You can
check ongoing changes in git."_
For more information on how the policy engine works, see the [Policy Engine
Guide].
For more information on how the policy engine works, see the [policy engine]
docs.
### Custom Plan Directory and Policies
By default, planning artifacts are stored in a managed temporary directory
outside your project: `~/.gemini/tmp/<project>/<session-id>/plans/`.
You can configure a custom directory for plans in your `settings.json`. For
example, to store plans in a `.gemini/plans` directory within your project:
```json
{
"general": {
"plan": {
"directory": ".gemini/plans"
}
}
}
```
To maintain the safety of Plan Mode, user-configured paths for the plans
directory are restricted to the project root. This ensures that custom planning
locations defined within a project's workspace cannot be used to escape and
overwrite sensitive files elsewhere. Any user-configured directory must reside
within the project boundary.
Using a custom directory requires updating your [policy engine] configurations
to allow `write_file` and `replace` in that specific location. For example, to
allow writing to the `.gemini/plans` directory within your project, create a
policy file at `~/.gemini/policies/plan-custom-directory.toml`:
```toml
[[rule]]
toolName = ["write_file", "replace"]
decision = "allow"
priority = 100
modes = ["plan"]
# Adjust the pattern to match your custom directory.
# This example matches any .md file in a .gemini/plans directory within the project.
argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\""
```
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
@@ -183,8 +237,9 @@ Guide].
[`replace`]: /docs/tools/file-system.md#6-replace-edit
[MCP tools]: /docs/tools/mcp-server.md
[`activate_skill`]: /docs/cli/skills.md
[experimental research sub-agents]: /docs/core/subagents.md
[Policy Engine Guide]: /docs/core/policy-engine.md
[subagents]: /docs/core/subagents.md
[policy engine]: /docs/reference/policy-engine.md
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: /docs/tools/ask-user.md
[YOLO mode]: /docs/reference/configuration.md#command-line-arguments
+3 -3
View File
@@ -167,6 +167,6 @@ gemini -s -p "run shell command: mount | grep workspace"
## Related documentation
- [Configuration](../get-started/configuration.md): Full configuration options.
- [Commands](./commands.md): Available commands.
- [Troubleshooting](../troubleshooting.md): General troubleshooting.
- [Configuration](../reference/configuration.md): Full configuration options.
- [Commands](../reference/commands.md): Available commands.
- [Troubleshooting](../resources/troubleshooting.md): General troubleshooting.
+47 -46
View File
@@ -22,16 +22,16 @@ they appear in the UI.
### General
| UI Label | Setting | Description | Default |
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | 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"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
| 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` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | 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"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
| 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` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
### Output
@@ -41,36 +41,36 @@ they appear in the UI.
### UI
| UI Label | Setting | Description | Default |
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
### IDE
@@ -128,12 +128,13 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
### Skills
+1 -1
View File
@@ -93,7 +93,7 @@ Environment variables can be used to override the settings in the file.
`true` or `1` will enable the feature. Any other value will disable it.
For detailed information about all configuration options, see the
[Configuration guide](../get-started/configuration.md).
[Configuration guide](../reference/configuration.md).
## Google Cloud telemetry
+3 -3
View File
@@ -41,8 +41,8 @@ can change the theme using the `/theme` command.
### Theme persistence
Selected themes are saved in Gemini CLI's
[configuration](../get-started/configuration.md) so your preference is
remembered across sessions.
[configuration](../reference/configuration.md) so your preference is remembered
across sessions.
---
@@ -194,7 +194,7 @@ untrusted sources.
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui`
object in your `settings.json`.
- Custom themes can be set at the user, project, or system level, and follow the
same [configuration precedence](../get-started/configuration.md) as other
same [configuration precedence](../reference/configuration.md) as other
settings.
### Themes from extensions
+31
View File
@@ -38,6 +38,37 @@ folder, a dialog will automatically appear, prompting you to make a choice:
Your choice is saved in a central file (`~/.gemini/trustedFolders.json`), so you
will only be asked once per folder.
## Understanding folder contents: The discovery phase
Before you make a choice, the Gemini CLI performs a **discovery phase** to scan
the folder for potential configurations. This information is displayed in the
trust dialog to help you make an informed decision.
The discovery UI lists the following categories of items found in the project:
- **Commands**: Custom `.toml` command definitions that add new functionality.
- **MCP Servers**: Configured Model Context Protocol servers that the CLI will
attempt to connect to.
- **Hooks**: System or custom hooks that can intercept and modify CLI behavior.
- **Skills**: Local agent skills that provide specialized capabilities.
- **Setting overrides**: Any project-specific configurations that override your
global user settings.
### Security warnings and errors
The trust dialog also highlights critical information that requires your
attention:
- **Security Warnings**: The CLI will explicitly flag potentially dangerous
settings, such as auto-approving certain tools or disabling the security
sandbox.
- **Discovery Errors**: If the CLI encounters issues while scanning the folder
(e.g., a malformed `settings.json` file), these errors will be displayed
prominently.
By reviewing these details, you can ensure that you only grant trust to projects
that you know are safe.
## Why trust matters: The impact of an untrusted workspace
When a folder is **untrusted**, the Gemini CLI runs in a restricted "safe mode"
+2 -2
View File
@@ -121,6 +121,6 @@ immediately. Force a reload with:
- Learn about [Session management](session-management.md) to see how short-term
history works.
- Explore the [Command reference](../../cli/commands.md) for more `/memory`
options.
- Explore the [Command reference](../../reference/commands.md) for more
`/memory` options.
- Read the technical spec for [Project context](../../cli/gemini-md.md).
+1 -1
View File
@@ -101,5 +101,5 @@ This creates a new branch of history without losing your original work.
- Learn about [Checkpointing](../../cli/checkpointing.md) to understand the
underlying safety mechanism.
- Explore [Task planning](task-planning.md) to keep complex sessions organized.
- See the [Command reference](../../cli/commands.md) for all `/chat` and
- See the [Command reference](../../reference/commands.md) for all `/chat` and
`/resume` options.
+7 -7
View File
@@ -9,11 +9,11 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
- **[Sub-agents (experimental)](./subagents.md):** Learn how to create and use
specialized sub-agents for complex tasks.
- **[Core tools API](./tools-api.md):** Information on how tools are defined,
registered, and used by the core.
- **[Memory Import Processor](./memport.md):** Documentation for the modular
GEMINI.md import feature using @file.md syntax.
- **[Policy Engine](./policy-engine.md):** Use the Policy Engine for
- **[Core tools API](../reference/tools-api.md):** Information on how tools are
defined, registered, and used by the core.
- **[Memory Import Processor](../reference/memport.md):** Documentation for the
modular GEMINI.md import feature using @file.md syntax.
- **[Policy Engine](../reference/policy-engine.md):** Use the Policy Engine for
fine-grained control over tool execution.
## Role of the core
@@ -92,8 +92,8 @@ This allows you to have global, project-level, and component-level context
files, which are all combined to provide the model with the most relevant
information.
You can use the [`/memory` command](../cli/commands.md) to `show`, `add`, and
`refresh` the content of loaded `GEMINI.md` files.
You can use the [`/memory` command](../reference/commands.md) to `show`, `add`,
and `refresh` the content of loaded `GEMINI.md` files.
## Citations
+117 -1
View File
@@ -17,7 +17,7 @@ the main agent's context or toolset.
> ```
>
> **Warning:** Subagents currently operate in
> ["YOLO mode"](../get-started/configuration.md#command-line-arguments), meaning
> ["YOLO mode"](../reference/configuration.md#command-line-arguments), meaning
> they may execute tools without individual user confirmation for each step.
> Proceed with caution when defining agents with powerful tools like
> `run_shell_command` or `write_file`.
@@ -80,6 +80,122 @@ Gemini CLI comes with the following built-in subagents:
invoked by the user.
- **Configuration:** Enabled by default. No specific configuration options.
### Browser Agent (experimental)
- **Name:** `browser_agent`
- **Purpose:** Automate web browser tasks — navigating websites, filling forms,
clicking buttons, and extracting information from web pages — using the
accessibility tree.
- **When to use:** "Go to example.com and fill out the contact form," "Extract
the pricing table from this page," "Click the login button and enter my
credentials."
> **Note:** This is a preview feature currently under active development.
#### Prerequisites
The browser agent requires:
- **Chrome** version 144 or later (any recent stable release will work).
- **Node.js** with `npx` available (used to launch the
[`chrome-devtools-mcp`](https://www.npmjs.com/package/chrome-devtools-mcp)
server).
#### Enabling the browser agent
The browser agent is disabled by default. Enable it in your `settings.json`:
```json
{
"agents": {
"overrides": {
"browser_agent": {
"enabled": true
}
}
}
}
```
#### Session modes
The `sessionMode` setting controls how Chrome is launched and managed. Set it
under `agents.browser`:
```json
{
"agents": {
"overrides": {
"browser_agent": {
"enabled": true
}
},
"browser": {
"sessionMode": "persistent"
}
}
}
```
The available modes are:
| Mode | Description |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `persistent` | **(Default)** Launches Chrome with a persistent profile stored at `~/.gemini/cli-browser-profile/`. Cookies, history, and settings are preserved between sessions. |
| `isolated` | Launches Chrome with a temporary profile that is deleted after each session. Use this for clean-state automation. |
| `existing` | Attaches to an already-running Chrome instance. You must enable remote debugging first by navigating to `chrome://inspect/#remote-debugging` in Chrome. No new browser process is launched. |
#### Configuration reference
All browser-specific settings go under `agents.browser` in your `settings.json`.
| Setting | Type | Default | Description |
| :------------ | :-------- | :------------- | :---------------------------------------------------------------------------------------------- |
| `sessionMode` | `string` | `"persistent"` | How Chrome is managed: `"persistent"`, `"isolated"`, or `"existing"`. |
| `headless` | `boolean` | `false` | Run Chrome in headless mode (no visible window). |
| `profilePath` | `string` | — | Custom path to a browser profile directory. |
| `visualModel` | `string` | — | Model override for the visual agent (for example, `"gemini-2.5-computer-use-preview-10-2025"`). |
#### Security
The browser agent enforces the following security restrictions:
- **Blocked URL patterns:** `file://`, `javascript:`, `data:text/html`,
`chrome://extensions`, and `chrome://settings/passwords` are always blocked.
- **Sensitive action confirmation:** Actions like form filling, file uploads,
and form submissions require user confirmation through the standard policy
engine.
#### Visual agent
By default, the browser agent interacts with pages through the accessibility
tree using element `uid` values. For tasks that require visual identification
(for example, "click the yellow button" or "find the red error message"), you
can enable the visual agent by setting a `visualModel`:
```json
{
"agents": {
"overrides": {
"browser_agent": {
"enabled": true
}
},
"browser": {
"visualModel": "gemini-2.5-computer-use-preview-10-2025"
}
}
}
```
When enabled, the agent gains access to the `analyze_screenshot` tool, which
captures a screenshot and sends it to the vision model for analysis. The model
returns coordinates and element descriptions that the browser agent uses with
the `click_at` tool for precise, coordinate-based interactions.
> **Note:** The visual agent requires API key or Vertex AI authentication. It is
> not available when using Google Login.
## Creating custom subagents
You can create your own subagents to automate specific workflows or enforce
+1 -1
View File
@@ -130,7 +130,7 @@ The manifest file defines the extension's behavior and configuration.
- `description`: A short summary shown in the extension gallery.
- <a id="mcp-servers"></a>`mcpServers`: A map of Model Context Protocol (MCP)
servers. Extension servers follow the same format as standard
[CLI configuration](../get-started/configuration.md).
[CLI configuration](../reference/configuration.md).
- `contextFileName`: The name of the context file (defaults to `GEMINI.md`). Can
also be an array of strings to load multiple context files.
- `excludeTools`: An array of tools to block from the model. You can restrict
+4 -4
View File
@@ -22,8 +22,8 @@ Select the authentication method that matches your situation in the table below:
### What is my Google account type?
- **Individual Google accounts:** Includes all
[free tier accounts](../quota-and-pricing/#free-usage) such as Gemini Code
Assist for individuals, as well as paid subscriptions for
[free tier accounts](../resources/quota-and-pricing.md#free-usage) such as
Gemini Code Assist for individuals, as well as paid subscriptions for
[Google AI Pro and Ultra](https://gemini.google/subscriptions/).
- **Organization accounts:** Accounts using paid licenses through an
@@ -317,5 +317,5 @@ configure authentication using environment variables:
Your authentication method affects your quotas, pricing, Terms of Service, and
privacy notices. Review the following pages to learn more:
- [Gemini CLI: Quotas and Pricing](../quota-and-pricing.md).
- [Gemini CLI: Terms of Service and Privacy Notice](../tos-privacy.md).
- [Gemini CLI: Quotas and Pricing](../resources/quota-and-pricing.md).
- [Gemini CLI: Terms of Service and Privacy Notice](../resources/tos-privacy.md).
+17 -3
View File
@@ -2,6 +2,21 @@
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
> **Note:** Gemini 3.1 Pro Preview is rolling out. To determine whether you have
> access to Gemini 3.1, use the `/model` command and select **Manual**. If you
> have access, you will see `gemini-3.1-pro-preview`.
>
> If you have access to Gemini 3.1, it will be included in model routing when
> you select **Auto (Gemini 3)**. You can also launch the Gemini 3.1 model
> directly using the `-m` flag:
>
> ```
> gemini -m gemini-3.1-pro-preview
> ```
>
> Learn more about [models](../cli/model.md) and
> [model routing](../cli/model-routing.md).
## How to get started with Gemini 3 on Gemini CLI
Get started by upgrading Gemini CLI to the latest version:
@@ -12,9 +27,8 @@ npm install -g @google/gemini-cli@latest
After youve confirmed your version is 0.21.1 or later:
1. Use the `/settings` command in Gemini CLI.
2. Toggle **Preview Features** to `true`.
3. Run `/model` and select **Auto (Gemini 3)**.
1. Run `/model`.
2. Select **Auto (Gemini 3)**.
For more information, see [Gemini CLI model selection](../cli/model.md).
+11 -1
View File
@@ -54,7 +54,7 @@ Gemini CLI offers several ways to configure its behavior, including environment
variables, command-line arguments, and settings files.
To explore your configuration options, see
[Gemini CLI Configuration](./configuration.md).
[Gemini CLI Configuration](../reference/configuration.md).
## Use
@@ -64,6 +64,16 @@ and more.
To explore the power of Gemini CLI, see [Gemini CLI examples](./examples.md).
## Check usage and quota
You can check your current token usage and quota information using the
`/stats model` command. This command provides a snapshot of your current
session's token usage, as well as your overall quota and usage for the supported
models.
For more information on the `/stats` command and its subcommands, see the
[Command Reference](../../reference/commands.md#stats).
## Next steps
- Follow the [File management](../cli/tutorials/file-management.md) guide to
+1 -1
View File
@@ -420,7 +420,7 @@ When you open a project with hooks defined in `.gemini/settings.json`:
Hooks inherit the environment of the Gemini CLI process, which may include
sensitive API keys. Gemini CLI provides a
[redaction system](/docs/get-started/configuration#environment-variable-redaction)
[redaction system](/docs/reference/configuration.md#environment-variable-redaction)
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
`TOKEN`).
+19 -46
View File
@@ -48,41 +48,8 @@ User-focused guides and tutorials for daily development workflows.
## Features
Technical reference documentation for each capability of Gemini CLI.
Technical documentation for each capability of Gemini CLI.
- **[/about](./cli/commands.md#about):** About Gemini CLI.
- **[/auth](./get-started/authentication.md):** Authentication.
- **[/bug](./cli/commands.md#bug):** Report a bug.
- **[/chat](./cli/commands.md#chat):** Chat history.
- **[/clear](./cli/commands.md#clear):** Clear screen.
- **[/compress](./cli/commands.md#compress):** Compress context.
- **[/copy](./cli/commands.md#copy):** Copy output.
- **[/directory](./cli/commands.md#directory-or-dir):** Manage workspace.
- **[/docs](./cli/commands.md#docs):** Open documentation.
- **[/editor](./cli/commands.md#editor):** Select editor.
- **[/extensions](./extensions/index.md):** Manage extensions.
- **[/help](./cli/commands.md#help-or):** Show help.
- **[/hooks](./hooks/index.md):** Hooks.
- **[/ide](./ide-integration/index.md):** IDE integration.
- **[/init](./cli/commands.md#init):** Initialize context.
- **[/mcp](./tools/mcp-server.md):** MCP servers.
- **[/memory](./cli/commands.md#memory):** Manage memory.
- **[/model](./cli/model.md):** Model selection.
- **[/policies](./cli/commands.md#policies):** Manage policies.
- **[/privacy](./cli/commands.md#privacy):** Privacy notice.
- **[/quit](./cli/commands.md#quit-or-exit):** Exit CLI.
- **[/restore](./cli/checkpointing.md):** Restore files.
- **[/resume](./cli/commands.md#resume):** Resume session.
- **[/rewind](./cli/rewind.md):** Rewind.
- **[/settings](./cli/settings.md):** Settings.
- **[/setup-github](./cli/commands.md#setup-github):** GitHub setup.
- **[/shells](./cli/commands.md#shells-or-bashes):** Manage processes.
- **[/skills](./cli/skills.md):** Agent skills.
- **[/stats](./cli/commands.md#stats):** Session statistics.
- **[/terminal-setup](./cli/commands.md#terminal-setup):** Terminal keybindings.
- **[/theme](./cli/themes.md):** Themes.
- **[/tools](./cli/commands.md#tools):** List tools.
- **[/vim](./cli/commands.md#vim):** Vim mode.
- **[Activate skill (tool)](./tools/activate-skill.md):** Internal mechanism for
loading expert procedures.
- **[Ask user (tool)](./tools/ask-user.md):** Internal dialog system for
@@ -97,12 +64,12 @@ Technical reference documentation for each capability of Gemini CLI.
- **[Model routing](./cli/model-routing.md):** Automatic fallback resilience.
- **[Plan mode 🧪](./cli/plan-mode.md):** Use a safe, read-only mode for
planning complex changes.
- **[Subagents 🧪](./core/subagents.md):** Using specialized agents for specific
tasks.
- **[Remote subagents 🧪](./core/remote-agents.md):** Connecting to and using
remote agents.
- **[Sandboxing](./cli/sandbox.md):** Isolate tool execution.
- **[Shell (tool)](./tools/shell.md):** Detailed system execution parameters.
- **[Subagents 🧪](./core/subagents.md):** Using specialized agents for specific
tasks.
- **[Telemetry](./cli/telemetry.md):** Usage and performance metric details.
- **[Todo (tool)](./tools/todos.md):** Progress tracking specification.
- **[Token caching](./cli/token-caching.md):** Performance optimization.
@@ -134,23 +101,29 @@ Settings and customization options for Gemini CLI.
Deep technical documentation and API specifications.
- **[Command reference](./cli/commands.md):** Detailed slash command guide.
- **[Configuration reference](./get-started/configuration.md):** Settings and
- **[Command reference](./reference/commands.md):** Detailed slash command
guide.
- **[Configuration reference](./reference/configuration.md):** Settings and
environment variables.
- **[Keyboard shortcuts](./cli/keyboard-shortcuts.md):** Productivity tips.
- **[Memory import processor](./core/memport.md):** How Gemini CLI processes
memory from various sources.
- **[Policy engine](./core/policy-engine.md):** Fine-grained execution control.
- **[Tools API](./core/tools-api.md):** The API for defining and using tools.
- **[Keyboard shortcuts](./reference/keyboard-shortcuts.md):** Productivity
tips.
- **[Memory import processor](./reference/memport.md):** How Gemini CLI
processes memory from various sources.
- **[Policy engine](./reference/policy-engine.md):** Fine-grained execution
control.
- **[Tools API](./reference/tools-api.md):** The API for defining and using
tools.
## Resources
Support, release history, and legal information.
- **[FAQ](./faq.md):** Answers to frequently asked questions.
- **[FAQ](./resources/faq.md):** Answers to frequently asked questions.
- **[Changelogs](./changelogs/index.md):** Highlights and notable changes.
- **[Quota and pricing](./quota-and-pricing.md):** Limits and billing details.
- **[Terms and privacy](./tos-privacy.md):** Official notices and terms.
- **[Quota and pricing](./resources/quota-and-pricing.md):** Limits and billing
details.
- **[Terms and privacy](./resources/tos-privacy.md):** Official notices and
terms.
## Development
+19
View File
@@ -0,0 +1,19 @@
{
"/docs/architecture": "/docs/cli/index",
"/docs/cli/commands": "/docs/reference/commands",
"/docs/cli": "/docs",
"/docs/cli/index": "/docs",
"/docs/cli/keyboard-shortcuts": "/docs/reference/keyboard-shortcuts",
"/docs/cli/uninstall": "/docs/resources/uninstall",
"/docs/core/concepts": "/docs",
"/docs/core/memport": "/docs/reference/memport",
"/docs/core/policy-engine": "/docs/reference/policy-engine",
"/docs/core/tools-api": "/docs/reference/tools-api",
"/docs/faq": "/docs/resources/faq",
"/docs/get-started/configuration": "/docs/reference/configuration",
"/docs/get-started/configuration-v1": "/docs/reference/configuration",
"/docs/index": "/docs",
"/docs/quota-and-pricing": "/docs/resources/quota-and-pricing",
"/docs/tos-privacy": "/docs/resources/tos-privacy",
"/docs/troubleshooting": "/docs/resources/troubleshooting"
}
@@ -32,6 +32,8 @@ Slash commands provide meta-level control over the CLI itself.
conversation state interactively, or resuming a previous state from a later
session.
- **Sub-commands:**
- **`debug`**
- **Description:** Export the most recent API request as a JSON payload.
- **`delete <tag>`**
- **Description:** Deletes a saved conversation checkpoint.
- **`list`**
@@ -128,8 +130,29 @@ Slash commands provide meta-level control over the CLI itself.
### `/extensions`
- **Description:** Lists all active extensions in the current Gemini CLI
session. See [Gemini CLI Extensions](../extensions/index.md).
- **Description:** Manage extensions. See
[Gemini CLI Extensions](../extensions/index.md).
- **Sub-commands:**
- **`config`**:
- **Description:** Configure extension settings.
- **`disable`**:
- **Description:** Disable an extension.
- **`enable`**:
- **Description:** Enable an extension.
- **`explore`**:
- **Description:** Open extensions page in your browser.
- **`install`**:
- **Description:** Install an extension from a git repo or local path.
- **`link`**:
- **Description:** Link an extension from a local path.
- **`list`**:
- **Description:** List active extensions.
- **`restart`**:
- **Description:** Restart all extensions.
- **`uninstall`**:
- **Description:** Uninstall an extension.
- **`update`**:
- **Description:** Update extensions. Usage: update <extension-names>|--all
### `/help` (or `/?`)
@@ -184,6 +207,10 @@ Slash commands provide meta-level control over the CLI itself.
servers that support OAuth authentication.
- **`desc`**
- **Description:** List configured MCP servers and tools with descriptions.
- **`disable`**
- **Description:** Disable an MCP server.
- **`enable`**
- **Description:** Enable a disabled MCP server.
- **`list`** or **`ls`**:
- **Description:** List configured MCP servers and tools. This is the
default action if no subcommand is specified.
@@ -217,11 +244,25 @@ Slash commands provide meta-level control over the CLI itself.
model.
- **Note:** For more details on how `GEMINI.md` files contribute to
hierarchical memory, see the
[CLI Configuration documentation](../get-started/configuration.md).
[CLI Configuration documentation](./configuration.md).
### `/model`
- **Description:** Opens a dialog to choose your Gemini model.
- **Description:** Manage model configuration.
- **Sub-commands:**
- **`manage`**:
- **Description:** Opens a dialog to configure the model.
- **`set`**:
- **Description:** Set the model to use.
- **Usage:** `/model set <model-name> [--persist]`
### `/permissions`
- **Description:** Manage folder trust settings and other permissions.
- **Sub-commands:**
- **`trust`**:
- **Description:** Manage folder trust settings.
- **Usage:** `/permissions trust [<directory-path>]`
### `/plan`
@@ -254,7 +295,7 @@ Slash commands provide meta-level control over the CLI itself.
checkpoints to restore from.
- **Usage:** `/restore [tool_call_id]`
- **Note:** Only available if checkpointing is configured via
[settings](../get-started/configuration.md). See
[settings](./configuration.md). See
[Checkpointing documentation](../cli/checkpointing.md) for more details.
### `/rewind`
@@ -293,7 +334,8 @@ Slash commands provide meta-level control over the CLI itself.
settings that control the behavior and appearance of Gemini CLI. It is
equivalent to manually editing the `.gemini/settings.json` file, but with
validation and guidance to prevent errors. See the
[settings documentation](./settings.md) for a full list of available settings.
[settings documentation](../cli/settings.md) for a full list of available
settings.
- **Usage:** Simply run `/settings` and the editor will open. You can then
browse or search for specific settings, view their current values, and modify
them as desired. Changes to some settings are applied immediately, while
@@ -330,10 +372,16 @@ Slash commands provide meta-level control over the CLI itself.
### `/stats`
- **Description:** Display detailed statistics for the current Gemini CLI
session, including token usage, cached token savings (when available), and
session duration. Note: Cached token information is only displayed when cached
tokens are being used, which occurs with API key authentication but not with
OAuth authentication at this time.
session.
- **Sub-commands:**
- **`session`**:
- **Description:** Show session-specific usage statistics, including
duration, tool calls, and performance metrics. This is the default view.
- **`model`**:
- **Description:** Show model-specific usage statistics, including token
counts and quota information.
- **`tools`**:
- **Description:** Show tool-specific usage statistics.
### `/terminal-setup`
@@ -380,7 +428,8 @@ Slash commands provide meta-level control over the CLI itself.
Custom commands allow you to create personalized shortcuts for your most-used
prompts. For detailed instructions on how to create, manage, and use them,
please see the dedicated [Custom Commands documentation](./custom-commands.md).
please see the dedicated
[Custom Commands documentation](../cli/custom-commands.md).
## Input prompt shortcuts
@@ -131,10 +131,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`general.enablePromptCompletion`** (boolean):
- **Description:** Enable AI-powered prompt completion suggestions while
typing.
- **Default:** `false`
- **`general.plan.directory`** (string):
- **Description:** The directory where planning artifacts are stored. If not
specified, defaults to the system temporary directory.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`general.retryFetchErrors`** (boolean):
@@ -305,13 +305,20 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Show the spinner during operations.
- **Default:** `true`
- **`ui.loadingPhrases`** (enum):
- **Description:** What to show while the model is working: tips, witty
comments, both, or nothing.
- **Default:** `"tips"`
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
- **`ui.customWittyPhrases`** (array):
- **Description:** Custom witty phrases to display during loading. When
provided, the CLI cycles through these instead of the defaults.
- **Default:** `[]`
- **`ui.accessibility.enableLoadingPhrases`** (boolean):
- **Description:** Enable loading phrases during operations.
- **Description:** @deprecated Use ui.loadingPhrases instead. Enable loading
phrases during operations.
- **Default:** `true`
- **Requires restart:** Yes
@@ -634,6 +641,27 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `{}`
- **Requires restart:** Yes
- **`agents.browser.sessionMode`** (enum):
- **Description:** Session mode: 'persistent', 'isolated', or 'existing'.
- **Default:** `"persistent"`
- **Values:** `"persistent"`, `"isolated"`, `"existing"`
- **Requires restart:** Yes
- **`agents.browser.headless`** (boolean):
- **Description:** Run browser in headless mode.
- **Default:** `false`
- **Requires restart:** Yes
- **`agents.browser.profilePath`** (string):
- **Description:** Path to browser profile directory for session persistence.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`agents.browser.visualModel`** (string):
- **Description:** Model override for the visual agent.
- **Default:** `undefined`
- **Requires restart:** Yes
#### `context`
- **`context.fileName`** (string | string[]):
@@ -941,8 +969,15 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
(useful for remote sessions).
- **Description:** Use OSC 52 for pasting. This may be more robust than the
default system when using remote terminal sessions (if your terminal is
configured to allow it).
- **Default:** `false`
- **`experimental.useOSC52Copy`** (boolean):
- **Description:** Use OSC 52 for copying. This may be more robust than the
default system when using remote terminal sessions (if your terminal is
configured to allow it).
- **Default:** `false`
- **`experimental.plan`** (boolean):
@@ -1214,8 +1249,8 @@ within your user's home folder.
Environment variables are a common way to configure applications, especially for
sensitive information like API keys or for settings that might change between
environments. For authentication setup, see the
[Authentication documentation](./authentication.md) which covers all available
authentication methods.
[Authentication documentation](../get-started/authentication.md) which covers
all available authentication methods.
The CLI automatically loads environment variables from an `.env` file. The
loading order is:
@@ -1234,7 +1269,8 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_API_KEY`**:
- Your API key for the Gemini API.
- One of several available [authentication methods](./authentication.md).
- One of several available
[authentication methods](../get-started/authentication.md).
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env`
file.
- **`GEMINI_MODEL`**:
@@ -1580,15 +1616,15 @@ conventions and context.
about the active instructional context.
- **Importing content:** You can modularize your context files by importing
other Markdown files using the `@path/to/file.md` syntax. For more details,
see the [Memory Import Processor documentation](../core/memport.md).
see the [Memory Import Processor documentation](./memport.md).
- **Commands for memory management:**
- Use `/memory refresh` to force a re-scan and reload of all context files
from all configured locations. This updates the AI's instructional context.
- Use `/memory show` to display the combined instructional context currently
loaded, allowing you to verify the hierarchy and content being used by the
AI.
- See the [Commands documentation](../cli/commands.md#memory) for full details
on the `/memory` command and its sub-commands (`show` and `refresh`).
- See the [Commands documentation](./commands.md#memory) for full details on
the `/memory` command and its sub-commands (`show` and `refresh`).
By understanding and utilizing these configuration layers and the hierarchical
nature of context files, you can effectively manage the AI's memory and tailor
@@ -64,9 +64,11 @@ primary conditions are the tool's name and its arguments.
The `toolName` in the rule must match the name of the tool being called.
- **Wildcards**: For Model-hosting-protocol (MCP) servers, you can use a
wildcard. A `toolName` of `my-server__*` will match any tool from the
`my-server` MCP.
- **Wildcards**: You can use wildcards to match multiple tools.
- `*`: Matches **any tool** (built-in or MCP).
- `server__*`: Matches any tool from a specific MCP server.
- `*__toolName`: Matches a specific tool name across **all** MCP servers.
- `*__*`: Matches **any tool from any MCP server**.
#### Arguments pattern
@@ -92,11 +94,12 @@ rule with the highest priority wins**.
To provide a clear hierarchy, policies are organized into three tiers. Each tier
has a designated number that forms the base of the final priority calculation.
| Tier | Base | Description |
| :------ | :--- | :------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
| User | 2 | Custom policies defined by the user. |
| Admin | 3 | Policies managed by an administrator (e.g., in an enterprise environment). |
| Tier | Base | Description |
| :-------- | :--- | :------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
| Workspace | 2 | Policies defined in the current workspace's configuration directory. |
| User | 3 | Custom policies defined by the user. |
| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). |
Within a TOML policy file, you assign a priority value from **0 to 999**. The
engine transforms this into a final priority using the following formula:
@@ -105,15 +108,17 @@ engine transforms this into a final priority using the following formula:
This system guarantees that:
- Admin policies always override User and Default policies.
- User policies always override Default policies.
- Admin policies always override User, Workspace, and Default policies.
- User policies override Workspace and Default policies.
- Workspace policies override Default policies.
- You can still order rules within a single tier with fine-grained control.
For example:
- A `priority: 50` rule in a Default policy file becomes `1.050`.
- A `priority: 100` rule in a User policy file becomes `2.100`.
- A `priority: 20` rule in an Admin policy file becomes `3.020`.
- A `priority: 10` rule in a Workspace policy policy file becomes `2.010`.
- A `priority: 100` rule in a User policy file becomes `3.100`.
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
### Approval modes
@@ -141,9 +146,9 @@ A rule matches a tool call if all of its conditions are met:
1. **Tool name**: The `toolName` in the rule must match the name of the tool
being called.
- **Wildcards**: For Model-hosting-protocol (MCP) servers, you can use a
wildcard. A `toolName` of `my-server__*` will match any tool from the
`my-server` MCP.
- **Wildcards**: You can use wildcards like `*`, `server__*`, or
`*__toolName` to match multiple tools. See [Tool Name](#tool-name) for
details.
2. **Arguments pattern**: If `argsPattern` is specified, the tool's arguments
are converted to a stable JSON string, which is then tested against the
provided regular expression. If the arguments don't match the pattern, the
@@ -156,10 +161,11 @@ User, and (if configured) Admin directories.
### Policy locations
| Tier | Type | Location |
| :-------- | :----- | :-------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
| Tier | Type | Location |
| :------------ | :----- | :---------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
#### System-wide policies (Admin)
@@ -268,13 +274,12 @@ priority = 100
### Special syntax for MCP tools
You can create rules that target tools from Model-hosting-protocol (MCP) servers
using the `mcpName` field or a wildcard pattern.
You can create rules that target tools from Model Context Protocol (MCP) servers
using the `mcpName` field or composite wildcard patterns.
**1. Using `mcpName`**
**1. Targeting a specific tool on a server**
To target a specific tool from a specific server, combine `mcpName` and
`toolName`.
Combine `mcpName` and `toolName` to target a single operation.
```toml
# Allows the `search` tool on the `my-jira-server` MCP
@@ -285,10 +290,10 @@ decision = "allow"
priority = 200
```
**2. Using a wildcard**
**2. Targeting all tools on a specific server**
To create a rule that applies to _all_ tools on a specific MCP server, specify
only the `mcpName`.
Specify only the `mcpName` to apply a rule to every tool provided by that
server.
```toml
# Denies all tools from the `untrusted-server` MCP
@@ -299,6 +304,33 @@ priority = 500
deny_message = "This server is not trusted by the admin."
```
**3. Targeting all MCP servers**
Use `mcpName = "*"` to create a rule that applies to **all** tools from **any**
registered MCP server. This is useful for setting category-wide defaults.
```toml
# Ask user for any tool call from any MCP server
[[rule]]
mcpName = "*"
decision = "ask_user"
priority = 10
```
**4. Targeting a tool name across all servers**
Use `mcpName = "*"` with a specific `toolName` to target that operation
regardless of which server provides it.
```toml
# Allow the `search` tool across all connected MCP servers
[[rule]]
mcpName = "*"
toolName = "search"
decision = "allow"
priority = 50
```
## Default policies
The Gemini CLI ships with a set of default policies to provide a safe
+1 -1
View File
@@ -104,7 +104,7 @@ The Gemini CLI configuration is stored in two `settings.json` files:
1. In your home directory: `~/.gemini/settings.json`.
2. In your project's root directory: `./.gemini/settings.json`.
Refer to [Gemini CLI Configuration](./get-started/configuration.md) for more
Refer to [Gemini CLI Configuration](../reference/configuration.md) for more
details.
## Google AI Pro/Ultra and subscription FAQs
@@ -135,6 +135,18 @@ Flow video editor). These plans do not apply to the API usage which powers the
Gemini CLI. Supporting these plans is under active consideration for future
support.
## Check usage and quota
You can check your current token usage and quota information using the
`/stats model` command. This command provides a snapshot of your current
session's token usage, as well as your overall quota and usage for the supported
models.
For more information on the `/stats` command and its subcommands, see the
[Command Reference](../../reference/commands.md#stats).
A summary of model usage is also presented on exit at the end of a session.
## Tips to avoid high costs
When using a Pay as you Go API key, be mindful of your usage to avoid unexpected
@@ -151,8 +163,3 @@ costs.
models directly.
- Vertex AI: This is the enterprise-grade platform for building, deploying, and
managing Gemini models with specific security and control requirements.
## Understanding your usage
A summary of model usage is available through the `/stats` command and presented
on exit at the end of a session.
@@ -10,8 +10,8 @@ and Privacy Notices applicable to those services apply to such access and use.
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
Policy.
**Note:** See [quotas and pricing](/docs/quota-and-pricing.md) for the quota and
pricing details that apply to your usage of the Gemini CLI.
**Note:** See [quotas and pricing](/docs/resources/quota-and-pricing.md) for the
quota and pricing details that apply to your usage of the Gemini CLI.
## Supported authentication methods
@@ -93,4 +93,4 @@ backend, these Terms of Service and Privacy Notice documents apply:
You may opt-out from sending Gemini CLI Usage Statistics to Google by following
the instructions available here:
[Usage Statistics Configuration](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md#usage-statistics).
[Usage Statistics Configuration](https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md#usage-statistics).
@@ -93,7 +93,7 @@ topics on:
- **Cause:** When sandboxing is enabled, Gemini CLI may attempt operations
that are restricted by your sandbox configuration, such as writing outside
the project directory or system temp directory.
- **Solution:** Refer to the [Configuration: Sandboxing](./cli/sandbox.md)
- **Solution:** Refer to the [Configuration: Sandboxing](../cli/sandbox.md)
documentation for more information, including how to customize your sandbox
configuration.
+200 -160
View File
@@ -1,193 +1,233 @@
[
{
"label": "Get started",
"items": [
{ "label": "Overview", "slug": "docs" },
{ "label": "Quickstart", "slug": "docs/get-started" },
{ "label": "Installation", "slug": "docs/get-started/installation" },
{ "label": "Authentication", "slug": "docs/get-started/authentication" },
{ "label": "Examples", "slug": "docs/get-started/examples" },
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
{ "label": "Gemini 3 on Gemini CLI", "slug": "docs/get-started/gemini-3" }
]
},
{
"label": "Use Gemini CLI",
"label": "docs_tab",
"items": [
{
"label": "File management",
"slug": "docs/cli/tutorials/file-management"
"label": "Get started",
"items": [
{ "label": "Overview", "slug": "docs" },
{ "label": "Quickstart", "slug": "docs/get-started" },
{ "label": "Installation", "slug": "docs/get-started/installation" },
{
"label": "Authentication",
"slug": "docs/get-started/authentication"
},
{ "label": "Examples", "slug": "docs/get-started/examples" },
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
{
"label": "Gemini 3 on Gemini CLI",
"slug": "docs/get-started/gemini-3"
}
]
},
{
"label": "Get started with Agent skills",
"slug": "docs/cli/tutorials/skills-getting-started"
"label": "Use Gemini CLI",
"items": [
{
"label": "File management",
"slug": "docs/cli/tutorials/file-management"
},
{
"label": "Get started with Agent skills",
"slug": "docs/cli/tutorials/skills-getting-started"
},
{
"label": "Manage context and memory",
"slug": "docs/cli/tutorials/memory-management"
},
{
"label": "Execute shell commands",
"slug": "docs/cli/tutorials/shell-commands"
},
{
"label": "Manage sessions and history",
"slug": "docs/cli/tutorials/session-management"
},
{
"label": "Plan tasks with todos",
"slug": "docs/cli/tutorials/task-planning"
},
{
"label": "Web search and fetch",
"slug": "docs/cli/tutorials/web-tools"
},
{
"label": "Set up an MCP server",
"slug": "docs/cli/tutorials/mcp-setup"
},
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
]
},
{
"label": "Manage context and memory",
"slug": "docs/cli/tutorials/memory-management"
"label": "Features",
"items": [
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
{
"label": "Authentication",
"slug": "docs/get-started/authentication"
},
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{
"label": "Extensions",
"slug": "docs/extensions/index"
},
{ "label": "Headless mode", "slug": "docs/cli/headless" },
{ "label": "Hooks", "slug": "docs/hooks" },
{ "label": "IDE integration", "slug": "docs/ide-integration" },
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{ "label": "Plan mode", "badge": "🧪", "slug": "docs/cli/plan-mode" },
{
"label": "Subagents",
"badge": "🧪",
"slug": "docs/core/subagents"
},
{
"label": "Remote subagents",
"badge": "🧪",
"slug": "docs/core/remote-agents"
},
{ "label": "Rewind", "slug": "docs/cli/rewind" },
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
{ "label": "Settings", "slug": "docs/cli/settings" },
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
{ "label": "Token caching", "slug": "docs/cli/token-caching" }
]
},
{
"label": "Execute shell commands",
"slug": "docs/cli/tutorials/shell-commands"
"label": "Configuration",
"items": [
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
{
"label": "Enterprise configuration",
"slug": "docs/cli/enterprise"
},
{
"label": "Ignore files (.geminiignore)",
"slug": "docs/cli/gemini-ignore"
},
{
"label": "Model configuration",
"slug": "docs/cli/generation-settings"
},
{
"label": "Project context (GEMINI.md)",
"slug": "docs/cli/gemini-md"
},
{ "label": "Settings", "slug": "docs/cli/settings" },
{
"label": "System prompt override",
"slug": "docs/cli/system-prompt"
},
{ "label": "Themes", "slug": "docs/cli/themes" },
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
]
},
{
"label": "Manage sessions and history",
"slug": "docs/cli/tutorials/session-management"
},
{
"label": "Plan tasks with todos",
"slug": "docs/cli/tutorials/task-planning"
},
{
"label": "Web search and fetch",
"slug": "docs/cli/tutorials/web-tools"
},
{
"label": "Set up an MCP server",
"slug": "docs/cli/tutorials/mcp-setup"
},
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
]
},
{
"label": "Features",
"items": [
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
{
"label": "Authentication",
"slug": "docs/get-started/authentication"
},
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{
"label": "Extensions",
"slug": "docs/extensions/index"
},
{ "label": "Headless mode", "slug": "docs/cli/headless" },
{ "label": "Help", "link": "/docs/cli/commands/#help-or" },
{ "label": "Hooks", "slug": "docs/hooks" },
{ "label": "IDE integration", "slug": "docs/ide-integration" },
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{
"label": "Memory management",
"link": "/docs/cli/commands/#memory"
},
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{ "label": "Plan mode", "badge": "🧪", "slug": "docs/cli/plan-mode" },
{ "label": "Subagents", "badge": "🧪", "slug": "docs/core/subagents" },
{
"label": "Remote subagents",
"badge": "🧪",
"slug": "docs/core/remote-agents"
},
{ "label": "Rewind", "slug": "docs/cli/rewind" },
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
{ "label": "Settings", "slug": "docs/cli/settings" },
{
"label": "Shell",
"link": "/docs/cli/commands/#shells-or-bashes"
"items": [
{
"label": "Overview",
"slug": "docs/extensions"
},
{
"label": "User guide: Install and manage",
"link": "/docs/extensions/#manage-extensions"
},
{
"label": "Developer guide: Build extensions",
"slug": "docs/extensions/writing-extensions"
},
{
"label": "Developer guide: Best practices",
"slug": "docs/extensions/best-practices"
},
{
"label": "Developer guide: Releasing",
"slug": "docs/extensions/releasing"
},
{
"label": "Developer guide: Reference",
"slug": "docs/extensions/reference"
}
]
},
{
"label": "Stats",
"link": "/docs/cli/commands/#stats"
},
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
{ "label": "Token caching", "slug": "docs/cli/token-caching" },
{ "label": "Tools", "link": "/docs/cli/commands/#tools" }
]
},
{
"label": "Configuration",
"items": [
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
{ "label": "Enterprise configuration", "slug": "docs/cli/enterprise" },
{
"label": "Ignore files (.geminiignore)",
"slug": "docs/cli/gemini-ignore"
},
{
"label": "Model configuration",
"slug": "docs/cli/generation-settings"
},
{ "label": "Project context (GEMINI.md)", "slug": "docs/cli/gemini-md" },
{ "label": "Settings", "slug": "docs/cli/settings" },
{ "label": "System prompt override", "slug": "docs/cli/system-prompt" },
{ "label": "Themes", "slug": "docs/cli/themes" },
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
]
},
{
"label": "Extensions",
"items": [
{
"label": "Overview",
"slug": "docs/extensions"
},
{
"label": "User guide: Install and manage",
"link": "/docs/extensions/#manage-extensions"
},
{
"label": "Developer guide: Build extensions",
"slug": "docs/extensions/writing-extensions"
},
{
"label": "Developer guide: Best practices",
"slug": "docs/extensions/best-practices"
},
{
"label": "Developer guide: Releasing",
"slug": "docs/extensions/releasing"
},
{
"label": "Developer guide: Reference",
"slug": "docs/extensions/reference"
"label": "Development",
"items": [
{ "label": "Contribution guide", "slug": "docs/contributing" },
{ "label": "Integration testing", "slug": "docs/integration-tests" },
{
"label": "Issue and PR automation",
"slug": "docs/issue-and-pr-automation"
},
{ "label": "Local development", "slug": "docs/local-development" },
{ "label": "NPM package structure", "slug": "docs/npm" }
]
}
]
},
{
"label": "Reference",
"label": "reference_tab",
"items": [
{ "label": "Command reference", "slug": "docs/cli/commands" },
{
"label": "Configuration reference",
"slug": "docs/get-started/configuration"
},
{ "label": "Keyboard shortcuts", "slug": "docs/cli/keyboard-shortcuts" },
{ "label": "Memory import processor", "slug": "docs/core/memport" },
{ "label": "Policy engine", "slug": "docs/core/policy-engine" },
{ "label": "Tools API", "slug": "docs/core/tools-api" }
"label": "Reference",
"items": [
{ "label": "Command reference", "slug": "docs/reference/commands" },
{
"label": "Configuration reference",
"slug": "docs/reference/configuration"
},
{
"label": "Keyboard shortcuts",
"slug": "docs/reference/keyboard-shortcuts"
},
{
"label": "Memory import processor",
"slug": "docs/reference/memport"
},
{ "label": "Policy engine", "slug": "docs/reference/policy-engine" },
{ "label": "Tools API", "slug": "docs/reference/tools-api" }
]
}
]
},
{
"label": "Resources",
"label": "resources_tab",
"items": [
{ "label": "FAQ", "slug": "docs/faq" },
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
]
},
{
"label": "Development",
"items": [
{ "label": "Contribution guide", "slug": "docs/contributing" },
{ "label": "Integration testing", "slug": "docs/integration-tests" },
{
"label": "Issue and PR automation",
"slug": "docs/issue-and-pr-automation"
},
{ "label": "Local development", "slug": "docs/local-development" },
{ "label": "NPM package structure", "slug": "docs/npm" }
"label": "Resources",
"items": [
{ "label": "FAQ", "slug": "docs/resources/faq" },
{
"label": "Quota and pricing",
"slug": "docs/resources/quota-and-pricing"
},
{
"label": "Terms and privacy",
"slug": "docs/resources/tos-privacy"
},
{
"label": "Troubleshooting",
"slug": "docs/resources/troubleshooting"
},
{ "label": "Uninstall", "slug": "docs/resources/uninstall" }
]
}
]
},
{
"label": "Releases",
"label": "releases_tab",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
{
"label": "Releases",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
]
}
]
}
]
+5 -2
View File
@@ -52,6 +52,9 @@ These tools help the model manage its plan and interact with you.
complex plans.
- **[Agent Skills](../cli/skills.md) (`activate_skill`):** Loads specialized
procedural expertise when needed.
- **[Browser agent](../core/subagents.md#browser-agent-experimental)
(`browser_agent`):** Automates web browser tasks through the accessibility
tree.
- **Internal docs (`get_internal_docs`):** Accesses Gemini CLI's own
documentation to help answer your questions.
@@ -98,5 +101,5 @@ Always review confirmation prompts carefully before allowing a tool to execute.
## Next steps
- Learn how to [Provide context](../cli/gemini-md.md) to guide tool use.
- Explore the [Command reference](../cli/commands.md) for tool-related slash
commands.
- Explore the [Command reference](../reference/commands.md) for tool-related
slash commands.
+5 -5
View File
@@ -14,8 +14,8 @@ provides direct access to the Markdown files in the `docs/` directory.
`get_internal_docs` takes one optional argument:
- `path` (string, optional): The relative path to a specific documentation file
(for example, `cli/commands.md`). If omitted, the tool returns a list of all
available documentation paths.
(for example, `reference/commands.md`). If omitted, the tool returns a list of
all available documentation paths.
## Usage
@@ -40,7 +40,7 @@ Gemini CLI uses this tool to ensure technical accuracy:
## Next steps
- Explore the [Command reference](../cli/commands.md) for a detailed guide to
slash commands.
- See the [Configuration guide](../get-started/configuration.md) for settings
- Explore the [Command reference](../reference/commands.md) for a detailed guide
to slash commands.
- See the [Configuration guide](../reference/configuration.md) for settings
reference.
+2
View File
@@ -11,6 +11,8 @@ by the agent when you ask it to "start a plan" using natural language. In this
mode, the agent is restricted to read-only tools to allow for safe exploration
and planning.
> **Note:** This tool is not available when the CLI is in YOLO mode.
- **Tool name:** `enter_plan_mode`
- **Display name:** Enter Plan Mode
- **File:** `enter-plan-mode.ts`
+3 -3
View File
@@ -131,9 +131,9 @@ configuration file.
commands. Including the generic `run_shell_command` acts as a wildcard,
allowing any command not explicitly blocked.
- `tools.exclude` [DEPRECATED]: To block specific commands, use the
[Policy Engine](../core/policy-engine.md). Historically, this setting allowed
adding entries to the `exclude` list under the `tools` category in the format
`run_shell_command(<command>)`. For example,
[Policy Engine](../reference/policy-engine.md). Historically, this setting
allowed adding entries to the `exclude` list under the `tools` category in the
format `run_shell_command(<command>)`. For example,
`"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.
The validation logic is designed to be secure and flexible:
+4 -1
View File
@@ -38,6 +38,7 @@ export default tseslint.config(
'dist/**',
'evals/**',
'packages/test-utils/**',
'.gemini/skills/**',
],
},
eslint.configs.recommended,
@@ -55,7 +56,7 @@ export default tseslint.config(
},
{
// Import specific config
files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
files: ['packages/*/src/**/*.{ts,tsx}'], // Target all TS/TSX in the packages
plugins: {
import: importPlugin,
},
@@ -199,6 +200,8 @@ export default tseslint.config(
ignores: ['**/*.test.ts', '**/*.test.tsx'],
rules: {
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
},
},
{
+170
View File
@@ -0,0 +1,170 @@
/**
* @license
* Copyright 202 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest, TestRig } from './test-helper.js';
import {
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
describe('grep_search_functionality', () => {
const TEST_PREFIX = 'Grep Search Functionality: ';
evalTest('USUALLY_PASSES', {
name: 'should find a simple string in a file',
files: {
'test.txt': `hello
world
hello world`,
},
prompt: 'Find "world" in test.txt',
assert: async (rig: TestRig, result: string) => {
await rig.waitForToolCall('grep_search');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/L2: world/, /L3: hello world/],
testName: `${TEST_PREFIX}simple search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should perform a case-sensitive search',
files: {
'test.txt': `Hello
hello`,
},
prompt: 'Find "Hello" in test.txt, case-sensitively.',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.case_sensitive === true;
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with case_sensitive: true',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/L1: Hello/],
forbiddenContent: [/L2: hello/],
testName: `${TEST_PREFIX}case-sensitive search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should return only file names when names_only is used',
files: {
'file1.txt': 'match me',
'file2.txt': 'match me',
},
prompt: 'Find the files containing "match me".',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.names_only === true;
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with names_only: true',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/file1.txt/, /file2.txt/],
forbiddenContent: [/L1:/],
testName: `${TEST_PREFIX}names_only search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should search only within the specified include glob',
files: {
'file.js': 'my_function();',
'file.ts': 'my_function();',
},
prompt: 'Find "my_function" in .js files.',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.include === '*.js';
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with include: "*.js"',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/file.js/],
forbiddenContent: [/file.ts/],
testName: `${TEST_PREFIX}include glob search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should search within a specific subdirectory',
files: {
'src/main.js': 'unique_string_1',
'lib/main.js': 'unique_string_2',
},
prompt: 'Find "unique_string" in the src directory.',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.dir_path === 'src';
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with dir_path: "src"',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/unique_string_1/],
forbiddenContent: [/unique_string_2/],
testName: `${TEST_PREFIX}subdirectory search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should report no matches correctly',
files: {
'file.txt': 'nothing to see here',
},
prompt: 'Find "nonexistent" in file.txt',
assert: async (rig: TestRig, result: string) => {
await rig.waitForToolCall('grep_search');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/No matches found/],
testName: `${TEST_PREFIX}no matches`,
});
},
});
});
+1 -1
View File
@@ -26,7 +26,7 @@ class MockClient implements acp.Client {
};
}
describe('ACP Environment and Auth', () => {
describe.skip('ACP Environment and Auth', () => {
let rig: TestRig;
let child: ChildProcess | undefined;
@@ -0,0 +1,12 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/1"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/2"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/3"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/4"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/5"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/6"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/7"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/8"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/9"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/10"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/11"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":500,"totalTokenCount":600}}]}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 1 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 2 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 3 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 4 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 5 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 6 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 7 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 8 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 9 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 10 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Some requests were rate limited: Rate limit exceeded for host. Please wait 60 seconds before trying again."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1000,"candidatesTokenCount":50,"totalTokenCount":1050}}]}
@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
describe('web-fetch rate limiting', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
if (rig) {
await rig.cleanup();
}
});
it('should rate limit multiple requests to the same host', async () => {
rig.setup('web-fetch rate limit', {
settings: { tools: { core: ['web_fetch'] } },
fakeResponsesPath: join(
import.meta.dirname,
'concurrency-limit.responses',
),
});
const result = await rig.run({
args: `Fetch 11 pages from example.com`,
});
// We expect to find at least one tool call that failed with a rate limit error.
const toolLogs = rig.readToolLogs();
const rateLimitedCalls = toolLogs.filter(
(log) =>
log.toolRequest.name === 'web_fetch' &&
log.toolRequest.error?.includes('Rate limit exceeded'),
);
expect(rateLimitedCalls.length).toBeGreaterThan(0);
expect(result).toContain('Rate limit exceeded');
});
});
@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file2.txt"}}},{"functionCall":{"name":"write_file","args":{"file_path":"output.txt","content":"wave2"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file3.txt"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file4.txt"}}}, {"text":"All waves completed successfully."}]},"finishReason":"STOP","index":0}]}]}
+77
View File
@@ -0,0 +1,77 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
import fs from 'node:fs';
describe('Parallel Tool Execution Integration', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should execute [read, read, write, read, read] in correct waves with user approval', async () => {
rig.setup('parallel-wave-execution', {
fakeResponsesPath: join(import.meta.dirname, 'parallel-tools.responses'),
settings: {
tools: {
core: ['read_file', 'write_file'],
approval: 'ASK', // Disable YOLO mode to show permission prompts
confirmationRequired: ['write_file'],
},
},
});
rig.createFile('file1.txt', 'c1');
rig.createFile('file2.txt', 'c2');
rig.createFile('file3.txt', 'c3');
rig.createFile('file4.txt', 'c4');
rig.sync();
const run = await rig.runInteractive({ approvalMode: 'default' });
// 1. Trigger the wave
await run.type('ok');
await run.type('\r');
// 3. Wait for the write_file prompt.
await run.expectText('Allow', 5000);
// 4. Press Enter to approve the write_file.
await run.type('y');
await run.type('\r');
// 5. Wait for the final model response
await run.expectText('All waves completed successfully.', 5000);
// Verify all tool calls were made and succeeded in the logs
await rig.expectToolCallSuccess(['write_file']);
const toolLogs = rig.readToolLogs();
const readFiles = toolLogs.filter(
(l) => l.toolRequest.name === 'read_file',
);
const writeFiles = toolLogs.filter(
(l) => l.toolRequest.name === 'write_file',
);
expect(readFiles.length).toBe(4);
expect(writeFiles.length).toBe(1);
expect(toolLogs.every((l) => l.toolRequest.success)).toBe(true);
// Check that output.txt was actually written
expect(fs.readFileSync(join(rig.testDir!, 'output.txt'), 'utf8')).toBe(
'wave2',
);
});
});
+288 -217
View File
@@ -1402,19 +1402,22 @@
"link": true
},
"node_modules/@google/genai": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz",
"integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==",
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.42.0.tgz",
"integrity": "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
"p-retry": "^4.6.2",
"protobufjs": "^7.5.4",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.20.1"
"@modelcontextprotocol/sdk": "^1.25.2"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
@@ -1422,10 +1425,18 @@
}
}
},
"node_modules/@google/genai/node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"dev": true,
"license": "MIT"
},
"node_modules/@google/genai/node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
@@ -1441,6 +1452,7 @@
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
@@ -1455,6 +1467,7 @@
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
@@ -1473,6 +1486,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=14"
@@ -1482,6 +1496,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
@@ -1495,6 +1510,7 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
@@ -1509,6 +1525,20 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/@google/genai/node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
@@ -2979,16 +3009,6 @@
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@pnpm/config.env-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
@@ -5133,46 +5153,6 @@
"win32"
]
},
"node_modules/@vscode/vsce/node_modules/glob": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/vsce/node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -5212,33 +5192,6 @@
"node": ">=4"
}
},
"node_modules/@vscode/vsce/node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/vsce/node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"dev": true,
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@vscode/vsce/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -5816,6 +5769,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@@ -10478,6 +10432,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-network-error": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz",
"integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-node-process": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
@@ -12908,6 +12874,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-retry": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
"license": "MIT",
"dependencies": {
"is-network-error": "^1.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-json": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
@@ -13447,9 +13428,9 @@
}
},
"node_modules/protobufjs": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -14164,81 +14145,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup": {
"version": "4.53.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
@@ -15706,43 +15612,6 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/test-exclude/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/test-exclude/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -15759,23 +15628,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/test-exclude/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -17310,7 +17162,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@google/genai": "1.41.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
"ansi-escapes": "^7.3.0",
@@ -17353,7 +17205,6 @@
"gemini": "dist/index.js"
},
"devDependencies": {
"@google/gemini-cli-devtools": "file:../devtools",
"@google/gemini-cli-test-utils": "file:../test-utils",
"@types/command-exists": "^1.2.3",
"@types/hast": "^3.0.4",
@@ -17371,6 +17222,29 @@
"node": ">=20"
}
},
"packages/cli/node_modules/@google/genai": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.41.0.tgz",
"integrity": "sha512-S4WGil+PG0NBQRAx+0yrQuM/TWOLn2gGEy5wn4IsoOI6ouHad0P61p3OWdhJ3aqr9kfj8o904i/jevfaGoGuIQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
"p-retry": "^7.1.1",
"protobufjs": "^7.5.4",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
}
},
"packages/cli/node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
@@ -17386,6 +17260,93 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/cli/node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.1"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/gcp-metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/google-auth-library": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^8.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/google-logging-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"packages/cli/node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"packages/cli/node_modules/string-width": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
@@ -17411,7 +17372,7 @@
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
"@google/genai": "1.30.0",
"@google/genai": "1.41.0",
"@iarna/toml": "^2.2.5",
"@joshua.litt/get-ripgrep": "^0.0.3",
"@modelcontextprotocol/sdk": "^1.23.0",
@@ -17443,7 +17404,7 @@
"fdir": "^6.4.6",
"fzf": "^0.5.2",
"glob": "^12.0.0",
"google-auth-library": "^9.11.0",
"google-auth-library": "^10.5.0",
"html-to-text": "^9.0.5",
"https-proxy-agent": "^7.0.6",
"ignore": "^7.0.0",
@@ -17489,6 +17450,29 @@
"node-pty": "^1.0.0"
}
},
"packages/core/node_modules/@google/genai": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.41.0.tgz",
"integrity": "sha512-S4WGil+PG0NBQRAx+0yrQuM/TWOLn2gGEy5wn4IsoOI6ouHad0P61p3OWdhJ3aqr9kfj8o904i/jevfaGoGuIQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
"p-retry": "^7.1.1",
"protobufjs": "^7.5.4",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
}
},
"packages/core/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
@@ -17519,6 +17503,75 @@
}
}
},
"packages/core/node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.1"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/gcp-metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/google-auth-library": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^8.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/google-logging-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"packages/core/node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -17543,6 +17596,24 @@
"node": ">=16"
}
},
"packages/core/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"packages/core/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+3 -1
View File
@@ -68,7 +68,9 @@
"wrap-ansi": "9.0.2",
"cliui": {
"wrap-ansi": "7.0.0"
}
},
"glob": "^12.0.0",
"node-domexception": "^1.0.0"
},
"bin": {
"gemini": "bundle/gemini.js"
+7 -5
View File
@@ -14,19 +14,21 @@ import {
type Mock,
} from 'vitest';
import { Task } from './task.js';
import type {
ToolCall,
Config,
ToolCallRequestInfo,
GitService,
CompletedToolCall,
} from '@google/gemini-cli-core';
import {
GeminiEventType,
type Config,
type ToolCallRequestInfo,
type GitService,
type CompletedToolCall,
ApprovalMode,
ToolConfirmationOutcome,
} from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
import { CoderAgentEvent } from '../types.js';
import type { ToolCall } from '@google/gemini-cli-core';
const mockProcessRestorableToolCalls = vi.hoisted(() => vi.fn());
+7 -6
View File
@@ -13,6 +13,7 @@ import {
getAllMCPServerStatuses,
MCPServerStatus,
isNodeError,
getErrorMessage,
parseAndFormatApiError,
safeLiteralReplace,
DEFAULT_GUI_EDITOR,
@@ -30,8 +31,7 @@ import {
EDIT_TOOL_NAMES,
processRestorableToolCalls,
} from '@google/gemini-cli-core';
import type { RequestContext } from '@a2a-js/sdk/server';
import { type ExecutionEventBus } from '@a2a-js/sdk/server';
import type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
import type {
TaskStatusUpdateEvent,
TaskArtifactUpdateEvent,
@@ -728,16 +728,17 @@ export class Task {
// Block scope for lexical declaration
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const errorEvent = event as ServerGeminiErrorEvent; // Type assertion
const errorMessage =
errorEvent.value?.error.message ?? 'Unknown error from LLM stream';
const errorMessage = errorEvent.value?.error
? getErrorMessage(errorEvent.value.error)
: 'Unknown error from LLM stream';
logger.error(
'[Task] Received error event from LLM stream:',
errorMessage,
);
let errMessage = `Unknown error from LLM stream: ${JSON.stringify(event)}`;
if (errorEvent.value) {
errMessage = parseAndFormatApiError(errorEvent.value);
if (errorEvent.value?.error) {
errMessage = parseAndFormatApiError(errorEvent.value.error);
}
this.cancelPendingTools(`LLM stream error: ${errorMessage}`);
this.setTaskStateAndPublishUpdate(
@@ -69,6 +69,7 @@ export class RestoreCommand implements Command {
throw error;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const toolCallData = JSON.parse(data);
const ToolCallDataSchema = getToolCallDataSchema();
const parseResult = ToolCallDataSchema.safeParse(toolCallData);
+5 -3
View File
@@ -8,17 +8,19 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import * as dotenv from 'dotenv';
import type { TelemetryTarget } from '@google/gemini-cli-core';
import type {
TelemetryTarget,
ConfigParameters,
ExtensionLoader,
} from '@google/gemini-cli-core';
import {
AuthType,
Config,
type ConfigParameters,
FileDiscoveryService,
ApprovalMode,
loadServerHierarchicalMemory,
GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
type ExtensionLoader,
startupProfiler,
PREVIEW_GEMINI_MODEL,
homedir,
+3 -2
View File
@@ -7,8 +7,8 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { MCPServerConfig } from '@google/gemini-cli-core';
import {
type MCPServerConfig,
debugLogger,
GEMINI_DIR,
getErrorMessage,
@@ -122,6 +122,7 @@ export function loadSettings(workspaceDir: string): Settings {
function resolveEnvVarsInString(value: string): string {
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
return value.replace(envVarRegex, (match, varName1, varName2) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const varName = varName1 || varName2;
if (process && process.env && typeof process.env[varName] === 'string') {
return process.env[varName];
@@ -146,7 +147,7 @@ function resolveEnvVarsInObject<T>(obj: T): T {
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-return
return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T;
}
+4 -5
View File
@@ -4,12 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '@google/gemini-cli-core';
import {
GeminiEventType,
ApprovalMode,
type ToolCallConfirmationDetails,
import type {
Config,
ToolCallConfirmationDetails,
} from '@google/gemini-cli-core';
import { GeminiEventType, ApprovalMode } from '@google/gemini-cli-core';
import type {
TaskStatusUpdateEvent,
SendStreamingMessageSuccessResponse,
+8 -3
View File
@@ -7,8 +7,8 @@
import express from 'express';
import type { AgentCard, Message } from '@a2a-js/sdk';
import type { TaskStore } from '@a2a-js/sdk/server';
import {
type TaskStore,
DefaultRequestHandler,
InMemoryTaskStore,
DefaultExecutionEventBus,
@@ -25,9 +25,12 @@ import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
import { loadSettings } from '../config/settings.js';
import { loadExtensions } from '../config/extension.js';
import { commandRegistry } from '../commands/command-registry.js';
import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
import {
debugLogger,
SimpleExtensionLoader,
GitService,
} from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js';
import { GitService } from '@google/gemini-cli-core';
type CommandResponse = {
name: string;
@@ -88,6 +91,7 @@ async function handleExecuteCommand(
},
) {
logger.info('[CoreAgent] Received /executeCommand request: ', req.body);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { command, args } = req.body;
try {
if (typeof command !== 'string') {
@@ -211,6 +215,7 @@ export async function createApp() {
const agentSettings = req.body.agentSettings as
| AgentSettings
| undefined;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const contextId = req.body.contextId || uuidv4();
const wrapper = await agentExecutor.createTask(
taskId,
@@ -246,6 +246,7 @@ export class GCSTaskStore implements TaskStore {
}
const [compressedMetadata] = await metadataFile.download();
const jsonData = gunzipSync(compressedMetadata).toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const loadedMetadata = JSON.parse(jsonData);
logger.info(`Task ${taskId} metadata loaded from GCS.`);
@@ -282,12 +283,14 @@ export class GCSTaskStore implements TaskStore {
return {
id: taskId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
contextId: loadedMetadata._contextId || uuidv4(),
kind: 'task',
status: {
state: persistedState._taskState,
timestamp: new Date().toISOString(),
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
metadata: loadedMetadata,
history: [],
artifacts: [],
+16 -1
View File
@@ -36,7 +36,21 @@ process.on('uncaughtException', (error) => {
});
main().catch(async (error) => {
await runExitCleanup();
// Set a timeout to force exit if cleanup hangs
const cleanupTimeout = setTimeout(() => {
writeToStderr('Cleanup timed out, forcing exit...\n');
process.exit(1);
}, 5000);
try {
await runExitCleanup();
} catch (cleanupError) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
}
if (error instanceof FatalError) {
let errorMessage = error.message;
@@ -46,6 +60,7 @@ main().catch(async (error) => {
writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
}
writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
+1 -2
View File
@@ -31,7 +31,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@google/genai": "1.41.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
"ansi-escapes": "^7.3.0",
@@ -71,7 +71,6 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@google/gemini-cli-devtools": "file:../devtools",
"@google/gemini-cli-test-utils": "file:../test-utils",
"@types/command-exists": "^1.2.3",
"@types/hast": "^3.0.4",
@@ -47,6 +47,7 @@ const defaultRequestConfirmation: RequestConfirmationCallback = async (
message,
initial: false,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.confirm;
};
+20 -1
View File
@@ -21,7 +21,11 @@ import {
type MCPServerConfig,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import { type Settings, createTestMergedSettings } from './settings.js';
import {
type Settings,
type MergedSettings,
createTestMergedSettings,
} from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -2599,6 +2603,21 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should pass planSettings.directory from settings to config', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
plan: {
directory: '.custom-plans',
},
},
} as unknown as MergedSettings);
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
const plansDir = config.storage.getPlansDir();
expect(plansDir).toContain('.custom-plans');
});
// --- Untrusted Folder Scenarios ---
describe('when folder is NOT trusted', () => {
beforeEach(() => {
+14 -2
View File
@@ -56,7 +56,10 @@ import { resolvePath } from '../utils/resolvePath.js';
import { RESUME_LATEST } from '../utils/sessionUtils.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { createPolicyEngineConfig } from './policy.js';
import {
createPolicyEngineConfig,
resolveWorkspacePolicyState,
} from './policy.js';
import { ExtensionManager } from './extension-manager.js';
import { McpServerEnablementManager } from './mcp/mcpServerEnablement.js';
import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
@@ -692,9 +695,17 @@ export async function loadCliConfig(
policyPaths: argv.policy,
};
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
await resolveWorkspacePolicyState({
cwd,
trustedFolder,
interactive,
});
const policyEngineConfig = await createPolicyEngineConfig(
effectiveSettings,
approvalMode,
workspacePoliciesDir,
);
policyEngineConfig.nonInteractive = !interactive;
@@ -758,6 +769,7 @@ export async function loadCliConfig(
coreTools: settings.tools?.core || undefined,
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
policyEngineConfig,
policyUpdateConfirmationRequest,
excludeTools,
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
@@ -814,6 +826,7 @@ export async function loadCliConfig(
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
plan: settings.experimental?.plan,
planSettings: settings.general.plan,
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
@@ -835,7 +848,6 @@ export async function loadCliConfig(
enableShellOutputEfficiency:
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
@@ -866,6 +866,7 @@ Would you like to attempt to install via "git clone" instead?`,
try {
const hooksContent = await fs.promises.readFile(hooksFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const rawHooks = JSON.parse(hooksContent);
if (
@@ -224,4 +224,59 @@ describe('ExtensionRegistryClient', () => {
'Failed to fetch extensions: Not Found',
);
});
it('should not return irrelevant results', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => [
...mockExtensions,
{
id: 'dataplex',
extensionName: 'dataplex',
extensionDescription: 'Connect to Dataplex Universal Catalog...',
fullName: 'google-cloud/dataplex',
rank: 6,
stars: 6,
url: '',
repoDescription: '',
lastUpdated: '',
extensionVersion: '1.0.0',
avatarUrl: '',
hasMCP: false,
hasContext: false,
isGoogleOwned: true,
licenseKey: '',
hasHooks: false,
hasCustomCommands: false,
hasSkills: false,
},
{
id: 'conductor',
extensionName: 'conductor',
extensionDescription: 'A conductor extension that actually matches.',
fullName: 'someone/conductor',
rank: 100,
stars: 100,
url: '',
repoDescription: '',
lastUpdated: '',
extensionVersion: '1.0.0',
avatarUrl: '',
hasMCP: false,
hasContext: false,
isGoogleOwned: false,
licenseKey: '',
hasHooks: false,
hasCustomCommands: false,
hasSkills: false,
},
],
});
const results = await client.searchExtensions('conductor');
const ids = results.map((r) => r.id);
expect(ids).not.toContain('dataplex');
expect(ids).toContain('conductor');
});
});
@@ -79,9 +79,11 @@ export class ExtensionRegistryClient {
const fzf = new AsyncFzf(allExtensions, {
selector: (ext: RegistryExtension) =>
`${ext.extensionName} ${ext.extensionDescription} ${ext.fullName}`,
fuzzy: 'v2',
fuzzy: true,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results = await fzf.find(query);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results.map((r: { item: RegistryExtension }) => r.item);
}
@@ -108,7 +110,6 @@ export class ExtensionRegistryClient {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return (await response.json()) as RegistryExtension[];
} catch (error) {
// Clear the promise on failure so that subsequent calls can try again
ExtensionRegistryClient.fetchPromise = null;
throw error;
}
@@ -179,6 +179,7 @@ export class ExtensionEnablementManager {
readConfig(): AllExtensionsEnablementConfig {
try {
const content = fs.readFileSync(this.configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(content);
} catch (error) {
if (
@@ -156,6 +156,7 @@ export async function promptForSetting(
name: 'value',
message: `${setting.name}\n${setting.description}`,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.value;
}
@@ -58,6 +58,7 @@ export function recursivelyHydrateStrings<T>(
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
recursivelyHydrateStrings(item, values),
) as unknown as T;
}
@@ -132,6 +132,35 @@ describe('Policy Engine Integration Tests', () => {
).toBe(PolicyDecision.ASK_USER);
});
it('should handle global MCP wildcard (*) in settings', async () => {
const settings: Settings = {
mcp: {
allowed: ['*'],
},
};
const config = await createPolicyEngineConfig(
settings,
ApprovalMode.DEFAULT,
);
const engine = new PolicyEngine(config);
// ANY tool with a server name should be allowed
expect(
(await engine.check({ name: 'mcp-server__tool' }, 'mcp-server'))
.decision,
).toBe(PolicyDecision.ALLOW);
expect(
(await engine.check({ name: 'another-server__tool' }, 'another-server'))
.decision,
).toBe(PolicyDecision.ALLOW);
// Built-in tools should NOT be allowed by the MCP wildcard
expect(
(await engine.check({ name: 'run_shell_command' }, undefined)).decision,
).toBe(PolicyDecision.ASK_USER);
});
it('should correctly prioritize specific tool excludes over MCP server wildcards', async () => {
const settings: Settings = {
mcp: {
@@ -148,13 +177,13 @@ describe('Policy Engine Integration Tests', () => {
);
const engine = new PolicyEngine(config);
// MCP server allowed (priority 2.1) provides general allow for server
// MCP server allowed (priority 2.1) provides general allow for server
// MCP server allowed (priority 3.1) provides general allow for server
// MCP server allowed (priority 3.1) provides general allow for server
expect(
(await engine.check({ name: 'my-server__safe-tool' }, undefined))
.decision,
).toBe(PolicyDecision.ALLOW);
// But specific tool exclude (priority 2.4) wins over server allow
// But specific tool exclude (priority 3.4) wins over server allow
expect(
(await engine.check({ name: 'my-server__dangerous-tool' }, undefined))
.decision,
@@ -339,6 +368,8 @@ describe('Policy Engine Integration Tests', () => {
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
'/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
'C:\\Users\\user\\.gemini\\tmp\\project-id\\session-id\\plans\\plan.md',
'D:\\gemini-cli\\.gemini\\tmp\\project-id\\session-1\\plans\\plan.md', // no session ID
];
for (const file_path of validPaths) {
@@ -364,7 +395,8 @@ describe('Policy Engine Integration Tests', () => {
const invalidPaths = [
'/project/src/file.ts', // Workspace
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal (Unix)
'C:\\Users\\user\\.gemini\\tmp\\id\\session\\plans\\..\\..\\..\\Windows\\System32\\config\\SAM', // Path traversal (Windows)
'/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir
];
@@ -412,25 +444,25 @@ describe('Policy Engine Integration Tests', () => {
// Find rules and verify their priorities
const blockedToolRule = rules.find((r) => r.toolName === 'blocked-tool');
expect(blockedToolRule?.priority).toBe(2.4); // Command line exclude
expect(blockedToolRule?.priority).toBe(3.4); // Command line exclude
const blockedServerRule = rules.find(
(r) => r.toolName === 'blocked-server__*',
);
expect(blockedServerRule?.priority).toBe(2.9); // MCP server exclude
expect(blockedServerRule?.priority).toBe(3.9); // MCP server exclude
const specificToolRule = rules.find(
(r) => r.toolName === 'specific-tool',
);
expect(specificToolRule?.priority).toBe(2.3); // Command line allow
expect(specificToolRule?.priority).toBe(3.3); // Command line allow
const trustedServerRule = rules.find(
(r) => r.toolName === 'trusted-server__*',
);
expect(trustedServerRule?.priority).toBe(2.2); // MCP trusted server
expect(trustedServerRule?.priority).toBe(3.2); // MCP trusted server
const mcpServerRule = rules.find((r) => r.toolName === 'mcp-server__*');
expect(mcpServerRule?.priority).toBe(2.1); // MCP allowed server
expect(mcpServerRule?.priority).toBe(3.1); // MCP allowed server
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
@@ -577,16 +609,16 @@ describe('Policy Engine Integration Tests', () => {
// Verify each rule has the expected priority
const tool3Rule = rules.find((r) => r.toolName === 'tool3');
expect(tool3Rule?.priority).toBe(2.4); // Excluded tools (user tier)
expect(tool3Rule?.priority).toBe(3.4); // Excluded tools (user tier)
const server2Rule = rules.find((r) => r.toolName === 'server2__*');
expect(server2Rule?.priority).toBe(2.9); // Excluded servers (user tier)
expect(server2Rule?.priority).toBe(3.9); // Excluded servers (user tier)
const tool1Rule = rules.find((r) => r.toolName === 'tool1');
expect(tool1Rule?.priority).toBe(2.3); // Allowed tools (user tier)
expect(tool1Rule?.priority).toBe(3.3); // Allowed tools (user tier)
const server1Rule = rules.find((r) => r.toolName === 'server1__*');
expect(server1Rule?.priority).toBe(2.1); // Allowed servers (user tier)
expect(server1Rule?.priority).toBe(3.1); // Allowed servers (user tier)
const globRule = rules.find((r) => r.toolName === 'glob');
// Priority 70 in default tier → 1.07
+145
View File
@@ -0,0 +1,145 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { resolveWorkspacePolicyState } from './policy.js';
import { writeToStderr } from '@google/gemini-cli-core';
// Mock debugLogger to avoid noise in test output
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
writeToStderr: vi.fn(),
};
});
describe('resolveWorkspacePolicyState', () => {
let tempDir: string;
let workspaceDir: string;
let policiesDir: string;
beforeEach(() => {
// Create a temporary directory for the test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
// Redirect GEMINI_CLI_HOME to the temp directory to isolate integrity storage
vi.stubEnv('GEMINI_CLI_HOME', tempDir);
workspaceDir = path.join(tempDir, 'workspace');
fs.mkdirSync(workspaceDir);
policiesDir = path.join(workspaceDir, '.gemini', 'policies');
vi.clearAllMocks();
});
afterEach(() => {
// Clean up temporary directory
fs.rmSync(tempDir, { recursive: true, force: true });
vi.unstubAllEnvs();
});
it('should return empty state if folder is not trusted', async () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: false,
interactive: true,
});
expect(result).toEqual({
workspacePoliciesDir: undefined,
policyUpdateConfirmationRequest: undefined,
});
});
it('should return policy directory if integrity matches', async () => {
// Set up policies directory with a file
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
// First call to establish integrity (interactive accept)
const firstResult = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined();
// Establish integrity manually as if accepted
const { PolicyIntegrityManager } = await import('@google/gemini-cli-core');
const integrityManager = new PolicyIntegrityManager();
await integrityManager.acceptIntegrity(
'workspace',
workspaceDir,
firstResult.policyUpdateConfirmationRequest!.newHash,
);
// Second call should match
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
});
it('should return undefined if integrity is NEW but fileCount is 0', async () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
});
it('should return confirmation request if changed in interactive mode', async () => {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toEqual({
scope: 'workspace',
identifier: workspaceDir,
policyDir: policiesDir,
newHash: expect.any(String),
});
});
it('should warn and auto-accept if changed in non-interactive mode', async () => {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: false,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
expect(writeToStderr).toHaveBeenCalledWith(
expect.stringContaining('Automatically accepting and loading'),
);
});
});
+74 -1
View File
@@ -12,12 +12,18 @@ import {
type PolicySettings,
createPolicyEngineConfig as createCorePolicyEngineConfig,
createPolicyUpdater as createCorePolicyUpdater,
PolicyIntegrityManager,
IntegrityStatus,
Storage,
type PolicyUpdateConfirmationRequest,
writeToStderr,
} from '@google/gemini-cli-core';
import { type Settings } from './settings.js';
export async function createPolicyEngineConfig(
settings: Settings,
approvalMode: ApprovalMode,
workspacePoliciesDir?: string,
): Promise<PolicyEngineConfig> {
// Explicitly construct PolicySettings from Settings to ensure type safety
// and avoid accidental leakage of other settings properties.
@@ -26,6 +32,7 @@ export async function createPolicyEngineConfig(
tools: settings.tools,
mcpServers: settings.mcpServers,
policyPaths: settings.policyPaths,
workspacePoliciesDir,
};
return createCorePolicyEngineConfig(policySettings, approvalMode);
@@ -34,6 +41,72 @@ export async function createPolicyEngineConfig(
export function createPolicyUpdater(
policyEngine: PolicyEngine,
messageBus: MessageBus,
storage: Storage,
) {
return createCorePolicyUpdater(policyEngine, messageBus);
return createCorePolicyUpdater(policyEngine, messageBus, storage);
}
export interface WorkspacePolicyState {
workspacePoliciesDir?: string;
policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest;
}
/**
* Resolves the workspace policy state by checking folder trust and policy integrity.
*/
export async function resolveWorkspacePolicyState(options: {
cwd: string;
trustedFolder: boolean;
interactive: boolean;
}): Promise<WorkspacePolicyState> {
const { cwd, trustedFolder, interactive } = options;
let workspacePoliciesDir: string | undefined;
let policyUpdateConfirmationRequest:
| PolicyUpdateConfirmationRequest
| undefined;
if (trustedFolder) {
const potentialWorkspacePoliciesDir = new Storage(
cwd,
).getWorkspacePoliciesDir();
const integrityManager = new PolicyIntegrityManager();
const integrityResult = await integrityManager.checkIntegrity(
'workspace',
cwd,
potentialWorkspacePoliciesDir,
);
if (integrityResult.status === IntegrityStatus.MATCH) {
workspacePoliciesDir = potentialWorkspacePoliciesDir;
} else if (
integrityResult.status === IntegrityStatus.NEW &&
integrityResult.fileCount === 0
) {
// No workspace policies found
workspacePoliciesDir = undefined;
} else if (interactive) {
// Policies changed or are new, and we are in interactive mode
policyUpdateConfirmationRequest = {
scope: 'workspace',
identifier: cwd,
policyDir: potentialWorkspacePoliciesDir,
newHash: integrityResult.hash,
};
} else {
// Non-interactive mode: warn and automatically accept/load
await integrityManager.acceptIntegrity(
'workspace',
cwd,
integrityResult.hash,
);
workspacePoliciesDir = potentialWorkspacePoliciesDir;
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
writeToStderr(
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n',
);
}
}
return { workspacePoliciesDir, policyUpdateConfirmationRequest };
}
+79
View File
@@ -2032,6 +2032,85 @@ describe('Settings Loading and Merging', () => {
}),
}),
);
// Check that enableLoadingPhrases: false was further migrated to loadingPhrases: 'off'
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
loadingPhrases: 'off',
}),
);
});
it('should migrate enableLoadingPhrases: false to loadingPhrases: off', () => {
const userSettingsContent = {
ui: {
accessibility: {
enableLoadingPhrases: false,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings);
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
loadingPhrases: 'off',
}),
);
});
it('should not migrate enableLoadingPhrases: true to loadingPhrases', () => {
const userSettingsContent = {
ui: {
accessibility: {
enableLoadingPhrases: true,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings);
// Should not set loadingPhrases when enableLoadingPhrases is true
const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui');
for (const call of uiCalls) {
const uiValue = call[2] as Record<string, unknown>;
expect(uiValue).not.toHaveProperty('loadingPhrases');
}
});
it('should not overwrite existing loadingPhrases during migration', () => {
const userSettingsContent = {
ui: {
loadingPhrases: 'witty',
accessibility: {
enableLoadingPhrases: false,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings);
// Should not overwrite existing loadingPhrases
const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui');
for (const call of uiCalls) {
const uiValue = call[2] as Record<string, unknown>;
if (uiValue['loadingPhrases'] !== undefined) {
expect(uiValue['loadingPhrases']).toBe('witty');
}
}
});
it('should prioritize new settings over deprecated ones and respect removeDeprecated flag', () => {
+19
View File
@@ -165,7 +165,10 @@ export interface SummarizeToolOutputSettings {
tokenBudget?: number;
}
export type LoadingPhrasesMode = 'tips' | 'witty' | 'all' | 'off';
export interface AccessibilitySettings {
/** @deprecated Use ui.loadingPhrases instead. */
enableLoadingPhrases?: boolean;
screenReader?: boolean;
}
@@ -928,6 +931,22 @@ export function migrateDeprecatedSettings(
anyModified = true;
}
}
// Migrate enableLoadingPhrases: false → loadingPhrases: 'off'
const enableLP = newAccessibility['enableLoadingPhrases'];
if (
typeof enableLP === 'boolean' &&
newUi['loadingPhrases'] === undefined
) {
if (!enableLP) {
newUi['loadingPhrases'] = 'off';
loadedSettings.setValue(scope, 'ui', newUi);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
foundDeprecated.push('ui.accessibility.enableLoadingPhrases');
}
}
}
@@ -83,6 +83,19 @@ describe('SettingsSchema', () => {
).toBe('boolean');
});
it('should have loadingPhrases enum property', () => {
const definition = getSettingsSchema().ui?.properties?.loadingPhrases;
expect(definition).toBeDefined();
expect(definition?.type).toBe('enum');
expect(definition?.default).toBe('tips');
expect(definition?.options?.map((o) => o.value)).toEqual([
'tips',
'witty',
'all',
'off',
]);
});
it('should have checkpointing nested properties', () => {
expect(
getSettingsSchema().general?.properties?.checkpointing.properties
@@ -94,6 +107,16 @@ describe('SettingsSchema', () => {
).toBe('boolean');
});
it('should have plan nested properties', () => {
expect(
getSettingsSchema().general?.properties?.plan?.properties?.directory,
).toBeDefined();
expect(
getSettingsSchema().general?.properties?.plan?.properties?.directory
.type,
).toBe('string');
});
it('should have fileFiltering nested properties', () => {
expect(
getSettingsSchema().context.properties.fileFiltering.properties
+103 -10
View File
@@ -266,15 +266,26 @@ const SETTINGS_SCHEMA = {
},
},
},
enablePromptCompletion: {
type: 'boolean',
label: 'Enable Prompt Completion',
plan: {
type: 'object',
label: 'Plan',
category: 'General',
requiresRestart: true,
default: false,
description:
'Enable AI-powered prompt completion suggestions while typing.',
showInDialog: true,
default: {},
description: 'Planning features configuration.',
showInDialog: false,
properties: {
directory: {
type: 'string',
label: 'Plan Directory',
category: 'General',
requiresRestart: true,
default: undefined as string | undefined,
description:
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
showInDialog: true,
},
},
},
retryFetchErrors: {
type: 'boolean',
@@ -672,6 +683,22 @@ const SETTINGS_SCHEMA = {
description: 'Show the spinner during operations.',
showInDialog: true,
},
loadingPhrases: {
type: 'enum',
label: 'Loading Phrases',
category: 'UI',
requiresRestart: false,
default: 'tips',
description:
'What to show while the model is working: tips, witty comments, both, or nothing.',
showInDialog: true,
options: [
{ value: 'tips', label: 'Tips' },
{ value: 'witty', label: 'Witty' },
{ value: 'all', label: 'All' },
{ value: 'off', label: 'Off' },
],
},
customWittyPhrases: {
type: 'array',
label: 'Custom Witty Phrases',
@@ -700,8 +727,9 @@ const SETTINGS_SCHEMA = {
category: 'UI',
requiresRestart: true,
default: true,
description: 'Enable loading phrases during operations.',
showInDialog: true,
description:
'@deprecated Use ui.loadingPhrases instead. Enable loading phrases during operations.',
showInDialog: false,
},
screenReader: {
type: 'boolean',
@@ -936,6 +964,60 @@ const SETTINGS_SCHEMA = {
ref: 'AgentOverride',
},
},
browser: {
type: 'object',
label: 'Browser Agent',
category: 'Advanced',
requiresRestart: true,
default: {},
description: 'Settings specific to the browser agent.',
showInDialog: false,
properties: {
sessionMode: {
type: 'enum',
label: 'Browser Session Mode',
category: 'Advanced',
requiresRestart: true,
default: 'persistent',
description:
"Session mode: 'persistent', 'isolated', or 'existing'.",
showInDialog: false,
options: [
{ value: 'persistent', label: 'Persistent' },
{ value: 'isolated', label: 'Isolated' },
{ value: 'existing', label: 'Existing' },
],
},
headless: {
type: 'boolean',
label: 'Browser Headless',
category: 'Advanced',
requiresRestart: true,
default: false,
description: 'Run browser in headless mode.',
showInDialog: false,
},
profilePath: {
type: 'string',
label: 'Browser Profile Path',
category: 'Advanced',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Path to browser profile directory for session persistence.',
showInDialog: false,
},
visualModel: {
type: 'string',
label: 'Browser Visual Model',
category: 'Advanced',
requiresRestart: true,
default: undefined as string | undefined,
description: 'Model override for the visual agent.',
showInDialog: false,
},
},
},
},
},
@@ -1296,6 +1378,7 @@ const SETTINGS_SCHEMA = {
},
},
},
useWriteTodos: {
type: 'boolean',
label: 'Use WriteTodos',
@@ -1632,7 +1715,17 @@ const SETTINGS_SCHEMA = {
requiresRestart: false,
default: false,
description:
'Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).',
'Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
showInDialog: true,
},
useOSC52Copy: {
type: 'boolean',
label: 'Use OSC 52 Copy',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
showInDialog: true,
},
plan: {
@@ -131,7 +131,6 @@ describe('Settings Repro', () => {
},
general: {
debugKeystrokeLogging: false,
enablePromptCompletion: false,
preferredEditor: 'vim',
vimMode: false,
},
@@ -0,0 +1,239 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as path from 'node:path';
import { loadCliConfig, type CliArgs } from './config.js';
import { createTestMergedSettings } from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
// Mock dependencies
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi.fn(),
}));
const mockCheckIntegrity = vi.fn();
const mockAcceptIntegrity = vi.fn();
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual<typeof ServerConfig>(
'@google/gemini-cli-core',
);
return {
...actual,
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
}),
createPolicyEngineConfig: vi.fn().mockResolvedValue({
rules: [],
checkers: [],
}),
getVersion: vi.fn().mockResolvedValue('test-version'),
PolicyIntegrityManager: vi.fn().mockImplementation(() => ({
checkIntegrity: mockCheckIntegrity,
acceptIntegrity: mockAcceptIntegrity,
})),
IntegrityStatus: { MATCH: 'match', NEW: 'new', MISMATCH: 'mismatch' },
debugLogger: {
warn: vi.fn(),
error: vi.fn(),
},
isHeadlessMode: vi.fn().mockReturnValue(false), // Default to interactive
};
});
describe('Workspace-Level Policy CLI Integration', () => {
const MOCK_CWD = process.cwd();
beforeEach(() => {
vi.clearAllMocks();
// Default to MATCH for existing tests
mockCheckIntegrity.mockResolvedValue({
status: 'match',
hash: 'test-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
});
it('should have getWorkspacePoliciesDir on Storage class', () => {
const storage = new ServerConfig.Storage(MOCK_CWD);
expect(storage.getWorkspacePoliciesDir).toBeDefined();
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
});
it('should pass workspacePoliciesDir to createPolicyEngineConfig when folder is trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should NOT pass workspacePoliciesDir to createPolicyEngineConfig when folder is NOT trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: 'file',
});
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should NOT pass workspacePoliciesDir if integrity is NEW but fileCount is 0', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'new',
hash: 'hash',
fileCount: 0,
});
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in non-interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'mismatch',
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(true); // Non-interactive
const settings = createTestMergedSettings();
const argv = { prompt: 'do something' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
'workspace',
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'mismatch',
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
query: 'test',
promptInteractive: 'test',
} as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
// In interactive mode without accept flag, it waits for user confirmation (handled by UI),
// so it currently DOES NOT pass the directory to createPolicyEngineConfig yet.
// The UI will handle the confirmation and reload/update.
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'new',
hash: 'new-hash',
fileCount: 5,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
});
+6 -2
View File
@@ -38,6 +38,8 @@ import { appEvents, AppEvent } from './utils/events.js';
import {
type Config,
type ResumedSessionData,
type StartupWarning,
WarningPriority,
debugLogger,
coreEvents,
AuthType,
@@ -1193,7 +1195,9 @@ describe('startInteractiveUI', () => {
},
},
} as LoadedSettings;
const mockStartupWarnings = ['warning1'];
const mockStartupWarnings: StartupWarning[] = [
{ id: 'w1', message: 'warning1', priority: WarningPriority.High },
];
const mockWorkspaceRoot = '/root';
const mockInitializationResult = {
authError: null,
@@ -1226,7 +1230,7 @@ describe('startInteractiveUI', () => {
async function startTestInteractiveUI(
config: Config,
settings: LoadedSettings,
startupWarnings: string[],
startupWarnings: StartupWarning[],
workspaceRoot: string,
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
+33 -16
View File
@@ -11,6 +11,7 @@ import { loadCliConfig, parseArguments } from './config/config.js';
import * as cliConfig from './config/config.js';
import { readStdin } from './utils/readStdin.js';
import { basename } from 'node:path';
import { createHash } from 'node:crypto';
import v8 from 'node:v8';
import os from 'node:os';
import dns from 'node:dns';
@@ -37,6 +38,8 @@ import {
cleanupExpiredSessions,
} from './utils/sessionCleanup.js';
import {
type StartupWarning,
WarningPriority,
type Config,
type ResumedSessionData,
type OutputPayload,
@@ -99,6 +102,7 @@ import { createPolicyUpdater } from './config/policy.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { profiler } from './ui/components/DebugProfiler.js';
@@ -180,7 +184,7 @@ ${reason.stack}`
export async function startInteractiveUI(
config: Config,
settings: LoadedSettings,
startupWarnings: string[],
startupWarnings: StartupWarning[],
workspaceRoot: string = process.cwd(),
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
@@ -235,17 +239,19 @@ export async function startInteractiveUI(
>
<TerminalProvider>
<ScrollProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
initializationResult={initializationResult}
/>
</VimModeProvider>
</SessionStatsProvider>
<OverflowProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
initializationResult={initializationResult}
/>
</VimModeProvider>
</SessionStatsProvider>
</OverflowProvider>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
@@ -580,7 +586,7 @@ export async function main() {
const policyEngine = config.getPolicyEngine();
const messageBus = config.getMessageBus();
createPolicyUpdater(policyEngine, messageBus);
createPolicyUpdater(policyEngine, messageBus, config.storage);
// Register SessionEnd hook to fire on graceful exit
// This runs before telemetry shutdown in runExitCleanup()
@@ -668,9 +674,20 @@ export async function main() {
}
let input = config.getQuestion();
const startupWarnings = [
...(await getStartupWarnings()),
...(await getUserStartupWarnings(settings.merged)),
const useAlternateBuffer = shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings),
config.getScreenReader(),
);
const rawStartupWarnings = await getStartupWarnings();
const startupWarnings: StartupWarning[] = [
...rawStartupWarnings.map((message) => ({
id: `startup-${createHash('sha256').update(message).digest('hex').substring(0, 16)}`,
message,
priority: WarningPriority.High,
})),
...(await getUserStartupWarnings(settings.merged, undefined, {
isAlternateBuffer: useAlternateBuffer,
})),
];
// Handle --resume flag
+3 -3
View File
@@ -13,6 +13,7 @@ import type {
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
convertSessionToClientHistory,
GeminiEventType,
FatalInputError,
promptIdContext,
@@ -35,7 +36,6 @@ import type { Content, Part } from '@google/genai';
import readline from 'node:readline';
import stripAnsi from 'strip-ansi';
import { convertSessionToHistoryFormats } from './ui/hooks/useSessionBrowser.js';
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
@@ -220,9 +220,9 @@ export async function runNonInteractive({
// Initialize chat. Resume if resume data is passed.
if (resumedSessionData) {
await geminiClient.resumeChat(
convertSessionToHistoryFormats(
convertSessionToClientHistory(
resumedSessionData.conversation.messages,
).clientHistory,
),
resumedSessionData,
);
}
+2 -1
View File
@@ -217,13 +217,14 @@ export class AppRig {
}
private stubRefreshAuth() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const gcConfig = this.config as any;
gcConfig.refreshAuth = async (authMethod: AuthType) => {
gcConfig.modelAvailabilityService.reset();
const newContentGeneratorConfig = {
authType: authMethod,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
proxy: gcConfig.getProxy(),
apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
};
@@ -21,7 +21,7 @@ import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
const invalidCharsRegex = /[\b\x1b]/;
function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
const { isNot } = this as any;
let pass = true;
const invalidLines: Array<{ line: number; content: string }> = [];
@@ -45,7 +45,7 @@ export const createMockCommandContext = (
forScope: vi.fn().mockReturnValue({ settings: {} }),
} as unknown as LoadedSettings,
git: undefined as GitService | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
logger: {
log: vi.fn(),
logMessage: vi.fn(),
@@ -54,7 +54,7 @@ export const createMockCommandContext = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any, // Cast because Logger is a class.
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
ui: {
addItem: vi.fn(),
clear: vi.fn(),
@@ -94,11 +94,14 @@ export const createMockCommandContext = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const merge = (target: any, source: any): any => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const output = { ...target };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const sourceValue = source[key];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const targetValue = output[key];
if (
@@ -106,9 +109,11 @@ export const createMockCommandContext = (
Object.prototype.toString.call(sourceValue) === '[object Object]' &&
Object.prototype.toString.call(targetValue) === '[object Object]'
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
output[key] = merge(targetValue, sourceValue);
} else {
// If not, we do a direct assignment. This preserves Date objects and others.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
output[key] = sourceValue;
}
}
@@ -116,5 +121,6 @@ export const createMockCommandContext = (
return output;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return merge(defaultMocks, overrides);
};
@@ -128,7 +128,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getShellToolInactivityTimeout: vi.fn().mockReturnValue(300000),
getShellExecutionConfig: vi.fn().mockReturnValue({}),
setShellExecutionConfig: vi.fn(),
getEnablePromptCompletion: vi.fn().mockReturnValue(false),
getEnableToolOutputTruncation: vi.fn().mockReturnValue(true),
getTruncateToolOutputThreshold: vi.fn().mockReturnValue(1000),
getTruncateToolOutputLines: vi.fn().mockReturnValue(100),
+105 -58
View File
@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render as inkRenderDirect, type Instance as InkInstance } from 'ink';
import {
render as inkRenderDirect,
type Instance as InkInstance,
type RenderOptions,
} from 'ink';
import { EventEmitter } from 'node:events';
import { Box } from 'ink';
import type React from 'react';
@@ -31,6 +35,13 @@ import { type HistoryItemToolGroup, StreamingState } from '../ui/types.js';
import { ToolActionsProvider } from '../ui/contexts/ToolActionsContext.js';
import { AskUserActionsProvider } from '../ui/contexts/AskUserActionsContext.js';
import { TerminalProvider } from '../ui/contexts/TerminalContext.js';
import {
OverflowProvider,
useOverflowActions,
useOverflowState,
type OverflowActions,
type OverflowState,
} from '../ui/contexts/OverflowContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
@@ -68,6 +79,25 @@ type TerminalState = {
rows: number;
};
type RenderMetrics = Parameters<NonNullable<RenderOptions['onRender']>>[0];
interface InkRenderMetrics extends RenderMetrics {
output: string;
staticOutput?: string;
}
function isInkRenderMetrics(
metrics: RenderMetrics,
): metrics is InkRenderMetrics {
const m = metrics as Record<string, unknown>;
return (
typeof m === 'object' &&
m !== null &&
'output' in m &&
typeof m['output'] === 'string'
);
}
class XtermStdout extends EventEmitter {
private state: TerminalState;
private pendingWrites = 0;
@@ -76,7 +106,6 @@ class XtermStdout extends EventEmitter {
isTTY = true;
private lastRenderOutput: string | undefined = undefined;
private lastRenderStaticContent: string | undefined = undefined;
constructor(state: TerminalState, queue: { promise: Promise<void> }) {
super();
@@ -109,7 +138,6 @@ class XtermStdout extends EventEmitter {
clear = () => {
this.state.terminal.reset();
this.lastRenderOutput = undefined;
this.lastRenderStaticContent = undefined;
};
dispose = () => {
@@ -118,33 +146,23 @@ class XtermStdout extends EventEmitter {
onRender = (staticContent: string, output: string) => {
this.renderCount++;
this.lastRenderStaticContent = staticContent;
this.lastRenderOutput = output;
this.emit('render');
};
lastFrame = (options: { allowEmpty?: boolean } = {}) => {
let result: string;
// On Windows, xterm.js headless can sometimes have timing or rendering issues
// that lead to duplicated content or incorrect buffer state in tests.
// As a fallback, we can trust the raw output Ink provided during onRender.
if (os.platform() === 'win32') {
result =
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? '');
} else {
const buffer = this.state.terminal.buffer.active;
const allLines: string[] = [];
for (let i = 0; i < buffer.length; i++) {
allLines.push(buffer.getLine(i)?.translateToString(true) ?? '');
}
const trimmed = [...allLines];
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
trimmed.pop();
}
result = trimmed.join('\n');
const buffer = this.state.terminal.buffer.active;
const allLines: string[] = [];
for (let i = 0; i < buffer.length; i++) {
allLines.push(buffer.getLine(i)?.translateToString(true) ?? '');
}
const trimmed = [...allLines];
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
trimmed.pop();
}
const result = trimmed.join('\n');
// Normalize for cross-platform snapshot stability:
// Normalize any \r\n to \n
const normalized = result.replace(/\r\n/g, '\n');
@@ -195,9 +213,7 @@ class XtermStdout extends EventEmitter {
const currentFrame = stripAnsi(
this.lastFrame({ allowEmpty: true }),
).trim();
const expectedFrame = stripAnsi(
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''),
)
const expectedFrame = stripAnsi(this.lastRenderOutput ?? '')
.trim()
.replace(/\r\n/g, '\n');
@@ -326,6 +342,8 @@ export type RenderInstance = {
lastFrame: (options?: { allowEmpty?: boolean }) => string;
terminal: Terminal;
waitUntilReady: () => Promise<void>;
capturedOverflowState: OverflowState | undefined;
capturedOverflowActions: OverflowActions | undefined;
};
const instances: InkInstance[] = [];
@@ -334,9 +352,16 @@ const instances: InkInstance[] = [];
export const render = (
tree: React.ReactElement,
terminalWidth?: number,
): RenderInstance => {
): Omit<
RenderInstance,
'capturedOverflowState' | 'capturedOverflowActions'
> => {
const cols = terminalWidth ?? 100;
const rows = 40;
// We use 1000 rows to avoid windows with incorrect snapshots if a correct
// value was used (e.g. 40 rows). The alternatives to make things worse are
// windows unfortunately with odd duplicate content in the backbuffer
// which does not match actual behavior in xterm.js on windows.
const rows = 1000;
const terminal = new Terminal({
cols,
rows,
@@ -367,8 +392,10 @@ export const render = (
debug: false,
exitOnCtrlC: false,
patchConsole: false,
onRender: (metrics: { output: string; staticOutput?: string }) => {
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
onRender: (metrics: RenderMetrics) => {
if (isInkRenderMetrics(metrics)) {
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
}
},
});
});
@@ -516,6 +543,7 @@ const mockUIActions: UIActions = {
vimHandleInput: vi.fn(),
handleIdePromptComplete: vi.fn(),
handleFolderTrustSelect: vi.fn(),
setIsPolicyUpdateDialogOpen: vi.fn(),
setConstrainHeight: vi.fn(),
onEscapePromptChange: vi.fn(),
refreshStatic: vi.fn(),
@@ -546,6 +574,16 @@ const mockUIActions: UIActions = {
handleNewAgentsSelect: vi.fn(),
};
let capturedOverflowState: OverflowState | undefined;
let capturedOverflowActions: OverflowActions | undefined;
const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
capturedOverflowState = useOverflowState();
capturedOverflowActions = useOverflowActions();
return <>{children}</>;
};
export const renderWithProviders = (
component: React.ReactElement,
{
@@ -647,6 +685,9 @@ export const renderWithProviders = (
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
.flatMap((item) => item.tools);
capturedOverflowState = undefined;
capturedOverflowActions = undefined;
const renderResult = render(
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={config}>
@@ -659,35 +700,39 @@ export const renderWithProviders = (
value={finalUiState.streamingState}
>
<UIActionsContext.Provider value={finalUIActions}>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
<OverflowProvider>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<TerminalProvider>
<ScrollProvider>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<TerminalProvider>
<ScrollProvider>
<ContextCapture>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ContextCapture>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
</OverflowProvider>
</UIActionsContext.Provider>
</StreamingContext.Provider>
</SessionStatsProvider>
@@ -702,6 +747,8 @@ export const renderWithProviders = (
return {
...renderResult,
capturedOverflowState,
capturedOverflowActions,
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
simulateClick(renderResult.stdin, col, row, button),
};
+2 -1
View File
@@ -46,6 +46,7 @@ export const createMockSettings = (
workspace,
isTrusted,
errors,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
merged: mergedOverride,
...settingsOverrides
} = overrides;
@@ -75,7 +76,7 @@ export const createMockSettings = (
// Assign any function overrides (e.g., vi.fn() for methods)
for (const key in overrides) {
if (typeof overrides[key] === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
(loaded as any)[key] = overrides[key];
}
}
+343 -16
View File
@@ -26,6 +26,8 @@ import {
CoreEvent,
type UserFeedbackPayload,
type ResumedSessionData,
type StartupWarning,
WarningPriority,
AuthType,
type AgentDefinition,
CoreToolCallStatus,
@@ -103,6 +105,11 @@ import {
type UIActions,
} from './contexts/UIActionsContext.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
import { OverflowProvider } from './contexts/OverflowContext.js';
import {
useOverflowActions,
type OverflowActions,
} from './contexts/OverflowContext.js';
// Mock useStdout to capture terminal title writes
vi.mock('ink', async (importOriginal) => {
@@ -118,9 +125,11 @@ vi.mock('ink', async (importOriginal) => {
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedUIActions: UIActions;
let capturedOverflowActions: OverflowActions;
function TestContextConsumer() {
capturedUIState = useContext(UIStateContext)!;
capturedUIActions = useContext(UIActionsContext)!;
capturedOverflowActions = useOverflowActions()!;
return null;
}
@@ -227,7 +236,10 @@ import {
disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
import {
WARNING_PROMPT_DURATION_MS,
EXPAND_HINT_DURATION_MS,
} from './constants.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
@@ -248,18 +260,20 @@ describe('AppContainer State Management', () => {
config?: Config;
version?: string;
initResult?: InitializationResult;
startupWarnings?: string[];
startupWarnings?: StartupWarning[];
resumedSessionData?: ResumedSessionData;
} = {}) => (
<SettingsContext.Provider value={settings}>
<KeypressProvider config={config}>
<AppContainer
config={config}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
resumedSessionData={resumedSessionData}
/>
<OverflowProvider>
<AppContainer
config={config}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
resumedSessionData={resumedSessionData}
/>
</OverflowProvider>
</KeypressProvider>
</SettingsContext.Provider>
);
@@ -501,7 +515,18 @@ describe('AppContainer State Management', () => {
});
it('renders with startup warnings', async () => {
const startupWarnings = ['Warning 1', 'Warning 2'];
const startupWarnings: StartupWarning[] = [
{
id: 'w1',
message: 'Warning 1',
priority: WarningPriority.High,
},
{
id: 'w2',
message: 'Warning 2',
priority: WarningPriority.High,
},
];
let unmount: () => void;
await act(async () => {
@@ -2674,12 +2699,14 @@ describe('AppContainer State Management', () => {
const getTree = (settings: LoadedSettings) => (
<SettingsContext.Provider value={settings}>
<KeypressProvider config={mockConfig}>
<AppContainer
config={mockConfig}
version="1.0.0"
initializationResult={mockInitResult}
/>
<TestChild />
<OverflowProvider>
<AppContainer
config={mockConfig}
version="1.0.0"
initializationResult={mockInitResult}
/>
<TestChild />
</OverflowProvider>
</KeypressProvider>
</SettingsContext.Provider>
);
@@ -3290,6 +3317,306 @@ describe('AppContainer State Management', () => {
});
});
describe('Submission Handling', () => {
it('resets expansion state on submission when not in alternate buffer', async () => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
vi.mocked(checkPermissions).mockResolvedValue([]);
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
settings: {
...mockSettings,
merged: {
...mockSettings.merged,
ui: { ...mockSettings.merged.ui, useAlternateBuffer: false },
},
} as LoadedSettings,
}).unmount;
});
await waitFor(() => expect(capturedUIActions).toBeTruthy());
// Expand first
act(() => capturedUIActions.setConstrainHeight(false));
expect(capturedUIState.constrainHeight).toBe(false);
// Reset mock stdout to clear any initial writes
mocks.mockStdout.write.mockClear();
// Submit
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
// Should be reset
expect(capturedUIState.constrainHeight).toBe(true);
// Should refresh static (which clears terminal in non-alternate buffer)
expect(mocks.mockStdout.write).toHaveBeenCalledWith(
ansiEscapes.clearTerminal,
);
unmount!();
});
it('resets expansion state on submission when in alternate buffer without clearing terminal', async () => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
vi.mocked(checkPermissions).mockResolvedValue([]);
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
settings: {
...mockSettings,
merged: {
...mockSettings.merged,
ui: { ...mockSettings.merged.ui, useAlternateBuffer: true },
},
} as LoadedSettings,
}).unmount;
});
await waitFor(() => expect(capturedUIActions).toBeTruthy());
// Expand first
act(() => capturedUIActions.setConstrainHeight(false));
expect(capturedUIState.constrainHeight).toBe(false);
// Reset mock stdout
mocks.mockStdout.write.mockClear();
// Submit
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
// Should be reset
expect(capturedUIState.constrainHeight).toBe(true);
// Should NOT refresh static's clearTerminal in alternate buffer
expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(
ansiEscapes.clearTerminal,
);
unmount!();
});
});
describe('Overflow Hint Handling', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('sets showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Trigger overflow
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
await waitFor(() => {
// Should show hint because we are in Standard Mode (default settings) and have overflow
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
// Advance just before the timeout
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Advance to hit the timeout mark
act(() => {
vi.advanceTimersByTime(100);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount!();
});
it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
let unmount: () => void;
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
stdin = result.stdin;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Initial state is constrainHeight = true
expect(capturedUIState.constrainHeight).toBe(true);
// Trigger overflow so the hint starts showing
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
// Advance half the duration
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Simulate Ctrl+O
act(() => {
stdin.write('\x0f'); // \x0f is Ctrl+O
});
await waitFor(() => {
// constrainHeight should toggle
expect(capturedUIState.constrainHeight).toBe(false);
});
// Advance enough that the original timer would have expired if it hadn't reset
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 1000);
});
// We expect it to still be true because Ctrl+O should have reset the timer
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Advance remaining time to reach the new timeout
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 1000);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount!();
});
it('toggles Ctrl+O multiple times and verifies the hint disappears exactly after the last toggle', async () => {
let unmount: () => void;
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
stdin = result.stdin;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Initial state is constrainHeight = true
expect(capturedUIState.constrainHeight).toBe(true);
// Trigger overflow so the hint starts showing
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
// Advance half the duration
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// First toggle 'on' (expanded)
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
await waitFor(() => {
expect(capturedUIState.constrainHeight).toBe(false);
});
// Wait 1 second
act(() => {
vi.advanceTimersByTime(1000);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Second toggle 'off' (collapsed)
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
await waitFor(() => {
expect(capturedUIState.constrainHeight).toBe(true);
});
// Wait 1 second
act(() => {
vi.advanceTimersByTime(1000);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Third toggle 'on' (expanded)
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
await waitFor(() => {
expect(capturedUIState.constrainHeight).toBe(false);
});
// Now we wait just before the timeout from the LAST toggle.
// It should still be true.
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Wait 0.1s more to hit exactly the timeout since the last toggle.
// It should hide now.
act(() => {
vi.advanceTimersByTime(100);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount!();
});
it('does NOT set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
const alternateSettings = mergeSettings({}, {}, {}, {}, true);
const settingsWithAlternateBuffer = {
merged: {
...alternateSettings,
ui: {
...alternateSettings.ui,
useAlternateBuffer: true,
},
},
} as unknown as LoadedSettings;
let unmount: () => void;
await act(async () => {
const result = renderAppContainer({
settings: settingsWithAlternateBuffer,
});
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Trigger overflow
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
// Should NOT show hint because we are in Alternate Buffer Mode
expect(capturedUIState.showIsExpandableHint).toBe(false);
unmount!();
});
});
describe('Permission Handling', () => {
it('shows permission dialog when checkPermissions returns paths', async () => {
const { checkPermissions } = await import(
+138 -7
View File
@@ -41,6 +41,7 @@ import { checkPermissions } from './hooks/atCommandProcessor.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import {
type StartupWarning,
type EditorType,
type Config,
type IdeInfo,
@@ -94,6 +95,10 @@ import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useVimMode } from './contexts/VimModeContext.js';
import {
useOverflowActions,
useOverflowState,
} from './contexts/OverflowContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
@@ -150,6 +155,7 @@ import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js'
import {
WARNING_PROMPT_DURATION_MS,
QUEUE_ERROR_DISPLAY_DURATION_MS,
EXPAND_HINT_DURATION_MS,
} from './constants.js';
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
@@ -186,7 +192,7 @@ function isToolAwaitingConfirmation(
interface AppContainerProps {
config: Config;
startupWarnings?: string[];
startupWarnings?: StartupWarning[];
version: string;
initializationResult: InitializationResult;
resumedSessionData?: ResumedSessionData;
@@ -213,6 +219,7 @@ const SHELL_HEIGHT_PADDING = 10;
export const AppContainer = (props: AppContainerProps) => {
const { config, initializationResult, resumedSessionData } = props;
const settings = useSettings();
const { reset } = useOverflowActions()!;
const notificationsEnabled = isNotificationsEnabled(settings);
const historyManager = useHistory({
@@ -261,6 +268,54 @@ export const AppContainer = (props: AppContainerProps) => {
);
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
const [showIsExpandableHint, setShowIsExpandableHint] = useState(false);
const expandHintTimerRef = useRef<NodeJS.Timeout | null>(null);
const overflowState = useOverflowState();
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
/**
* Manages the visibility and x-second timer for the expansion hint.
*
* This effect triggers the timer countdown whenever an overflow is detected
* or the user manually toggles the expansion state with Ctrl+O. We use a stable
* boolean dependency (hasOverflowState) to ensure the timer only resets on
* genuine state transitions, preventing it from infinitely resetting during
* active text streaming.
*/
useEffect(() => {
if (isAlternateBuffer) {
setShowIsExpandableHint(false);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
return;
}
if (hasOverflowState) {
setShowIsExpandableHint(true);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
expandHintTimerRef.current = setTimeout(() => {
setShowIsExpandableHint(false);
}, EXPAND_HINT_DURATION_MS);
}
}, [hasOverflowState, isAlternateBuffer, constrainHeight]);
/**
* Safe cleanup to ensure the expansion hint timer is cancelled when the
* component unmounts, preventing memory leaks.
*/
useEffect(
() => () => {
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
},
[],
);
const [defaultBannerText, setDefaultBannerText] = useState('');
const [warningBannerText, setWarningBannerText] = useState('');
@@ -1188,6 +1243,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
const handleFinalSubmit = useCallback(
async (submittedValue: string) => {
reset();
// Explicitly hide the expansion hint and clear its x-second timer when a new turn begins.
setShowIsExpandableHint(false);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
if (!constrainHeight) {
setConstrainHeight(true);
if (!isAlternateBuffer) {
refreshStatic();
}
}
const isSlash = isSlashCommand(submittedValue.trim());
const isIdle = streamingState === StreamingState.Idle;
const isAgentRunning =
@@ -1246,15 +1314,32 @@ Logging in with Google... Restarting Gemini CLI to continue.
pendingSlashCommandHistoryItems,
pendingGeminiHistoryItems,
config,
constrainHeight,
setConstrainHeight,
isAlternateBuffer,
refreshStatic,
reset,
handleHintSubmit,
],
);
const handleClearScreen = useCallback(() => {
reset();
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
setShowIsExpandableHint(false);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
historyManager.clearItems();
clearConsoleMessagesState();
refreshStatic();
}, [historyManager, clearConsoleMessagesState, refreshStatic]);
}, [
historyManager,
clearConsoleMessagesState,
refreshStatic,
reset,
setShowIsExpandableHint,
]);
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
@@ -1424,7 +1509,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
windowMs: WARNING_PROMPT_DURATION_MS,
onRepeat: handleExitRepeat,
});
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
const [ideContextState, setIdeContextState] = useState<
IdeContext | undefined
>();
@@ -1436,8 +1521,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
type: TransientMessageType;
}>(WARNING_PROMPT_DURATION_MS);
const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
const {
isFolderTrustDialogOpen,
discoveryResults: folderDiscoveryResults,
handleFolderTrustSelect,
isRestarting,
} = useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
const policyUpdateConfirmationRequest =
config.getPolicyUpdateConfirmationRequest();
const [isPolicyUpdateDialogOpen, setIsPolicyUpdateDialogOpen] = useState(
!!policyUpdateConfirmationRequest,
);
const {
needsRestart: ideNeedsRestart,
restartReason: ideTrustRestartReason,
@@ -1596,6 +1691,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
streamingState,
shouldShowFocusHint,
retryStatus,
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
customWittyPhrases: settings.merged.ui.customWittyPhrases,
});
const handleGlobalKeypress = useCallback(
@@ -1642,6 +1739,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (!constrainHeight) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
// If the user manually collapses the view, show the hint and reset the x-second timer.
setShowIsExpandableHint(true);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
expandHintTimerRef.current = setTimeout(() => {
setShowIsExpandableHint(false);
}, EXPAND_HINT_DURATION_MS);
}
if (!isAlternateBuffer) {
refreshStatic();
}
}
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
@@ -1685,6 +1795,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
!enteringConstrainHeightMode
) {
setConstrainHeight(false);
// If the user manually expands the view, show the hint and reset the x-second timer.
setShowIsExpandableHint(true);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
expandHintTimerRef.current = setTimeout(() => {
setShowIsExpandableHint(false);
}, EXPAND_HINT_DURATION_MS);
if (!isAlternateBuffer) {
refreshStatic();
}
return true;
} else if (
(keyMatchers[Command.FOCUS_SHELL_INPUT](key) ||
@@ -1908,6 +2029,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
(shouldShowRetentionWarning && retentionCheckComplete) ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
isPolicyUpdateDialogOpen ||
adminSettingsChanged ||
!!commandConfirmationRequest ||
!!authConsentRequest ||
@@ -2064,8 +2186,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
const fetchBannerTexts = async () => {
const [defaultBanner, warningBanner] = await Promise.all([
// TODO: temporarily disabling the banner, it will be re-added.
'',
config.getBannerTextNoCapacityIssues(),
config.getBannerTextCapacityIssues(),
]);
@@ -2135,6 +2256,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
isResuming,
shouldShowIdePrompt,
isFolderTrustDialogOpen: isFolderTrustDialogOpen ?? false,
folderDiscoveryResults,
isPolicyUpdateDialogOpen,
policyUpdateConfirmationRequest,
isTrustedFolder,
constrainHeight,
showErrorDetails,
@@ -2202,6 +2326,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
isBackgroundShellListOpen,
adminSettingsChanged,
newAgents,
showIsExpandableHint,
hintMode:
config.isModelSteeringEnabled() &&
isToolExecuting([
@@ -2257,6 +2382,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
isResuming,
shouldShowIdePrompt,
isFolderTrustDialogOpen,
folderDiscoveryResults,
isPolicyUpdateDialogOpen,
policyUpdateConfirmationRequest,
isTrustedFolder,
constrainHeight,
showErrorDetails,
@@ -2325,6 +2453,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
adminSettingsChanged,
newAgents,
showIsExpandableHint,
],
);
@@ -2354,6 +2483,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
vimHandleInput,
handleIdePromptComplete,
handleFolderTrustSelect,
setIsPolicyUpdateDialogOpen,
setConstrainHeight,
onEscapePromptChange: handleEscapePromptChange,
refreshStatic,
@@ -2438,6 +2568,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
vimHandleInput,
handleIdePromptComplete,
handleFolderTrustSelect,
setIsPolicyUpdateDialogOpen,
setConstrainHeight,
handleEscapePromptChange,
refreshStatic,
@@ -125,6 +125,7 @@ describe('copyCommand', () => {
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Hi there! How can I help you?',
expect.anything(),
);
});
@@ -143,7 +144,10 @@ describe('copyCommand', () => {
const result = await copyCommand.action(mockContext, '');
expect(mockCopyToClipboard).toHaveBeenCalledWith('Part 1: Part 2: Part 3');
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Part 1: Part 2: Part 3',
expect.anything(),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -170,7 +174,10 @@ describe('copyCommand', () => {
const result = await copyCommand.action(mockContext, '');
expect(mockCopyToClipboard).toHaveBeenCalledWith('Text part more text');
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Text part more text',
expect.anything(),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -201,7 +208,10 @@ describe('copyCommand', () => {
const result = await copyCommand.action(mockContext, '');
expect(mockCopyToClipboard).toHaveBeenCalledWith('Second AI response');
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'Second AI response',
expect.anything(),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
@@ -230,6 +240,11 @@ describe('copyCommand', () => {
messageType: 'error',
content: `Failed to copy to the clipboard. ${clipboardError.message}`,
});
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'AI response',
expect.anything(),
);
});
it('should handle non-Error clipboard errors', async () => {
@@ -253,6 +268,11 @@ describe('copyCommand', () => {
messageType: 'error',
content: `Failed to copy to the clipboard. ${rejectedValue}`,
});
expect(mockCopyToClipboard).toHaveBeenCalledWith(
'AI response',
expect.anything(),
);
});
it('should return info message when no text parts found in AI message', async () => {
+2 -1
View File
@@ -38,7 +38,8 @@ export const copyCommand: SlashCommand = {
if (lastAiOutput) {
try {
await copyToClipboard(lastAiOutput);
const settings = context.services.settings.merged;
await copyToClipboard(lastAiOutput, settings);
return {
type: 'message',
@@ -20,6 +20,7 @@ import {
import {
type CommandContext,
type SlashCommand,
type SlashCommandActionReturn,
CommandKind,
} from './types.js';
import open from 'open';
@@ -35,6 +36,7 @@ import { stat } from 'node:fs/promises';
import { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
import { type ConfigLogger } from '../../commands/extensions/utils.js';
import { ConfigExtensionDialog } from '../components/ConfigExtensionDialog.js';
import { ExtensionRegistryView } from '../components/views/ExtensionRegistryView.js';
import React from 'react';
function showMessageIfNoExtensions(
@@ -265,7 +267,28 @@ async function restartAction(
}
}
async function exploreAction(context: CommandContext) {
async function exploreAction(
context: CommandContext,
): Promise<SlashCommandActionReturn | void> {
const settings = context.services.settings.merged;
const useRegistryUI = settings.experimental?.extensionRegistry;
if (useRegistryUI) {
const extensionManager = context.services.config?.getExtensionLoader();
if (extensionManager instanceof ExtensionManager) {
return {
type: 'custom_dialog' as const,
component: React.createElement(ExtensionRegistryView, {
onSelect: (extension) => {
debugLogger.debug(`Selected extension: ${extension.extensionName}`);
},
onClose: () => context.ui.removeComponent(),
extensionManager,
}),
};
}
}
const extensionsUrl = 'https://geminicli.com/extensions/';
// Only check for NODE_ENV for explicit test mode, not for unit test framework
@@ -51,7 +51,7 @@ describe('planCommand', () => {
getApprovalMode: vi.fn(),
getFileSystemService: vi.fn(),
storage: {
getProjectTempPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
},
},
},
+1 -1
View File
@@ -43,7 +43,7 @@ export const planCommand: SlashCommand = {
try {
const content = await processSingleFileContent(
approvedPlanPath,
config.storage.getProjectTempPlansDir(),
config.storage.getPlansDir(),
config.getFileSystemService(),
);
const fileName = path.basename(approvedPlanPath);
@@ -23,6 +23,7 @@ import {
RewindEvent,
type ChatRecordingService,
type GeminiClient,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
/**
@@ -54,9 +55,8 @@ async function rewindConversation(
}
// Convert to UI and Client formats
const { uiHistory, clientHistory } = convertSessionToHistoryFormats(
conversation.messages,
);
const { uiHistory } = convertSessionToHistoryFormats(conversation.messages);
const clientHistory = convertSessionToClientHistory(conversation.messages);
client.setHistory(clientHistory as Content[]);
+4 -1
View File
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { getDisplayString } from '@google/gemini-cli-core';
interface AboutBoxProps {
cliVersion: string;
@@ -79,7 +80,9 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{modelVersion}</Text>
<Text color={theme.text.primary}>
{getDisplayString(modelVersion)}
</Text>
</Box>
</Box>
<Box flexDirection="row">
@@ -391,16 +391,16 @@ describe('Composer', () => {
expect(output).not.toContain('ShortcutsHint');
});
it('renders LoadingIndicator without thought when accessibility disables loading phrases', async () => {
it('renders LoadingIndicator without thought when loadingPhrases is off', async () => {
const uiState = createMockUIState({
streamingState: StreamingState.Responding,
thought: { subject: 'Hidden', description: 'Should not show' },
});
const config = createMockConfig({
getAccessibility: vi.fn(() => ({ enableLoadingPhrases: false })),
const settings = createMockSettings({
merged: { ui: { loadingPhrases: 'off' } },
});
const { lastFrame } = await renderComposer(uiState, undefined, config);
const { lastFrame } = await renderComposer(uiState, settings);
const output = lastFrame();
expect(output).toContain('LoadingIndicator');

Some files were not shown because too many files have changed in this diff Show More