Compare commits

..

82 Commits

Author SHA1 Message Date
jacob314 a09f6b40ff Debug memory alloc 2026-03-19 11:30:58 -07:00
Sandy Tao 4e5dfd0cb7 feat(core): cap JIT context upward traversal at git root (#23074) 2026-03-19 17:50:58 +00:00
Jacob Richman 524b1e39a5 Fix logging and virtual list. (#23080) 2026-03-19 17:50:49 +00:00
Abhi 32a123fc54 feat(core): inject memory and JIT context into subagents (#23032) 2026-03-19 17:16:09 +00:00
Tommaso Sciortino 23264ced9a refactor(cli): integrate real config loading into async test utils (#23040) 2026-03-19 17:05:33 +00:00
cynthialong0-0 7de0616229 fix(browser-agent): enable "Allow all server tools" session policy (#22343) 2026-03-19 16:32:35 +00:00
joshualitt 39d3b0e28c feat(cli): Partial threading of AgentLoopContext. (#22978) 2026-03-19 16:02:13 +00:00
Gaurav 5acaacad96 fix(core): narrow browser agent description to prevent stealing URL tasks from web_fetch (#23086) 2026-03-19 10:45:59 +00:00
Jason Matthew Suhari a921bcd9ef fix(core): don't persist browser consent sentinel in non-interactive mode (#23073) 2026-03-19 06:47:13 +00:00
Sri Pasumarthi e9171fd792 fix: circumvent genai sdk requirement for api key when using gateway auth via ACP (#23042) 2026-03-19 04:31:02 +00:00
Adam Weidman 2009fbbd92 feat(core): add content-utils (#22984) 2026-03-19 02:30:48 +00:00
Bryan Morgan 8db2948361 fix(cli): correctly handle auto-update for standalone binaries (#23038) 2026-03-19 01:52:23 +00:00
Abhi 5fa14dbe42 feat(core): resilient subagent tool rejection with contextual feedback (#22951) 2026-03-19 01:09:37 +00:00
Suraj Sahani e6cd5d208c docs: add FAQ entry for checking Gemini CLI version (#21271) 2026-03-19 00:25:33 +00:00
Valery Teplyakov c9d48026c4 fix(acp): provide more meta in tool_call_update (#22663)
Co-authored-by: Mervap <megavaprold@gmail.com>
Co-authored-by: Sri Pasumarthi <sripas@google.com>
2026-03-19 00:02:07 +00:00
Dev Randalpura 34f271504a fix(ui): fix flickering on small terminal heights (#21416)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-03-18 21:28:21 +00:00
gemini-cli-robot d68100e6bc Changelog for v0.35.0-preview.1 (#23012)
Co-authored-by: g-samroberts <158088236+g-samroberts@users.noreply.github.com>
2026-03-18 20:55:55 +00:00
ruomeng 94e6bf8591 fix(cli): resolve duplicate footer on tool cancel via ESC (#21743) (#21781) 2026-03-18 20:27:38 +00:00
Abhi fd44718bfe docs: add additional notes (#23008) 2026-03-18 20:24:51 +00:00
Emily Hedlund f6e21f50fd feat(core): implement strict macOS sandboxing using Seatbelt allowlist (#22832) 2026-03-18 20:07:54 +00:00
ruomeng 1725ec346b feat(plan): support plan mode in non-interactive mode (#22670) 2026-03-18 20:00:26 +00:00
AK c12fc340c1 fix(core): explicitly map execution context in LocalAgentExecutor (#22949)
Co-authored-by: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com>
2026-03-18 19:54:48 +00:00
Aditya Bijalwan b6d5374fb7 Feat/browser privacy consent (#21119) 2026-03-18 19:33:24 +00:00
Christian Gunderman 0082e1ec97 Fix issue where subagent thoughts are appended. (#22975) 2026-03-18 19:20:31 +00:00
ANIRUDDHA ADAK 0ed9f1e7f5 feat(ui): remove write todo list tool from UI tips (#22281)
Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
Co-authored-by: anj-s <32556631+anj-s@users.noreply.github.com>
2026-03-18 19:06:42 +00:00
Sam Roberts adf21df71e Docs: Remove references to stale Gemini CLI file structure info (#22976) 2026-03-18 18:43:56 +00:00
Keith Guerin 4dcca1ca10 feat(ui): format multi-line banner warnings with a bold title (#22955)
Co-authored-by: Sehoon Shon <sshon@google.com>
2026-03-18 18:39:12 +00:00
Tommaso Sciortino a5a461c234 test(cli): simplify createMockSettings calls (#22952) 2026-03-18 18:12:44 +00:00
gemini-cli-robot fac3661980 Changelog for v0.34.0 (#22860)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-18 17:23:05 +00:00
Tommaso Sciortino d7dfcf7f99 refactor(cli): simplify keypress and mouse providers and update tests (#22853) 2026-03-18 16:38:56 +00:00
Abhi 81a97e78f1 refactor(cli): group subagent trajectory deletion and use native filesystem testing (#22890) 2026-03-18 14:42:15 +00:00
jhhornn 1311e8c480 fix: updates Docker image reference for GitHub MCP server (#22938) 2026-03-18 14:32:57 +00:00
Abhi 4ecb4bb24b refactor(ui): extract SessionBrowser search and navigation components (#22377) 2026-03-18 04:44:01 +00:00
Abhi be7c7bb83d fix(cli): resolve subagent grouping and UI state persistence (#22252) 2026-03-18 03:11:20 +00:00
AK 7bfe6ac418 feat(core): subagent local execution and tool isolation (#22718) 2026-03-18 02:34:44 +00:00
adithya32 bd34a42ec3 fix(cli): lazily load settings in onModelChange to prevent stale closure data loss (#20403)
Co-authored-by: Spencer <spencertang@google.com>
2026-03-18 00:40:38 +00:00
N. Taylor Mullen e2658ccda8 refactor(cli): remove extra newlines in ShellToolMessage.tsx (#22868)
Co-authored-by: Spencer <spencertang@google.com>
2026-03-17 23:48:16 +00:00
anj-s b8719bcd47 feat: add 'blocked' status to tasks and todos (#22735) 2026-03-17 23:24:26 +00:00
Sakshi semalti e1eefffcf1 fix(cli): automatically add all VSCode workspace folders to Gemini context (#21380)
Co-authored-by: Spencer <spencertang@google.com>
2026-03-17 23:05:49 +00:00
Alisa 7ae39fd622 feat(a2a): add agent acknowledgment command and enhance registry discovery (#22389) 2026-03-17 22:47:05 +00:00
N. Taylor Mullen fb9264bf80 chore: add posttest build hooks and fix missing dependencies (#22865) 2026-03-17 22:23:00 +00:00
anj-s d4397dbfc5 feat(tracker): integrate task tracker protocol into core system prompt (#22442) 2026-03-17 22:19:36 +00:00
Spencer 5fb0d1f01d fix(core): add actionable warnings for terminal fallbacks (#14426) (#22211) 2026-03-17 21:57:37 +00:00
Abhi 95bca2c3b3 fix(cli): expand tilde in policy paths from settings.json (#22772) 2026-03-17 21:48:24 +00:00
matt korwel e0be1b2afd fix(cli): use active sessionId in useLogger and improve resume robustness (#22606) 2026-03-17 21:42:40 +00:00
gemini-cli-robot 5d4e4c2814 chore(release): bump version to 0.36.0-nightly.20260317.2f90b4653 (#22858) 2026-03-17 21:18:21 +00:00
kevinjwang1 27a50191e3 Add support for dynamic model Resolution to ModelConfigService (#22578) 2026-03-17 21:15:50 +00:00
Sandy Tao 77ca3c0e13 fix(devtools): use theme-aware text colors for console warnings and errors (#22181) 2026-03-17 21:00:40 +00:00
AK 2504105a1c feat(core): multi-registry architecture and tool filtering for subagents (#22712) 2026-03-17 20:54:07 +00:00
gemini-cli-robot a361a84708 Changelog for v0.34.0-preview.4 (#22752)
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-03-17 20:46:38 +00:00
gemini-cli-robot ff196fbe6f Changelog for v0.33.2 (#22730)
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-03-17 20:33:30 +00:00
David Pierce 2f90b46537 Linux sandbox seccomp (#22815)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2026-03-17 20:29:13 +00:00
Sandy Tao 82d8680dcc refactor(core): align JIT memory placement with tiered context model (#22766) 2026-03-17 20:20:32 +00:00
Jomak-x 1f3f7247b1 fix(cli): clean up stale pasted placeholder metadata after word/line deletions (#20375)
Co-authored-by: ruomeng <ruomeng@google.com>
2026-03-17 20:16:26 +00:00
Sandy Tao 69e2d8c7ae docs: distribute package-specific GEMINI.md context to each package (#22734) 2026-03-17 19:51:23 +00:00
Adib234 77a874cf65 feat(plan): add 'All the above' option to multi-select AskUser questions (#22365)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-03-17 19:17:34 +00:00
Sehoon Shon b211f30d95 fix(cli): override j/k navigation in settings dialog to fix search input conflict (#22800) 2026-03-17 19:08:45 +00:00
Sehoon Shon fc51e50bc6 fix(core): handle surrogate pairs in truncateString (#22754) 2026-03-17 05:41:19 +00:00
AK 695bcaea0d feat(core): add foundation for subagent tool isolation (#22708) 2026-03-17 03:54:33 +00:00
gemini-cli-robot abe83fce0b Changelog for v0.34.0-preview.3 (#22393)
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-03-17 00:52:17 +00:00
Adib234 61fd71dc29 fix(plan): allowlist get_internal_docs in Plan Mode (#22668) 2026-03-17 00:34:30 +00:00
Kevin Ramdass cddd9e7148 fix(config): ensure discoveryMaxDirs is passed to global config during initialization (#22744)
Co-authored-by: matt korwel <matt.korwel@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-16 23:38:56 +00:00
Sandy Tao 30271d64eb feat(config): enable JIT context loading by default (#22736) 2026-03-16 22:12:28 +00:00
Adam Weidman 605432ea70 refactor(core): replace positional execute params with ExecuteOptions bag (#22674) 2026-03-16 21:50:24 +00:00
Aishanee Shah 990d010ecf feat(core): implement Stage 2 security and consistency improvements for web_fetch (#22217) 2026-03-16 21:38:53 +00:00
Bryan Morgan b6c6da3618 feat(core): increase thought signature retry resilience (#22202)
Co-authored-by: Aishanee Shah <aishaneeshah@google.com>
2026-03-16 21:35:33 +00:00
David Pierce 8f22ffd2b1 Linux sandbox bubblewrap (#22680) 2026-03-16 21:34:48 +00:00
Adam Weidman 44ce90d76c refactor(core): introduce InjectionService with source-aware injection and backend-native background completions (#22544) 2026-03-16 21:06:29 +00:00
Sandy Tao b91f75cd6d fix(core): fix three JIT context bugs in read_file, read_many_files, and memoryDiscovery (#22679) 2026-03-16 20:10:50 +00:00
gemini-cli-robot dfe22aae21 Changelog for v0.34.0-preview.2 (#22220)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-16 19:22:01 +00:00
anj-s bba9c07541 feat(tracker): polish UI sorting and formatting (#22437) 2026-03-16 19:18:01 +00:00
Emily Hedlund 05fda0cf01 feat(extensions): implement cryptographic integrity verification for extension updates (#21772) 2026-03-16 19:01:52 +00:00
Abhi d43ec6c8f3 feat: enable subagents (#22386) 2026-03-16 18:40:12 +00:00
Jack Wotherspoon 56e0865a7b docs(changelog): remove internal commands from release notes (#22529) 2026-03-16 18:39:00 +00:00
Michael Bleigh cd2096ca80 refactor(core): Creates AgentSession abstraction for consolidated agent interface. (#22270) 2026-03-16 17:59:02 +00:00
Sehoon Shon 48130ebd25 Guard pro model usage (#22665) 2026-03-16 17:44:25 +00:00
Christian Gunderman ef5627eece Disallow Object.create() and reflect. (#22408) 2026-03-16 16:24:27 +00:00
Sri Pasumarthi e3df87cf1a fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled tool calls. (#22230) 2026-03-16 15:50:11 +00:00
Emily Hedlund 8bad5823a9 fix(a2a-server): resolve unsafe assignment lint errors (#22661) 2026-03-16 15:28:35 +00:00
kawasin73 fd62938945 docs(policy): remove trailing space from commandPrefix examples (#22264)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-03-16 14:45:30 +00:00
anj-s fad032d466 perf: optimize TrackerService dependency checks (#22384) 2026-03-16 13:40:58 +00:00
cynthialong0-0 366aa84395 feat(agent): replace the runtime npx for browser agent chrome devtool mcp with pre-built bundle (#22213)
Co-authored-by: Gaurav Ghosh <gaghosh@google.com>
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2026-03-16 08:05:38 +00:00
436 changed files with 18171 additions and 7040 deletions
-15
View File
@@ -352,21 +352,6 @@ npm run lint
- **Imports:** Pay special attention to import paths. The project uses ESLint to
enforce restrictions on relative imports between packages.
### Project structure
- `packages/`: Contains the individual sub-packages of the project.
- `a2a-server`: A2A server implementation for the Gemini CLI. (Experimental)
- `cli/`: The command-line interface.
- `core/`: The core backend logic for the Gemini CLI.
- `test-utils` Utilities for creating and cleaning temporary file systems for
testing.
- `vscode-ide-companion/`: The Gemini CLI Companion extension pairs with
Gemini CLI.
- `docs/`: Contains all project documentation.
- `scripts/`: Utility scripts for building, testing, and development tasks.
For more detailed architecture, see `docs/architecture.md`.
### Debugging
#### VS Code
+3 -8
View File
@@ -22,9 +22,10 @@ powerful tool for developers.
rendering.
- `packages/core`: Backend logic, Gemini API orchestration, prompt
construction, and tool execution.
- `packages/core/src/tools/`: Built-in tools for file system, shell, and web
operations.
- `packages/a2a-server`: Experimental Agent-to-Agent server.
- `packages/sdk`: Programmatic SDK for embedding Gemini CLI capabilities.
- `packages/devtools`: Integrated developer tools (Network/Console inspector).
- `packages/test-utils`: Shared test utilities and test rig.
- `packages/vscode-ide-companion`: VS Code extension pairing with the CLI.
## Building and Running
@@ -58,10 +59,6 @@ powerful tool for developers.
## Development Conventions
- **Legacy Snippets:** `packages/core/src/prompts/snippets.legacy.ts` is a
snapshot of an older system prompt. Avoid changing the prompting verbiage to
preserve its historical behavior; however, structural changes to ensure
compilation or simplify the code are permitted.
- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires
signing the Google CLA.
- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue.
@@ -69,8 +66,6 @@ powerful tool for developers.
`gh` CLI.
- **Commit Messages:** Follow the
[Conventional Commits](https://www.conventionalcommits.org/) standard.
- **Coding Style:** Adhere to existing patterns in `packages/cli` (React/Ink)
and `packages/core` (Backend logic).
- **Imports:** Use specific imports and avoid restricted relative imports
between packages (enforced by ESLint).
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
-1
View File
@@ -314,7 +314,6 @@ gemini
- [**Headless Mode (Scripting)**](./docs/cli/headless.md) - Use Gemini CLI in
automated workflows.
- [**Architecture Overview**](./docs/architecture.md) - How Gemini CLI works.
- [**IDE Integration**](./docs/ide-integration/index.md) - VS Code companion.
- [**Sandboxing & Security**](./docs/cli/sandbox.md) - Safe execution
environments.
+13 -6
View File
@@ -18,6 +18,17 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.34.0 - 2026-03-17
- **Plan Mode Enabled by Default:** Plan Mode is now enabled by default to help
you break down complex tasks and execute them systematically
([#21713](https://github.com/google-gemini/gemini-cli/pull/21713) by @jerop).
- **Sandboxing Enhancements:** We've added native gVisor (runsc) and
experimental LXC container sandboxing support for safer execution environments
([#21062](https://github.com/google-gemini/gemini-cli/pull/21062) by
@Zheyuan-Lin, [#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
by @h30s).
## Announcements: v0.33.0 - 2026-03-11
- **Agent Architecture Enhancements:** Introduced HTTP authentication for A2A
@@ -125,10 +136,6 @@ on GitHub.
## Announcements: v0.28.0 - 2026-02-10
- **Slash Command:** We've added a new `/prompt-suggest` slash command to help
you generate prompt suggestions
([#17264](https://github.com/google-gemini/gemini-cli/pull/17264) by
@NTaylorMullen).
- **IDE Support:** Gemini CLI now supports the Positron IDE
([#15047](https://github.com/google-gemini/gemini-cli/pull/15047) by
@kapsner).
@@ -168,8 +175,8 @@ on GitHub.
([#16638](https://github.com/google-gemini/gemini-cli/pull/16638) by
@joshualitt).
- **UI/UX Improvements:** You can now "Rewind" through your conversation history
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by @Adib234)
and use a new `/introspect` command for debugging.
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by
@Adib234).
- **Core and Scheduler Refactoring:** The core scheduler has been significantly
refactored to improve performance and reliability
([#16895](https://github.com/google-gemini/gemini-cli/pull/16895) by
+460 -210
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.33.1
# Latest stable release: v0.34.0
Released: March 12, 2026
Released: March 17, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,224 +11,474 @@ npm install -g @google/gemini-cli
## Highlights
- **Agent Architecture Enhancements:** Introduced HTTP authentication support
for A2A remote agents, authenticated A2A agent card discovery, and directly
indicated auth-required states.
- **Plan Mode Updates:** Expanded Plan Mode capabilities with built-in research
subagents, annotation support for feedback during iteration, and a new `copy`
subcommand.
- **CLI UX Improvements:** Redesigned the header to be compact with an ASCII
icon, inverted the context window display to show usage, and allowed sub-agent
confirmation requests in the UI while preventing background flicker.
- **ACP & MCP Integrations:** Implemented slash command handling in ACP for
`/memory`, `/init`, `/extensions`, and `/restore`, added an MCPOAuthProvider,
and introduced a `set models` interface for ACP.
- **Admin & Core Stability:** Enabled a 30-day default retention for chat
history, added tool name validation in TOML policy files, and improved tool
parameter extraction.
- **Plan Mode Enabled by Default**: The comprehensive planning capability is now
enabled by default, allowing for better structured task management and
execution.
- **Enhanced Sandboxing Capabilities**: Added support for native gVisor (runsc)
sandboxing as well as experimental LXC container sandboxing to provide more
robust and isolated execution environments.
- **Improved Loop Detection & Recovery**: Implemented iterative loop detection
and model feedback mechanisms to prevent the CLI from getting stuck in
repetitive actions.
- **Customizable UI Elements**: You can now configure a custom footer using the
new `/footer` command, and enjoy standardized semantic focus colors for better
history visibility.
- **Extensive Subagent Updates**: Refinements across the tracker visualization
tools, background process logging, and broader fallback support for models in
tool execution scenarios.
## What's Changed
- fix(patch): cherry-pick 8432bce to release/v0.33.0-pr-22069 to patch version
v0.33.0 and create version 0.33.1 by @gemini-cli-robot in
[#22206](https://github.com/google-gemini/gemini-cli/pull/22206)
- Docs: Update model docs to remove Preview Features. by @jkcinouye in
[#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
[#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
- docs: add Windows PowerShell equivalents for environments and scripting by
@scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
- fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
[#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
- chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
- fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
[#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
- chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
- feat(cli): add chat resume footer on session quit by @lordshashank in
[#20667](https://github.com/google-gemini/gemini-cli/pull/20667)
- Support bold and other styles in svg snapshots by @jacob314 in
[#20937](https://github.com/google-gemini/gemini-cli/pull/20937)
- fix(core): increase A2A agent timeout to 30 minutes by @adamfweidman in
[#21028](https://github.com/google-gemini/gemini-cli/pull/21028)
- Cleanup old branches. by @jacob314 in
[#19354](https://github.com/google-gemini/gemini-cli/pull/19354)
- chore(release): bump version to 0.34.0-nightly.20260303.34f0c1538 by
@gemini-cli-robot in
[#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
- Changelog for v0.31.0 by @gemini-cli-robot in
[#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
- fix: use full paths for ACP diff payloads by @JagjeevanAK in
[#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
- Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
[#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
- fix: acp/zed race condition between MCP initialisation and prompt by
@kartikangiras in
[#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
- fix(cli): reset themeManager between tests to ensure isolation by
@NTaylorMullen in
[#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
- refactor(core): Extract tool parameter names as constants by @SandyTao520 in
[#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
- fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
mismatches by @sehoon38 in
[#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
- feat(skills): add github-issue-creator skill by @sehoon38 in
[#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
- fix(cli): allow sub-agent confirmation requests in UI while preventing
background flicker by @abhipatel12 in
[#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
- Merge User and Agent Card Descriptions #20849 by @adamfweidman in
[#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
- fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
[#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
- fix(plan): deflake plan mode integration tests by @Adib234 in
[#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
- Add /unassign support by @scidomino in
[#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
- feat(core): implement HTTP authentication support for A2A remote agents by
@SandyTao520 in
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
- feat(core): centralize read_file limits and update gemini-3 description by
[#21034](https://github.com/google-gemini/gemini-cli/pull/21034)
- feat(ui): standardize semantic focus colors and enhance history visibility by
@keithguerin in
[#20745](https://github.com/google-gemini/gemini-cli/pull/20745)
- fix: merge duplicate imports in packages/core (3/4) by @Nixxx19 in
[#20928](https://github.com/google-gemini/gemini-cli/pull/20928)
- Add extra safety checks for proto pollution by @jacob314 in
[#20396](https://github.com/google-gemini/gemini-cli/pull/20396)
- feat(core): Add tracker CRUD tools & visualization by @anj-s in
[#19489](https://github.com/google-gemini/gemini-cli/pull/19489)
- Revert "fix(ui): persist expansion in AskUser dialog when navigating options"
by @jacob314 in
[#21042](https://github.com/google-gemini/gemini-cli/pull/21042)
- Changelog for v0.33.0-preview.0 by @gemini-cli-robot in
[#21030](https://github.com/google-gemini/gemini-cli/pull/21030)
- fix: model persistence for all scenarios by @sripasg in
[#21051](https://github.com/google-gemini/gemini-cli/pull/21051)
- chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 by
@gemini-cli-robot in
[#21054](https://github.com/google-gemini/gemini-cli/pull/21054)
- Consistently guard restarts against concurrent auto updates by @scidomino in
[#21016](https://github.com/google-gemini/gemini-cli/pull/21016)
- Defensive coding to reduce the risk of Maximum update depth errors by
@jacob314 in [#20940](https://github.com/google-gemini/gemini-cli/pull/20940)
- fix(cli): Polish shell autocomplete rendering to be a little more shell native
feeling. by @jacob314 in
[#20931](https://github.com/google-gemini/gemini-cli/pull/20931)
- Docs: Update plan mode docs by @jkcinouye in
[#19682](https://github.com/google-gemini/gemini-cli/pull/19682)
- fix(mcp): Notifications/tools/list_changed support not working by @jacob314 in
[#21050](https://github.com/google-gemini/gemini-cli/pull/21050)
- fix(cli): register extension lifecycle events in DebugProfiler by
@fayerman-source in
[#20101](https://github.com/google-gemini/gemini-cli/pull/20101)
- chore(dev): update vscode settings for typescriptreact by @rohit-4321 in
[#19907](https://github.com/google-gemini/gemini-cli/pull/19907)
- fix(cli): enable multi-arch docker builds for sandbox by @ru-aish in
[#19821](https://github.com/google-gemini/gemini-cli/pull/19821)
- Changelog for v0.32.0 by @gemini-cli-robot in
[#21033](https://github.com/google-gemini/gemini-cli/pull/21033)
- Changelog for v0.33.0-preview.1 by @gemini-cli-robot in
[#21058](https://github.com/google-gemini/gemini-cli/pull/21058)
- feat(core): improve @scripts/copy_files.js autocomplete to prioritize
filenames by @sehoon38 in
[#21064](https://github.com/google-gemini/gemini-cli/pull/21064)
- feat(sandbox): add experimental LXC container sandbox support by @h30s in
[#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
- feat(evals): add overall pass rate row to eval nightly summary table by
@gundermanc in
[#20905](https://github.com/google-gemini/gemini-cli/pull/20905)
- feat(telemetry): include language in telemetry and fix accepted lines
computation by @gundermanc in
[#21126](https://github.com/google-gemini/gemini-cli/pull/21126)
- Changelog for v0.32.1 by @gemini-cli-robot in
[#21055](https://github.com/google-gemini/gemini-cli/pull/21055)
- feat(core): add robustness tests, logging, and metrics for CodeAssistServer
SSE parsing by @yunaseoul in
[#21013](https://github.com/google-gemini/gemini-cli/pull/21013)
- feat: add issue assignee workflow by @kartikangiras in
[#21003](https://github.com/google-gemini/gemini-cli/pull/21003)
- fix: improve error message when OAuth succeeds but project ID is required by
@Nixxx19 in [#21070](https://github.com/google-gemini/gemini-cli/pull/21070)
- feat(loop-reduction): implement iterative loop detection and model feedback by
@aishaneeshah in
[#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
- Do not block CI on evals by @gundermanc in
[#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
- document node limitation for shift+tab by @scidomino in
[#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
- Add install as an option when extension is selected. by @DavidAPierce in
[#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
- Update CODEOWNERS for README.md reviewers by @g-samroberts in
[#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
- feat(core): truncate large MCP tool output by @SandyTao520 in
[#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
- Subagent activity UX. by @gundermanc in
[#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
- style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
[#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
- feat: redesign header to be compact with ASCII icon by @keithguerin in
[#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
- fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
[#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
- feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
[#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
- refactor(cli): fully remove React anti patterns, improve type safety and fix
UX oversights in SettingsDialog.tsx by @psinha40898 in
[#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
- Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
@Nayana-Parameswarappa in
[#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
- feat(core): add tool name validation in TOML policy files by @allenhutchison
in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
- docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
[#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
- refactor(core): replace manual syncPlanModeTools with declarative policy rules
by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
- fix(core): increase default headers timeout to 5 minutes by @gundermanc in
[#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
- feat(admin): enable 30 day default retention for chat history & remove warning
[#20763](https://github.com/google-gemini/gemini-cli/pull/20763)
- chore(github): require prompt approvers for agent prompt files by @gundermanc
in [#20896](https://github.com/google-gemini/gemini-cli/pull/20896)
- Docs: Create tools reference by @jkcinouye in
[#19470](https://github.com/google-gemini/gemini-cli/pull/19470)
- fix(core, a2a-server): prevent hang during OAuth in non-interactive sessions
by @spencer426 in
[#21045](https://github.com/google-gemini/gemini-cli/pull/21045)
- chore(cli): enable deprecated settings removal by default by @yashodipmore in
[#20682](https://github.com/google-gemini/gemini-cli/pull/20682)
- feat(core): Disable fast ack helper for hints. by @joshualitt in
[#21011](https://github.com/google-gemini/gemini-cli/pull/21011)
- fix(ui): suppress redundant failure note when tool error note is shown by
@NTaylorMullen in
[#21078](https://github.com/google-gemini/gemini-cli/pull/21078)
- docs: document planning workflows with Conductor example by @jerop in
[#21166](https://github.com/google-gemini/gemini-cli/pull/21166)
- feat(release): ship esbuild bundle in npm package by @genneth in
[#19171](https://github.com/google-gemini/gemini-cli/pull/19171)
- fix(extensions): preserve symlinks in extension source path while enforcing
folder trust by @galz10 in
[#20867](https://github.com/google-gemini/gemini-cli/pull/20867)
- fix(cli): defer tool exclusions to policy engine in non-interactive mode by
@EricRahm in [#20639](https://github.com/google-gemini/gemini-cli/pull/20639)
- fix(ui): removed double padding on rendered content by @devr0306 in
[#21029](https://github.com/google-gemini/gemini-cli/pull/21029)
- fix(core): truncate excessively long lines in grep search output by
@gundermanc in
[#21147](https://github.com/google-gemini/gemini-cli/pull/21147)
- feat: add custom footer configuration via `/footer` by @jackwotherspoon in
[#19001](https://github.com/google-gemini/gemini-cli/pull/19001)
- perf(core): fix OOM crash in long-running sessions by @WizardsForgeGames in
[#19608](https://github.com/google-gemini/gemini-cli/pull/19608)
- refactor(cli): categorize built-in themes into dark/ and light/ directories by
@JayadityaGit in
[#18634](https://github.com/google-gemini/gemini-cli/pull/18634)
- fix(core): explicitly allow codebase_investigator and cli_help in read-only
mode by @Adib234 in
[#21157](https://github.com/google-gemini/gemini-cli/pull/21157)
- test: add browser agent integration tests by @kunal-10-cloud in
[#21151](https://github.com/google-gemini/gemini-cli/pull/21151)
- fix(cli): fix enabling kitty codes on Windows Terminal by @scidomino in
[#21136](https://github.com/google-gemini/gemini-cli/pull/21136)
- refactor(core): extract shared OAuth flow primitives from MCPOAuthProvider by
@SandyTao520 in
[#20895](https://github.com/google-gemini/gemini-cli/pull/20895)
- fix(ui): add partial output to cancelled shell UI by @devr0306 in
[#21178](https://github.com/google-gemini/gemini-cli/pull/21178)
- fix(cli): replace hardcoded keybinding strings with dynamic formatters by
@scidomino in [#21159](https://github.com/google-gemini/gemini-cli/pull/21159)
- DOCS: Update quota and pricing page by @g-samroberts in
[#21194](https://github.com/google-gemini/gemini-cli/pull/21194)
- feat(telemetry): implement Clearcut logging for startup statistics by
@yunaseoul in [#21172](https://github.com/google-gemini/gemini-cli/pull/21172)
- feat(triage): add area/documentation to issue triage by @g-samroberts in
[#21222](https://github.com/google-gemini/gemini-cli/pull/21222)
- Fix so shell calls are formatted by @jacob314 in
[#21237](https://github.com/google-gemini/gemini-cli/pull/21237)
- feat(cli): add native gVisor (runsc) sandboxing support by @Zheyuan-Lin in
[#21062](https://github.com/google-gemini/gemini-cli/pull/21062)
- docs: use absolute paths for internal links in plan-mode.md by @jerop in
[#21299](https://github.com/google-gemini/gemini-cli/pull/21299)
- fix(core): prevent unhandled AbortError crash during stream loop detection by
@7hokerz in [#21123](https://github.com/google-gemini/gemini-cli/pull/21123)
- fix:reorder env var redaction checks to scan values first by @kartikangiras in
[#21059](https://github.com/google-gemini/gemini-cli/pull/21059)
- fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences
by @skeshive in
[#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
- feat(plan): support annotating plans with feedback for iteration by @Adib234
in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
- Add some dos and don'ts to behavioral evals README. by @gundermanc in
[#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
- fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
[#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
- fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
models by @SandyTao520 in
[#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
- ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
[#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
- Build binary by @aswinashok44 in
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
- Code review fixes as a pr by @jacob314 in
[#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
- fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
[#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
- feat(cli): invert context window display to show usage by @keithguerin in
[#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
- fix(plan): clean up session directories and plans on deletion by @jerop in
[#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
- fix(core): enforce optionality for API response fields in code_assist by
@sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
- feat(extensions): add support for plan directory in extension manifest by
@mahimashanware in
[#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
- feat(plan): enable built-in research subagents in plan mode by @Adib234 in
[#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
- feat(agents): directly indicate auth required state by @adamfweidman in
[#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
- fix(cli): wait for background auto-update before relaunching by @scidomino in
[#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
- fix: pre-load @scripts/copy_files.js references from external editor prompts
by @kartikangiras in
[#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
- feat(evals): add behavioral evals for ask_user tool by @Adib234 in
[#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
- refactor common settings logic for skills,agents by @ishaanxgupta in
[#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
- Update docs-writer skill with new resource by @g-samroberts in
[#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
- fix(cli): pin clipboardy to ~5.2.x by @scidomino in
[#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
- feat: Implement slash command handling in ACP for
`/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
[#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
- Docs/add hooks reference by @AadithyaAle in
[#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
- feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
- fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
- Format the quota/limit style guide. by @g-samroberts in
[#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
- fix(core): send shell output to model on cancel by @devr0306 in
[#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
- remove hardcoded tiername when missing tier by @sehoon38 in
[#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
- feat(acp): add set models interface by @skeshive in
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
[#21171](https://github.com/google-gemini/gemini-cli/pull/21171)
- feat(core): fallback to 2.5 models with no access for toolcalls by @sehoon38
in [#21283](https://github.com/google-gemini/gemini-cli/pull/21283)
- test(core): improve testing for API request/response parsing by @sehoon38 in
[#21227](https://github.com/google-gemini/gemini-cli/pull/21227)
- docs(links): update docs-writer skill and fix broken link by @g-samroberts in
[#21314](https://github.com/google-gemini/gemini-cli/pull/21314)
- Fix code colorizer ansi escape bug. by @jacob314 in
[#21321](https://github.com/google-gemini/gemini-cli/pull/21321)
- remove wildcard behavior on keybindings by @scidomino in
[#21315](https://github.com/google-gemini/gemini-cli/pull/21315)
- feat(acp): Add support for AI Gateway auth by @skeshive in
[#21305](https://github.com/google-gemini/gemini-cli/pull/21305)
- fix(theme): improve theme color contrast for macOS Terminal.app by @clocky in
[#21175](https://github.com/google-gemini/gemini-cli/pull/21175)
- feat (core): Implement tracker related SI changes by @anj-s in
[#19964](https://github.com/google-gemini/gemini-cli/pull/19964)
- Changelog for v0.33.0-preview.2 by @gemini-cli-robot in
[#21333](https://github.com/google-gemini/gemini-cli/pull/21333)
- Changelog for v0.33.0-preview.3 by @gemini-cli-robot in
[#21347](https://github.com/google-gemini/gemini-cli/pull/21347)
- docs: format release times as HH:MM UTC by @pavan-sh in
[#20726](https://github.com/google-gemini/gemini-cli/pull/20726)
- fix(cli): implement --all flag for extensions uninstall by @sehoon38 in
[#21319](https://github.com/google-gemini/gemini-cli/pull/21319)
- docs: fix incorrect relative links to command reference by @kanywst in
[#20964](https://github.com/google-gemini/gemini-cli/pull/20964)
- documentiong ensures ripgrep by @Jatin24062005 in
[#21298](https://github.com/google-gemini/gemini-cli/pull/21298)
- fix(core): handle AbortError thrown during processTurn by @MumuTW in
[#21296](https://github.com/google-gemini/gemini-cli/pull/21296)
- docs(cli): clarify ! command output visibility in shell commands tutorial by
@MohammedADev in
[#21041](https://github.com/google-gemini/gemini-cli/pull/21041)
- fix: logic for task tracker strategy and remove tracker tools by @anj-s in
[#21355](https://github.com/google-gemini/gemini-cli/pull/21355)
- fix(partUtils): display media type and size for inline data parts by @Aboudjem
in [#21358](https://github.com/google-gemini/gemini-cli/pull/21358)
- Fix(accessibility): add screen reader support to RewindViewer by @Famous077 in
[#20750](https://github.com/google-gemini/gemini-cli/pull/20750)
- fix(hooks): propagate stopHookActive in AfterAgent retry path (#20426) by
@Aarchi-07 in [#20439](https://github.com/google-gemini/gemini-cli/pull/20439)
- fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive
filesystems (#19904) by @Nixxx19 in
[#19915](https://github.com/google-gemini/gemini-cli/pull/19915)
- feat(core): add concurrency safety guidance for subagent delegation (#17753)
by @abhipatel12 in
[#21278](https://github.com/google-gemini/gemini-cli/pull/21278)
- feat(ui): dynamically generate all keybinding hints by @scidomino in
[#21346](https://github.com/google-gemini/gemini-cli/pull/21346)
- feat(core): implement unified KeychainService and migrate token storage by
@ehedlund in [#21344](https://github.com/google-gemini/gemini-cli/pull/21344)
- fix(cli): gracefully handle --resume when no sessions exist by @SandyTao520 in
[#21429](https://github.com/google-gemini/gemini-cli/pull/21429)
- fix(plan): keep approved plan during chat compression by @ruomengz in
[#21284](https://github.com/google-gemini/gemini-cli/pull/21284)
- feat(core): implement generic CacheService and optimize setupUser by @sehoon38
in [#21374](https://github.com/google-gemini/gemini-cli/pull/21374)
- Update quota and pricing documentation with subscription tiers by @srithreepo
in [#21351](https://github.com/google-gemini/gemini-cli/pull/21351)
- fix(core): append correct OTLP paths for HTTP exporters by
@sebastien-prudhomme in
[#16836](https://github.com/google-gemini/gemini-cli/pull/16836)
- Changelog for v0.33.0-preview.4 by @gemini-cli-robot in
[#21354](https://github.com/google-gemini/gemini-cli/pull/21354)
- feat(cli): implement dot-prefixing for slash command conflicts by @ehedlund in
[#20979](https://github.com/google-gemini/gemini-cli/pull/20979)
- refactor(core): standardize MCP tool naming to mcp\_ FQN format by
@abhipatel12 in
[#21425](https://github.com/google-gemini/gemini-cli/pull/21425)
- feat(cli): hide gemma settings from display and mark as experimental by
@abhipatel12 in
[#21471](https://github.com/google-gemini/gemini-cli/pull/21471)
- feat(skills): refine string-reviewer guidelines and description by @clocky in
[#20368](https://github.com/google-gemini/gemini-cli/pull/20368)
- fix(core): whitelist TERM and COLORTERM in environment sanitization by
@deadsmash07 in
[#20514](https://github.com/google-gemini/gemini-cli/pull/20514)
- fix(billing): fix overage strategy lifecycle and settings integration by
@gsquared94 in
[#21236](https://github.com/google-gemini/gemini-cli/pull/21236)
- fix: expand paste placeholders in TextInput on submit by @Jefftree in
[#19946](https://github.com/google-gemini/gemini-cli/pull/19946)
- fix(core): add in-memory cache to ChatRecordingService to prevent OOM by
@SandyTao520 in
[#21502](https://github.com/google-gemini/gemini-cli/pull/21502)
- feat(cli): overhaul thinking UI by @keithguerin in
[#18725](https://github.com/google-gemini/gemini-cli/pull/18725)
- fix(ui): unify Ctrl+O expansion hint experience across buffer modes by
@jwhelangoog in
[#21474](https://github.com/google-gemini/gemini-cli/pull/21474)
- fix(cli): correct shell height reporting by @jacob314 in
[#21492](https://github.com/google-gemini/gemini-cli/pull/21492)
- Make test suite pass when the GEMINI_SYSTEM_MD env variable or
GEMINI_WRITE_SYSTEM_MD variable happens to be set locally/ by @jacob314 in
[#21480](https://github.com/google-gemini/gemini-cli/pull/21480)
- Disallow underspecified types by @gundermanc in
[#21485](https://github.com/google-gemini/gemini-cli/pull/21485)
- refactor(cli): standardize on 'reload' verb for all components by @keithguerin
in [#20654](https://github.com/google-gemini/gemini-cli/pull/20654)
- feat(cli): Invert quota language to 'percent used' by @keithguerin in
[#20100](https://github.com/google-gemini/gemini-cli/pull/20100)
- Docs: Add documentation for notifications (experimental)(macOS) by @jkcinouye
in [#21163](https://github.com/google-gemini/gemini-cli/pull/21163)
- Code review comments as a pr by @jacob314 in
[#21209](https://github.com/google-gemini/gemini-cli/pull/21209)
- feat(cli): unify /chat and /resume command UX by @LyalinDotCom in
[#20256](https://github.com/google-gemini/gemini-cli/pull/20256)
- docs: fix typo 'allowslisted' -> 'allowlisted' in mcp-server.md by
@Gyanranjan-Priyam in
[#21665](https://github.com/google-gemini/gemini-cli/pull/21665)
- fix(core): display actual graph output in tracker_visualize tool by @anj-s in
[#21455](https://github.com/google-gemini/gemini-cli/pull/21455)
- fix(core): sanitize SSE-corrupted JSON and domain strings in error
classification by @gsquared94 in
[#21702](https://github.com/google-gemini/gemini-cli/pull/21702)
- Docs: Make documentation links relative by @diodesign in
[#21490](https://github.com/google-gemini/gemini-cli/pull/21490)
- feat(cli): expose /tools desc as explicit subcommand for discoverability by
@aworki in [#21241](https://github.com/google-gemini/gemini-cli/pull/21241)
- feat(cli): add /compact alias for /compress command by @jackwotherspoon in
[#21711](https://github.com/google-gemini/gemini-cli/pull/21711)
- feat(plan): enable Plan Mode by default by @jerop in
[#21713](https://github.com/google-gemini/gemini-cli/pull/21713)
- feat(core): Introduce `AgentLoopContext`. by @joshualitt in
[#21198](https://github.com/google-gemini/gemini-cli/pull/21198)
- fix(core): resolve symlinks for non-existent paths during validation by
@Adib234 in [#21487](https://github.com/google-gemini/gemini-cli/pull/21487)
- docs: document tool exclusion from memory via deny policy by @Abhijit-2592 in
[#21428](https://github.com/google-gemini/gemini-cli/pull/21428)
- perf(core): cache loadApiKey to reduce redundant keychain access by @sehoon38
in [#21520](https://github.com/google-gemini/gemini-cli/pull/21520)
- feat(cli): implement /upgrade command by @sehoon38 in
[#21511](https://github.com/google-gemini/gemini-cli/pull/21511)
- Feat/browser agent progress emission by @kunal-10-cloud in
[#21218](https://github.com/google-gemini/gemini-cli/pull/21218)
- fix(settings): display objects as JSON instead of [object Object] by
@Zheyuan-Lin in
[#21458](https://github.com/google-gemini/gemini-cli/pull/21458)
- Unmarshall update by @DavidAPierce in
[#21721](https://github.com/google-gemini/gemini-cli/pull/21721)
- Update mcp's list function to check for disablement. by @DavidAPierce in
[#21148](https://github.com/google-gemini/gemini-cli/pull/21148)
- robustness(core): static checks to validate history is immutable by @jacob314
in [#21228](https://github.com/google-gemini/gemini-cli/pull/21228)
- refactor(cli): better react patterns for BaseSettingsDialog by @psinha40898 in
[#21206](https://github.com/google-gemini/gemini-cli/pull/21206)
- feat(security): implement robust IP validation and safeFetch foundation by
@alisa-alisa in
[#21401](https://github.com/google-gemini/gemini-cli/pull/21401)
- feat(core): improve subagent result display by @joshualitt in
[#20378](https://github.com/google-gemini/gemini-cli/pull/20378)
- docs: fix broken markdown syntax and anchor links in /tools by @campox747 in
[#20902](https://github.com/google-gemini/gemini-cli/pull/20902)
- feat(policy): support subagent-specific policies in TOML by @akh64bit in
[#21431](https://github.com/google-gemini/gemini-cli/pull/21431)
- Add script to speed up reviewing PRs adding a worktree. by @jacob314 in
[#21748](https://github.com/google-gemini/gemini-cli/pull/21748)
- fix(core): prevent infinite recursion in symlink resolution by @Adib234 in
[#21750](https://github.com/google-gemini/gemini-cli/pull/21750)
- fix(docs): fix headless mode docs by @ame2en in
[#21287](https://github.com/google-gemini/gemini-cli/pull/21287)
- feat/redesign header compact by @jacob314 in
[#20922](https://github.com/google-gemini/gemini-cli/pull/20922)
- refactor: migrate to useKeyMatchers hook by @scidomino in
[#21753](https://github.com/google-gemini/gemini-cli/pull/21753)
- perf(cli): cache loadSettings to reduce redundant disk I/O at startup by
@sehoon38 in [#21521](https://github.com/google-gemini/gemini-cli/pull/21521)
- fix(core): resolve Windows line ending and path separation bugs across CLI by
@muhammadusman586 in
[#21068](https://github.com/google-gemini/gemini-cli/pull/21068)
- docs: fix heading formatting in commands.md and phrasing in tools-api.md by
@campox747 in [#20679](https://github.com/google-gemini/gemini-cli/pull/20679)
- refactor(ui): unify keybinding infrastructure and support string
initialization by @scidomino in
[#21776](https://github.com/google-gemini/gemini-cli/pull/21776)
- Add support for updating extension sources and names by @chrstnb in
[#21715](https://github.com/google-gemini/gemini-cli/pull/21715)
- fix(core): handle GUI editor non-zero exit codes gracefully by @reyyanxahmed
in [#20376](https://github.com/google-gemini/gemini-cli/pull/20376)
- fix(core): destroy PTY on kill() and exception to prevent fd leak by @nbardy
in [#21693](https://github.com/google-gemini/gemini-cli/pull/21693)
- fix(docs): update theme screenshots and add missing themes by @ashmod in
[#20689](https://github.com/google-gemini/gemini-cli/pull/20689)
- refactor(cli): rename 'return' key to 'enter' internally by @scidomino in
[#21796](https://github.com/google-gemini/gemini-cli/pull/21796)
- build(release): restrict npm bundling to non-stable tags by @sehoon38 in
[#21821](https://github.com/google-gemini/gemini-cli/pull/21821)
- fix(core): override toolRegistry property for sub-agent schedulers by
@gsquared94 in
[#21766](https://github.com/google-gemini/gemini-cli/pull/21766)
- fix(cli): make footer items equally spaced by @jacob314 in
[#21843](https://github.com/google-gemini/gemini-cli/pull/21843)
- docs: clarify global policy rules application in plan mode by @jerop in
[#21864](https://github.com/google-gemini/gemini-cli/pull/21864)
- fix(core): ensure correct flash model steering in plan mode implementation
phase by @jerop in
[#21871](https://github.com/google-gemini/gemini-cli/pull/21871)
- fix(core): update @a2a-js/sdk to 0.3.11 by @adamfweidman in
[#21875](https://github.com/google-gemini/gemini-cli/pull/21875)
- refactor(core): improve API response error logging when retry by @yunaseoul in
[#21784](https://github.com/google-gemini/gemini-cli/pull/21784)
- fix(ui): handle headless execution in credits and upgrade dialogs by
@gsquared94 in
[#21850](https://github.com/google-gemini/gemini-cli/pull/21850)
- fix(core): treat retryable errors with >5 min delay as terminal quota errors
by @gsquared94 in
[#21881](https://github.com/google-gemini/gemini-cli/pull/21881)
- feat(telemetry): add specific PR, issue, and custom tracking IDs for GitHub
Actions by @cocosheng-g in
[#21129](https://github.com/google-gemini/gemini-cli/pull/21129)
- feat(core): add OAuth2 Authorization Code auth provider for A2A agents by
@SandyTao520 in
[#21496](https://github.com/google-gemini/gemini-cli/pull/21496)
- feat(cli): give visibility to /tools list command in the TUI and follow the
subcommand pattern of other commands by @JayadityaGit in
[#21213](https://github.com/google-gemini/gemini-cli/pull/21213)
- Handle dirty worktrees better and warn about running scripts/review.sh on
untrusted code. by @jacob314 in
[#21791](https://github.com/google-gemini/gemini-cli/pull/21791)
- feat(policy): support auto-add to policy by default and scoped persistence by
@spencer426 in
[#20361](https://github.com/google-gemini/gemini-cli/pull/20361)
- fix(core): handle AbortError when ESC cancels tool execution by @PrasannaPal21
in [#20863](https://github.com/google-gemini/gemini-cli/pull/20863)
- fix(release): Improve Patch Release Workflow Comments: Clearer Approval
Guidance by @jerop in
[#21894](https://github.com/google-gemini/gemini-cli/pull/21894)
- docs: clarify telemetry setup and comprehensive data map by @jerop in
[#21879](https://github.com/google-gemini/gemini-cli/pull/21879)
- feat(core): add per-model token usage to stream-json output by @yongruilin in
[#21839](https://github.com/google-gemini/gemini-cli/pull/21839)
- docs: remove experimental badge from plan mode in sidebar by @jerop in
[#21906](https://github.com/google-gemini/gemini-cli/pull/21906)
- fix(cli): prevent race condition in loop detection retry by @skyvanguard in
[#17916](https://github.com/google-gemini/gemini-cli/pull/17916)
- Add behavioral evals for tracker by @anj-s in
[#20069](https://github.com/google-gemini/gemini-cli/pull/20069)
- fix(auth): update terminology to 'sign in' and 'sign out' by @clocky in
[#20892](https://github.com/google-gemini/gemini-cli/pull/20892)
- docs(mcp): standardize mcp tool fqn documentation by @abhipatel12 in
[#21664](https://github.com/google-gemini/gemini-cli/pull/21664)
- fix(ui): prevent empty tool-group border stubs after filtering by @Aaxhirrr in
[#21852](https://github.com/google-gemini/gemini-cli/pull/21852)
- make command names consistent by @scidomino in
[#21907](https://github.com/google-gemini/gemini-cli/pull/21907)
- refactor: remove agent_card_requires_auth config flag by @adamfweidman in
[#21914](https://github.com/google-gemini/gemini-cli/pull/21914)
- feat(a2a): implement standardized normalization and streaming reassembly by
@alisa-alisa in
[#21402](https://github.com/google-gemini/gemini-cli/pull/21402)
- feat(cli): enable skill activation via slash commands by @NTaylorMullen in
[#21758](https://github.com/google-gemini/gemini-cli/pull/21758)
- docs(cli): mention per-model token usage in stream-json result event by
@yongruilin in
[#21908](https://github.com/google-gemini/gemini-cli/pull/21908)
- fix(plan): prevent plan truncation in approval dialog by supporting
unconstrained heights by @Adib234 in
[#21037](https://github.com/google-gemini/gemini-cli/pull/21037)
- feat(a2a): switch from callback-based to event-driven tool scheduler by
@cocosheng-g in
[#21467](https://github.com/google-gemini/gemini-cli/pull/21467)
- feat(voice): implement speech-friendly response formatter by @ayush31010 in
[#20989](https://github.com/google-gemini/gemini-cli/pull/20989)
- feat: add pulsating blue border automation overlay to browser agent by
@kunal-10-cloud in
[#21173](https://github.com/google-gemini/gemini-cli/pull/21173)
- Add extensionRegistryURI setting to change where the registry is read from by
@kevinjwang1 in
[#20463](https://github.com/google-gemini/gemini-cli/pull/20463)
- fix: patch gaxios v7 Array.toString() stream corruption by @gsquared94 in
[#21884](https://github.com/google-gemini/gemini-cli/pull/21884)
- fix: prevent hangs in non-interactive mode and improve agent guidance by
@cocosheng-g in
[#20893](https://github.com/google-gemini/gemini-cli/pull/20893)
- Add ExtensionDetails dialog and support install by @chrstnb in
[#20845](https://github.com/google-gemini/gemini-cli/pull/20845)
- chore/release: bump version to 0.34.0-nightly.20260310.4653b126f by
@gemini-cli-robot in
[#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
@gemini-cli-robot in
[#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
- fix(patch): cherry-pick 0135b03 to release/v0.33.0-preview.2-pr-21171
[#21816](https://github.com/google-gemini/gemini-cli/pull/21816)
- Changelog for v0.33.0-preview.13 by @gemini-cli-robot in
[#21927](https://github.com/google-gemini/gemini-cli/pull/21927)
- fix(cli): stabilize prompt layout to prevent jumping when typing by
@NTaylorMullen in
[#21081](https://github.com/google-gemini/gemini-cli/pull/21081)
- fix: preserve prompt text when cancelling streaming by @Nixxx19 in
[#21103](https://github.com/google-gemini/gemini-cli/pull/21103)
- fix: robust UX for remote agent errors by @Shyam-Raghuwanshi in
[#20307](https://github.com/google-gemini/gemini-cli/pull/20307)
- feat: implement background process logging and cleanup by @galz10 in
[#21189](https://github.com/google-gemini/gemini-cli/pull/21189)
- Changelog for v0.33.0-preview.14 by @gemini-cli-robot in
[#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
[CONFLICTS] by @gemini-cli-robot in
[#21336](https://github.com/google-gemini/gemini-cli/pull/21336)
- fix(patch): cherry-pick 7ec477d to release/v0.33.0-preview.3-pr-21305 to patch
version v0.33.0-preview.3 and create version 0.33.0-preview.4 by
[#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
- fix(patch): cherry-pick 8432bce to release/v0.34.0-preview.1-pr-22069 to patch
version v0.34.0-preview.1 and create version 0.34.0-preview.2 by
@gemini-cli-robot in
[#21349](https://github.com/google-gemini/gemini-cli/pull/21349)
- fix(patch): cherry-pick 931e668 to release/v0.33.0-preview.4-pr-21425
[CONFLICTS] by @gemini-cli-robot in
[#21478](https://github.com/google-gemini/gemini-cli/pull/21478)
- fix(patch): cherry-pick 7837194 to release/v0.33.0-preview.5-pr-21487 to patch
version v0.33.0-preview.5 and create version 0.33.0-preview.6 by
[#22205](https://github.com/google-gemini/gemini-cli/pull/22205)
- fix(patch): cherry-pick 24adacd to release/v0.34.0-preview.2-pr-22332 to patch
version v0.34.0-preview.2 and create version 0.34.0-preview.3 by
@gemini-cli-robot in
[#21720](https://github.com/google-gemini/gemini-cli/pull/21720)
- fix(patch): cherry-pick 4f4431e to release/v0.33.0-preview.7-pr-21750 to patch
version v0.33.0-preview.7 and create version 0.33.0-preview.8 by
[#22391](https://github.com/google-gemini/gemini-cli/pull/22391)
- fix(patch): cherry-pick 48130eb to release/v0.34.0-preview.3-pr-22665 to patch
version v0.34.0-preview.3 and create version 0.34.0-preview.4 by
@gemini-cli-robot in
[#21782](https://github.com/google-gemini/gemini-cli/pull/21782)
- fix(patch): cherry-pick 9a74271 to release/v0.33.0-preview.8-pr-21236
[CONFLICTS] by @gemini-cli-robot in
[#21788](https://github.com/google-gemini/gemini-cli/pull/21788)
- fix(patch): cherry-pick 936f624 to release/v0.33.0-preview.9-pr-21702 to patch
version v0.33.0-preview.9 and create version 0.33.0-preview.10 by
@gemini-cli-robot in
[#21800](https://github.com/google-gemini/gemini-cli/pull/21800)
- fix(patch): cherry-pick 35ee2a8 to release/v0.33.0-preview.10-pr-21713 by
@gemini-cli-robot in
[#21859](https://github.com/google-gemini/gemini-cli/pull/21859)
- fix(patch): cherry-pick 5dd2dab to release/v0.33.0-preview.11-pr-21871 by
@gemini-cli-robot in
[#21876](https://github.com/google-gemini/gemini-cli/pull/21876)
- fix(patch): cherry-pick e5615f4 to release/v0.33.0-preview.12-pr-21037 to
patch version v0.33.0-preview.12 and create version 0.33.0-preview.13 by
@gemini-cli-robot in
[#21922](https://github.com/google-gemini/gemini-cli/pull/21922)
- fix(patch): cherry-pick 1b69637 to release/v0.33.0-preview.13-pr-21467
[CONFLICTS] by @gemini-cli-robot in
[#21930](https://github.com/google-gemini/gemini-cli/pull/21930)
- fix(patch): cherry-pick 3ff68a9 to release/v0.33.0-preview.14-pr-21884
[CONFLICTS] by @gemini-cli-robot in
[#21952](https://github.com/google-gemini/gemini-cli/pull/21952)
[#22719](https://github.com/google-gemini/gemini-cli/pull/22719)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.32.1...v0.33.1
https://github.com/google-gemini/gemini-cli/compare/v0.33.2...v0.34.0
+354 -449
View File
@@ -1,6 +1,6 @@
# Preview release: v0.34.0-preview.1
# Preview release: v0.35.0-preview.1
Released: March 12, 2026
Released: March 17, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -13,459 +13,364 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Plan Mode Enabled by Default:** Plan Mode is now enabled out-of-the-box,
providing a structured planning workflow and keeping approved plans during
chat compression.
- **Sandboxing Enhancements:** Added experimental LXC container sandbox support
and native gVisor (`runsc`) sandboxing for improved security and isolation.
- **Tracker Visualization and Tools:** Introduced CRUD tools and visualization
for trackers, along with task tracker strategy improvements.
- **Browser Agent Improvements:** Enhanced the browser agent with progress
emission, a new automation overlay, and additional integration tests.
- **CLI and UI Updates:** Standardized semantic focus colors, polished shell
autocomplete rendering, unified keybinding infrastructure, and added custom
footer configuration options.
- **Subagents & Architecture Enhancements**: Enabled subagents and laid the
foundation for subagent tool isolation. Added proxy routing support for remote
A2A subagents and integrated `SandboxManager` to sandbox all process-spawning
tools.
- **CLI & UI Improvements**: Introduced customizable keyboard shortcuts and
support for literal character keybindings. Added missing vim mode motions and
CJK input support. Enabled code splitting and deferred UI loading for improved
performance.
- **Context & Tools Optimization**: JIT context loading is now enabled by
default with deduplication for project memory. Introduced a model-driven
parallel tool scheduler and allowed safe tools to execute concurrently.
- **Security & Extensions**: Implemented cryptographic integrity verification
for extension updates and added a `disableAlwaysAllow` setting to prevent
auto-approvals for enhanced security.
- **Plan Mode & Web Fetch Updates**: Added an 'All the above' option for
multi-select AskUser questions in Plan Mode. Rolled out Stage 1 and Stage 2
security and consistency improvements for the `web_fetch` tool.
## What's Changed
- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
[CONFLICTS] by @gemini-cli-robot in
[#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
- feat(cli): add chat resume footer on session quit by @lordshashank in
[#20667](https://github.com/google-gemini/gemini-cli/pull/20667)
- Support bold and other styles in svg snapshots by @jacob314 in
[#20937](https://github.com/google-gemini/gemini-cli/pull/20937)
- fix(core): increase A2A agent timeout to 30 minutes by @adamfweidman in
[#21028](https://github.com/google-gemini/gemini-cli/pull/21028)
- Cleanup old branches. by @jacob314 in
[#19354](https://github.com/google-gemini/gemini-cli/pull/19354)
- chore(release): bump version to 0.34.0-nightly.20260303.34f0c1538 by
- feat(cli): customizable keyboard shortcuts by @scidomino in
[#21945](https://github.com/google-gemini/gemini-cli/pull/21945)
- feat(core): Thread `AgentLoopContext` through core. by @joshualitt in
[#21944](https://github.com/google-gemini/gemini-cli/pull/21944)
- chore(release): bump version to 0.35.0-nightly.20260311.657f19c1f by
@gemini-cli-robot in
[#21034](https://github.com/google-gemini/gemini-cli/pull/21034)
- feat(ui): standardize semantic focus colors and enhance history visibility by
@keithguerin in
[#20745](https://github.com/google-gemini/gemini-cli/pull/20745)
- fix: merge duplicate imports in packages/core (3/4) by @Nixxx19 in
[#20928](https://github.com/google-gemini/gemini-cli/pull/20928)
- Add extra safety checks for proto pollution by @jacob314 in
[#20396](https://github.com/google-gemini/gemini-cli/pull/20396)
- feat(core): Add tracker CRUD tools & visualization by @anj-s in
[#19489](https://github.com/google-gemini/gemini-cli/pull/19489)
- Revert "fix(ui): persist expansion in AskUser dialog when navigating options"
by @jacob314 in
[#21042](https://github.com/google-gemini/gemini-cli/pull/21042)
- Changelog for v0.33.0-preview.0 by @gemini-cli-robot in
[#21030](https://github.com/google-gemini/gemini-cli/pull/21030)
- fix: model persistence for all scenarios by @sripasg in
[#21051](https://github.com/google-gemini/gemini-cli/pull/21051)
- chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 by
@gemini-cli-robot in
[#21054](https://github.com/google-gemini/gemini-cli/pull/21054)
- Consistently guard restarts against concurrent auto updates by @scidomino in
[#21016](https://github.com/google-gemini/gemini-cli/pull/21016)
- Defensive coding to reduce the risk of Maximum update depth errors by
@jacob314 in [#20940](https://github.com/google-gemini/gemini-cli/pull/20940)
- fix(cli): Polish shell autocomplete rendering to be a little more shell native
feeling. by @jacob314 in
[#20931](https://github.com/google-gemini/gemini-cli/pull/20931)
- Docs: Update plan mode docs by @jkcinouye in
[#19682](https://github.com/google-gemini/gemini-cli/pull/19682)
- fix(mcp): Notifications/tools/list_changed support not working by @jacob314 in
[#21050](https://github.com/google-gemini/gemini-cli/pull/21050)
- fix(cli): register extension lifecycle events in DebugProfiler by
@fayerman-source in
[#20101](https://github.com/google-gemini/gemini-cli/pull/20101)
- chore(dev): update vscode settings for typescriptreact by @rohit-4321 in
[#19907](https://github.com/google-gemini/gemini-cli/pull/19907)
- fix(cli): enable multi-arch docker builds for sandbox by @ru-aish in
[#19821](https://github.com/google-gemini/gemini-cli/pull/19821)
- Changelog for v0.32.0 by @gemini-cli-robot in
[#21033](https://github.com/google-gemini/gemini-cli/pull/21033)
- Changelog for v0.33.0-preview.1 by @gemini-cli-robot in
[#21058](https://github.com/google-gemini/gemini-cli/pull/21058)
- feat(core): improve @scripts/copy_files.js autocomplete to prioritize
filenames by @sehoon38 in
[#21064](https://github.com/google-gemini/gemini-cli/pull/21064)
- feat(sandbox): add experimental LXC container sandbox support by @h30s in
[#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
- feat(evals): add overall pass rate row to eval nightly summary table by
@gundermanc in
[#20905](https://github.com/google-gemini/gemini-cli/pull/20905)
- feat(telemetry): include language in telemetry and fix accepted lines
computation by @gundermanc in
[#21126](https://github.com/google-gemini/gemini-cli/pull/21126)
- Changelog for v0.32.1 by @gemini-cli-robot in
[#21055](https://github.com/google-gemini/gemini-cli/pull/21055)
- feat(core): add robustness tests, logging, and metrics for CodeAssistServer
SSE parsing by @yunaseoul in
[#21013](https://github.com/google-gemini/gemini-cli/pull/21013)
- feat: add issue assignee workflow by @kartikangiras in
[#21003](https://github.com/google-gemini/gemini-cli/pull/21003)
- fix: improve error message when OAuth succeeds but project ID is required by
@Nixxx19 in [#21070](https://github.com/google-gemini/gemini-cli/pull/21070)
- feat(loop-reduction): implement iterative loop detection and model feedback by
@aishaneeshah in
[#20763](https://github.com/google-gemini/gemini-cli/pull/20763)
- chore(github): require prompt approvers for agent prompt files by @gundermanc
in [#20896](https://github.com/google-gemini/gemini-cli/pull/20896)
- Docs: Create tools reference by @jkcinouye in
[#19470](https://github.com/google-gemini/gemini-cli/pull/19470)
- fix(core, a2a-server): prevent hang during OAuth in non-interactive sessions
by @spencer426 in
[#21045](https://github.com/google-gemini/gemini-cli/pull/21045)
- chore(cli): enable deprecated settings removal by default by @yashodipmore in
[#20682](https://github.com/google-gemini/gemini-cli/pull/20682)
- feat(core): Disable fast ack helper for hints. by @joshualitt in
[#21011](https://github.com/google-gemini/gemini-cli/pull/21011)
- fix(ui): suppress redundant failure note when tool error note is shown by
@NTaylorMullen in
[#21078](https://github.com/google-gemini/gemini-cli/pull/21078)
- docs: document planning workflows with Conductor example by @jerop in
[#21166](https://github.com/google-gemini/gemini-cli/pull/21166)
- feat(release): ship esbuild bundle in npm package by @genneth in
[#19171](https://github.com/google-gemini/gemini-cli/pull/19171)
- fix(extensions): preserve symlinks in extension source path while enforcing
folder trust by @galz10 in
[#20867](https://github.com/google-gemini/gemini-cli/pull/20867)
- fix(cli): defer tool exclusions to policy engine in non-interactive mode by
@EricRahm in [#20639](https://github.com/google-gemini/gemini-cli/pull/20639)
- fix(ui): removed double padding on rendered content by @devr0306 in
[#21029](https://github.com/google-gemini/gemini-cli/pull/21029)
- fix(core): truncate excessively long lines in grep search output by
@gundermanc in
[#21147](https://github.com/google-gemini/gemini-cli/pull/21147)
- feat: add custom footer configuration via `/footer` by @jackwotherspoon in
[#19001](https://github.com/google-gemini/gemini-cli/pull/19001)
- perf(core): fix OOM crash in long-running sessions by @WizardsForgeGames in
[#19608](https://github.com/google-gemini/gemini-cli/pull/19608)
- refactor(cli): categorize built-in themes into dark/ and light/ directories by
@JayadityaGit in
[#18634](https://github.com/google-gemini/gemini-cli/pull/18634)
- fix(core): explicitly allow codebase_investigator and cli_help in read-only
mode by @Adib234 in
[#21157](https://github.com/google-gemini/gemini-cli/pull/21157)
- test: add browser agent integration tests by @kunal-10-cloud in
[#21151](https://github.com/google-gemini/gemini-cli/pull/21151)
- fix(cli): fix enabling kitty codes on Windows Terminal by @scidomino in
[#21136](https://github.com/google-gemini/gemini-cli/pull/21136)
- refactor(core): extract shared OAuth flow primitives from MCPOAuthProvider by
@SandyTao520 in
[#20895](https://github.com/google-gemini/gemini-cli/pull/20895)
- fix(ui): add partial output to cancelled shell UI by @devr0306 in
[#21178](https://github.com/google-gemini/gemini-cli/pull/21178)
- fix(cli): replace hardcoded keybinding strings with dynamic formatters by
@scidomino in [#21159](https://github.com/google-gemini/gemini-cli/pull/21159)
- DOCS: Update quota and pricing page by @g-samroberts in
[#21194](https://github.com/google-gemini/gemini-cli/pull/21194)
- feat(telemetry): implement Clearcut logging for startup statistics by
@yunaseoul in [#21172](https://github.com/google-gemini/gemini-cli/pull/21172)
- feat(triage): add area/documentation to issue triage by @g-samroberts in
[#21222](https://github.com/google-gemini/gemini-cli/pull/21222)
- Fix so shell calls are formatted by @jacob314 in
[#21237](https://github.com/google-gemini/gemini-cli/pull/21237)
- feat(cli): add native gVisor (runsc) sandboxing support by @Zheyuan-Lin in
[#21062](https://github.com/google-gemini/gemini-cli/pull/21062)
- docs: use absolute paths for internal links in plan-mode.md by @jerop in
[#21299](https://github.com/google-gemini/gemini-cli/pull/21299)
- fix(core): prevent unhandled AbortError crash during stream loop detection by
@7hokerz in [#21123](https://github.com/google-gemini/gemini-cli/pull/21123)
- fix:reorder env var redaction checks to scan values first by @kartikangiras in
[#21059](https://github.com/google-gemini/gemini-cli/pull/21059)
- fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences
by @skeshive in
[#21171](https://github.com/google-gemini/gemini-cli/pull/21171)
- feat(core): fallback to 2.5 models with no access for toolcalls by @sehoon38
in [#21283](https://github.com/google-gemini/gemini-cli/pull/21283)
- test(core): improve testing for API request/response parsing by @sehoon38 in
[#21227](https://github.com/google-gemini/gemini-cli/pull/21227)
- docs(links): update docs-writer skill and fix broken link by @g-samroberts in
[#21314](https://github.com/google-gemini/gemini-cli/pull/21314)
- Fix code colorizer ansi escape bug. by @jacob314 in
[#21321](https://github.com/google-gemini/gemini-cli/pull/21321)
- remove wildcard behavior on keybindings by @scidomino in
[#21315](https://github.com/google-gemini/gemini-cli/pull/21315)
- feat(acp): Add support for AI Gateway auth by @skeshive in
[#21305](https://github.com/google-gemini/gemini-cli/pull/21305)
- fix(theme): improve theme color contrast for macOS Terminal.app by @clocky in
[#21175](https://github.com/google-gemini/gemini-cli/pull/21175)
- feat (core): Implement tracker related SI changes by @anj-s in
[#19964](https://github.com/google-gemini/gemini-cli/pull/19964)
- Changelog for v0.33.0-preview.2 by @gemini-cli-robot in
[#21333](https://github.com/google-gemini/gemini-cli/pull/21333)
- Changelog for v0.33.0-preview.3 by @gemini-cli-robot in
[#21347](https://github.com/google-gemini/gemini-cli/pull/21347)
- docs: format release times as HH:MM UTC by @pavan-sh in
[#20726](https://github.com/google-gemini/gemini-cli/pull/20726)
- fix(cli): implement --all flag for extensions uninstall by @sehoon38 in
[#21319](https://github.com/google-gemini/gemini-cli/pull/21319)
- docs: fix incorrect relative links to command reference by @kanywst in
[#20964](https://github.com/google-gemini/gemini-cli/pull/20964)
- documentiong ensures ripgrep by @Jatin24062005 in
[#21298](https://github.com/google-gemini/gemini-cli/pull/21298)
- fix(core): handle AbortError thrown during processTurn by @MumuTW in
[#21296](https://github.com/google-gemini/gemini-cli/pull/21296)
- docs(cli): clarify ! command output visibility in shell commands tutorial by
@MohammedADev in
[#21041](https://github.com/google-gemini/gemini-cli/pull/21041)
- fix: logic for task tracker strategy and remove tracker tools by @anj-s in
[#21355](https://github.com/google-gemini/gemini-cli/pull/21355)
- fix(partUtils): display media type and size for inline data parts by @Aboudjem
in [#21358](https://github.com/google-gemini/gemini-cli/pull/21358)
- Fix(accessibility): add screen reader support to RewindViewer by @Famous077 in
[#20750](https://github.com/google-gemini/gemini-cli/pull/20750)
- fix(hooks): propagate stopHookActive in AfterAgent retry path (#20426) by
@Aarchi-07 in [#20439](https://github.com/google-gemini/gemini-cli/pull/20439)
- fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive
filesystems (#19904) by @Nixxx19 in
[#19915](https://github.com/google-gemini/gemini-cli/pull/19915)
- feat(core): add concurrency safety guidance for subagent delegation (#17753)
by @abhipatel12 in
[#21278](https://github.com/google-gemini/gemini-cli/pull/21278)
- feat(ui): dynamically generate all keybinding hints by @scidomino in
[#21346](https://github.com/google-gemini/gemini-cli/pull/21346)
- feat(core): implement unified KeychainService and migrate token storage by
@ehedlund in [#21344](https://github.com/google-gemini/gemini-cli/pull/21344)
- fix(cli): gracefully handle --resume when no sessions exist by @SandyTao520 in
[#21429](https://github.com/google-gemini/gemini-cli/pull/21429)
- fix(plan): keep approved plan during chat compression by @ruomengz in
[#21284](https://github.com/google-gemini/gemini-cli/pull/21284)
- feat(core): implement generic CacheService and optimize setupUser by @sehoon38
in [#21374](https://github.com/google-gemini/gemini-cli/pull/21374)
- Update quota and pricing documentation with subscription tiers by @srithreepo
in [#21351](https://github.com/google-gemini/gemini-cli/pull/21351)
- fix(core): append correct OTLP paths for HTTP exporters by
@sebastien-prudhomme in
[#16836](https://github.com/google-gemini/gemini-cli/pull/16836)
- Changelog for v0.33.0-preview.4 by @gemini-cli-robot in
[#21354](https://github.com/google-gemini/gemini-cli/pull/21354)
- feat(cli): implement dot-prefixing for slash command conflicts by @ehedlund in
[#20979](https://github.com/google-gemini/gemini-cli/pull/20979)
- refactor(core): standardize MCP tool naming to mcp\_ FQN format by
@abhipatel12 in
[#21425](https://github.com/google-gemini/gemini-cli/pull/21425)
- feat(cli): hide gemma settings from display and mark as experimental by
@abhipatel12 in
[#21471](https://github.com/google-gemini/gemini-cli/pull/21471)
- feat(skills): refine string-reviewer guidelines and description by @clocky in
[#20368](https://github.com/google-gemini/gemini-cli/pull/20368)
- fix(core): whitelist TERM and COLORTERM in environment sanitization by
@deadsmash07 in
[#20514](https://github.com/google-gemini/gemini-cli/pull/20514)
- fix(billing): fix overage strategy lifecycle and settings integration by
@gsquared94 in
[#21236](https://github.com/google-gemini/gemini-cli/pull/21236)
- fix: expand paste placeholders in TextInput on submit by @Jefftree in
[#19946](https://github.com/google-gemini/gemini-cli/pull/19946)
- fix(core): add in-memory cache to ChatRecordingService to prevent OOM by
@SandyTao520 in
[#21502](https://github.com/google-gemini/gemini-cli/pull/21502)
- feat(cli): overhaul thinking UI by @keithguerin in
[#18725](https://github.com/google-gemini/gemini-cli/pull/18725)
- fix(ui): unify Ctrl+O expansion hint experience across buffer modes by
@jwhelangoog in
[#21474](https://github.com/google-gemini/gemini-cli/pull/21474)
- fix(cli): correct shell height reporting by @jacob314 in
[#21492](https://github.com/google-gemini/gemini-cli/pull/21492)
- Make test suite pass when the GEMINI_SYSTEM_MD env variable or
GEMINI_WRITE_SYSTEM_MD variable happens to be set locally/ by @jacob314 in
[#21480](https://github.com/google-gemini/gemini-cli/pull/21480)
- Disallow underspecified types by @gundermanc in
[#21485](https://github.com/google-gemini/gemini-cli/pull/21485)
- refactor(cli): standardize on 'reload' verb for all components by @keithguerin
in [#20654](https://github.com/google-gemini/gemini-cli/pull/20654)
- feat(cli): Invert quota language to 'percent used' by @keithguerin in
[#20100](https://github.com/google-gemini/gemini-cli/pull/20100)
- Docs: Add documentation for notifications (experimental)(macOS) by @jkcinouye
in [#21163](https://github.com/google-gemini/gemini-cli/pull/21163)
- Code review comments as a pr by @jacob314 in
[#21209](https://github.com/google-gemini/gemini-cli/pull/21209)
- feat(cli): unify /chat and /resume command UX by @LyalinDotCom in
[#20256](https://github.com/google-gemini/gemini-cli/pull/20256)
- docs: fix typo 'allowslisted' -> 'allowlisted' in mcp-server.md by
[#21966](https://github.com/google-gemini/gemini-cli/pull/21966)
- refactor(a2a): remove legacy CoreToolScheduler by @adamfweidman in
[#21955](https://github.com/google-gemini/gemini-cli/pull/21955)
- feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)
by @aanari in [#21932](https://github.com/google-gemini/gemini-cli/pull/21932)
- Feat/retry fetch notifications by @aishaneeshah in
[#21813](https://github.com/google-gemini/gemini-cli/pull/21813)
- fix(core): remove OAuth check from handleFallback and clean up stray file by
@sehoon38 in [#21962](https://github.com/google-gemini/gemini-cli/pull/21962)
- feat(cli): support literal character keybindings and extended Kitty protocol
keys by @scidomino in
[#21972](https://github.com/google-gemini/gemini-cli/pull/21972)
- fix(ui): clamp cursor to last char after all NORMAL mode deletes by @aanari in
[#21973](https://github.com/google-gemini/gemini-cli/pull/21973)
- test(core): add missing tests for prompts/utils.ts by @krrishverma1805-web in
[#19941](https://github.com/google-gemini/gemini-cli/pull/19941)
- fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) by
@nsalerni in [#19933](https://github.com/google-gemini/gemini-cli/pull/19933)
- docs(cli): add custom keybinding documentation by @scidomino in
[#21980](https://github.com/google-gemini/gemini-cli/pull/21980)
- docs: fix misleading YOLO mode description in defaultApprovalMode by
@Gyanranjan-Priyam in
[#21665](https://github.com/google-gemini/gemini-cli/pull/21665)
- fix(core): display actual graph output in tracker_visualize tool by @anj-s in
[#21455](https://github.com/google-gemini/gemini-cli/pull/21455)
- fix(core): sanitize SSE-corrupted JSON and domain strings in error
classification by @gsquared94 in
[#21702](https://github.com/google-gemini/gemini-cli/pull/21702)
- Docs: Make documentation links relative by @diodesign in
[#21490](https://github.com/google-gemini/gemini-cli/pull/21490)
- feat(cli): expose /tools desc as explicit subcommand for discoverability by
@aworki in [#21241](https://github.com/google-gemini/gemini-cli/pull/21241)
- feat(cli): add /compact alias for /compress command by @jackwotherspoon in
[#21711](https://github.com/google-gemini/gemini-cli/pull/21711)
- feat(plan): enable Plan Mode by default by @jerop in
[#21713](https://github.com/google-gemini/gemini-cli/pull/21713)
- feat(core): Introduce `AgentLoopContext`. by @joshualitt in
[#21198](https://github.com/google-gemini/gemini-cli/pull/21198)
- fix(core): resolve symlinks for non-existent paths during validation by
@Adib234 in [#21487](https://github.com/google-gemini/gemini-cli/pull/21487)
- docs: document tool exclusion from memory via deny policy by @Abhijit-2592 in
[#21428](https://github.com/google-gemini/gemini-cli/pull/21428)
- perf(core): cache loadApiKey to reduce redundant keychain access by @sehoon38
in [#21520](https://github.com/google-gemini/gemini-cli/pull/21520)
- feat(cli): implement /upgrade command by @sehoon38 in
[#21511](https://github.com/google-gemini/gemini-cli/pull/21511)
- Feat/browser agent progress emission by @kunal-10-cloud in
[#21218](https://github.com/google-gemini/gemini-cli/pull/21218)
- fix(settings): display objects as JSON instead of [object Object] by
@Zheyuan-Lin in
[#21458](https://github.com/google-gemini/gemini-cli/pull/21458)
- Unmarshall update by @DavidAPierce in
[#21721](https://github.com/google-gemini/gemini-cli/pull/21721)
- Update mcp's list function to check for disablement. by @DavidAPierce in
[#21148](https://github.com/google-gemini/gemini-cli/pull/21148)
- robustness(core): static checks to validate history is immutable by @jacob314
in [#21228](https://github.com/google-gemini/gemini-cli/pull/21228)
- refactor(cli): better react patterns for BaseSettingsDialog by @psinha40898 in
[#21206](https://github.com/google-gemini/gemini-cli/pull/21206)
- feat(security): implement robust IP validation and safeFetch foundation by
@alisa-alisa in
[#21401](https://github.com/google-gemini/gemini-cli/pull/21401)
- feat(core): improve subagent result display by @joshualitt in
[#20378](https://github.com/google-gemini/gemini-cli/pull/20378)
- docs: fix broken markdown syntax and anchor links in /tools by @campox747 in
[#20902](https://github.com/google-gemini/gemini-cli/pull/20902)
- feat(policy): support subagent-specific policies in TOML by @akh64bit in
[#21431](https://github.com/google-gemini/gemini-cli/pull/21431)
- Add script to speed up reviewing PRs adding a worktree. by @jacob314 in
[#21748](https://github.com/google-gemini/gemini-cli/pull/21748)
- fix(core): prevent infinite recursion in symlink resolution by @Adib234 in
[#21750](https://github.com/google-gemini/gemini-cli/pull/21750)
- fix(docs): fix headless mode docs by @ame2en in
[#21287](https://github.com/google-gemini/gemini-cli/pull/21287)
- feat/redesign header compact by @jacob314 in
[#20922](https://github.com/google-gemini/gemini-cli/pull/20922)
- refactor: migrate to useKeyMatchers hook by @scidomino in
[#21753](https://github.com/google-gemini/gemini-cli/pull/21753)
- perf(cli): cache loadSettings to reduce redundant disk I/O at startup by
@sehoon38 in [#21521](https://github.com/google-gemini/gemini-cli/pull/21521)
- fix(core): resolve Windows line ending and path separation bugs across CLI by
@muhammadusman586 in
[#21068](https://github.com/google-gemini/gemini-cli/pull/21068)
- docs: fix heading formatting in commands.md and phrasing in tools-api.md by
@campox747 in [#20679](https://github.com/google-gemini/gemini-cli/pull/20679)
- refactor(ui): unify keybinding infrastructure and support string
initialization by @scidomino in
[#21776](https://github.com/google-gemini/gemini-cli/pull/21776)
- Add support for updating extension sources and names by @chrstnb in
[#21715](https://github.com/google-gemini/gemini-cli/pull/21715)
- fix(core): handle GUI editor non-zero exit codes gracefully by @reyyanxahmed
in [#20376](https://github.com/google-gemini/gemini-cli/pull/20376)
- fix(core): destroy PTY on kill() and exception to prevent fd leak by @nbardy
in [#21693](https://github.com/google-gemini/gemini-cli/pull/21693)
- fix(docs): update theme screenshots and add missing themes by @ashmod in
[#20689](https://github.com/google-gemini/gemini-cli/pull/20689)
- refactor(cli): rename 'return' key to 'enter' internally by @scidomino in
[#21796](https://github.com/google-gemini/gemini-cli/pull/21796)
- build(release): restrict npm bundling to non-stable tags by @sehoon38 in
[#21821](https://github.com/google-gemini/gemini-cli/pull/21821)
- fix(core): override toolRegistry property for sub-agent schedulers by
@gsquared94 in
[#21766](https://github.com/google-gemini/gemini-cli/pull/21766)
- fix(cli): make footer items equally spaced by @jacob314 in
[#21843](https://github.com/google-gemini/gemini-cli/pull/21843)
- docs: clarify global policy rules application in plan mode by @jerop in
[#21864](https://github.com/google-gemini/gemini-cli/pull/21864)
- fix(core): ensure correct flash model steering in plan mode implementation
phase by @jerop in
[#21871](https://github.com/google-gemini/gemini-cli/pull/21871)
- fix(core): update @a2a-js/sdk to 0.3.11 by @adamfweidman in
[#21875](https://github.com/google-gemini/gemini-cli/pull/21875)
- refactor(core): improve API response error logging when retry by @yunaseoul in
[#21784](https://github.com/google-gemini/gemini-cli/pull/21784)
- fix(ui): handle headless execution in credits and upgrade dialogs by
@gsquared94 in
[#21850](https://github.com/google-gemini/gemini-cli/pull/21850)
- fix(core): treat retryable errors with >5 min delay as terminal quota errors
by @gsquared94 in
[#21881](https://github.com/google-gemini/gemini-cli/pull/21881)
- feat(telemetry): add specific PR, issue, and custom tracking IDs for GitHub
Actions by @cocosheng-g in
[#21129](https://github.com/google-gemini/gemini-cli/pull/21129)
- feat(core): add OAuth2 Authorization Code auth provider for A2A agents by
@SandyTao520 in
[#21496](https://github.com/google-gemini/gemini-cli/pull/21496)
- feat(cli): give visibility to /tools list command in the TUI and follow the
subcommand pattern of other commands by @JayadityaGit in
[#21213](https://github.com/google-gemini/gemini-cli/pull/21213)
- Handle dirty worktrees better and warn about running scripts/review.sh on
untrusted code. by @jacob314 in
[#21791](https://github.com/google-gemini/gemini-cli/pull/21791)
- feat(policy): support auto-add to policy by default and scoped persistence by
[#21878](https://github.com/google-gemini/gemini-cli/pull/21878)
- fix: clean up /clear and /resume by @jackwotherspoon in
[#22007](https://github.com/google-gemini/gemini-cli/pull/22007)
- fix(core)#20941: reap orphaned descendant processes on PTY abort by @manavmax
in [#21124](https://github.com/google-gemini/gemini-cli/pull/21124)
- fix(core): update language detection to use LSP 3.18 identifiers by @yunaseoul
in [#21931](https://github.com/google-gemini/gemini-cli/pull/21931)
- feat(cli): support removing keybindings via '-' prefix by @scidomino in
[#22042](https://github.com/google-gemini/gemini-cli/pull/22042)
- feat(policy): add --admin-policy flag for supplemental admin policies by
@galz10 in [#20360](https://github.com/google-gemini/gemini-cli/pull/20360)
- merge duplicate imports packages/cli/src subtask1 by @Nixxx19 in
[#22040](https://github.com/google-gemini/gemini-cli/pull/22040)
- perf(core): parallelize user quota and experiments fetching in refreshAuth by
@sehoon38 in [#21648](https://github.com/google-gemini/gemini-cli/pull/21648)
- Changelog for v0.34.0-preview.0 by @gemini-cli-robot in
[#21965](https://github.com/google-gemini/gemini-cli/pull/21965)
- Changelog for v0.33.0 by @gemini-cli-robot in
[#21967](https://github.com/google-gemini/gemini-cli/pull/21967)
- fix(core): handle EISDIR in robustRealpath on Windows by @sehoon38 in
[#21984](https://github.com/google-gemini/gemini-cli/pull/21984)
- feat(core): include initiationMethod in conversation interaction telemetry by
@yunaseoul in [#22054](https://github.com/google-gemini/gemini-cli/pull/22054)
- feat(ui): add vim yank/paste (y/p/P) with unnamed register by @aanari in
[#22026](https://github.com/google-gemini/gemini-cli/pull/22026)
- fix(core): enable numerical routing for api key users by @sehoon38 in
[#21977](https://github.com/google-gemini/gemini-cli/pull/21977)
- feat(telemetry): implement retry attempt telemetry for network related retries
by @aishaneeshah in
[#22027](https://github.com/google-gemini/gemini-cli/pull/22027)
- fix(policy): remove unnecessary escapeRegex from pattern builders by
@spencer426 in
[#20361](https://github.com/google-gemini/gemini-cli/pull/20361)
- fix(core): handle AbortError when ESC cancels tool execution by @PrasannaPal21
in [#20863](https://github.com/google-gemini/gemini-cli/pull/20863)
- fix(release): Improve Patch Release Workflow Comments: Clearer Approval
Guidance by @jerop in
[#21894](https://github.com/google-gemini/gemini-cli/pull/21894)
- docs: clarify telemetry setup and comprehensive data map by @jerop in
[#21879](https://github.com/google-gemini/gemini-cli/pull/21879)
- feat(core): add per-model token usage to stream-json output by @yongruilin in
[#21839](https://github.com/google-gemini/gemini-cli/pull/21839)
- docs: remove experimental badge from plan mode in sidebar by @jerop in
[#21906](https://github.com/google-gemini/gemini-cli/pull/21906)
- fix(cli): prevent race condition in loop detection retry by @skyvanguard in
[#17916](https://github.com/google-gemini/gemini-cli/pull/17916)
- Add behavioral evals for tracker by @anj-s in
[#20069](https://github.com/google-gemini/gemini-cli/pull/20069)
- fix(auth): update terminology to 'sign in' and 'sign out' by @clocky in
[#20892](https://github.com/google-gemini/gemini-cli/pull/20892)
- docs(mcp): standardize mcp tool fqn documentation by @abhipatel12 in
[#21664](https://github.com/google-gemini/gemini-cli/pull/21664)
- fix(ui): prevent empty tool-group border stubs after filtering by @Aaxhirrr in
[#21852](https://github.com/google-gemini/gemini-cli/pull/21852)
- make command names consistent by @scidomino in
[#21907](https://github.com/google-gemini/gemini-cli/pull/21907)
- refactor: remove agent_card_requires_auth config flag by @adamfweidman in
[#21914](https://github.com/google-gemini/gemini-cli/pull/21914)
- feat(a2a): implement standardized normalization and streaming reassembly by
@alisa-alisa in
[#21402](https://github.com/google-gemini/gemini-cli/pull/21402)
- feat(cli): enable skill activation via slash commands by @NTaylorMullen in
[#21758](https://github.com/google-gemini/gemini-cli/pull/21758)
- docs(cli): mention per-model token usage in stream-json result event by
@yongruilin in
[#21908](https://github.com/google-gemini/gemini-cli/pull/21908)
- fix(plan): prevent plan truncation in approval dialog by supporting
unconstrained heights by @Adib234 in
[#21037](https://github.com/google-gemini/gemini-cli/pull/21037)
- feat(a2a): switch from callback-based to event-driven tool scheduler by
@cocosheng-g in
[#21467](https://github.com/google-gemini/gemini-cli/pull/21467)
- feat(voice): implement speech-friendly response formatter by @Solventerritory
in [#20989](https://github.com/google-gemini/gemini-cli/pull/20989)
- feat: add pulsating blue border automation overlay to browser agent by
@kunal-10-cloud in
[#21173](https://github.com/google-gemini/gemini-cli/pull/21173)
- Add extensionRegistryURI setting to change where the registry is read from by
@kevinjwang1 in
[#20463](https://github.com/google-gemini/gemini-cli/pull/20463)
- fix: patch gaxios v7 Array.toString() stream corruption by @gsquared94 in
[#21884](https://github.com/google-gemini/gemini-cli/pull/21884)
- fix: prevent hangs in non-interactive mode and improve agent guidance by
@cocosheng-g in
[#20893](https://github.com/google-gemini/gemini-cli/pull/20893)
- Add ExtensionDetails dialog and support install by @chrstnb in
[#20845](https://github.com/google-gemini/gemini-cli/pull/20845)
- chore/release: bump version to 0.34.0-nightly.20260310.4653b126f by
@gemini-cli-robot in
[#21816](https://github.com/google-gemini/gemini-cli/pull/21816)
- Changelog for v0.33.0-preview.13 by @gemini-cli-robot in
[#21927](https://github.com/google-gemini/gemini-cli/pull/21927)
- fix(cli): stabilize prompt layout to prevent jumping when typing by
[#21921](https://github.com/google-gemini/gemini-cli/pull/21921)
- fix(core): preserve dynamic tool descriptions on session resume by @sehoon38
in [#18835](https://github.com/google-gemini/gemini-cli/pull/18835)
- chore: allow 'gemini-3.1' in sensitive keyword linter by @scidomino in
[#22065](https://github.com/google-gemini/gemini-cli/pull/22065)
- feat(core): support custom base URL via env vars by @junaiddshaukat in
[#21561](https://github.com/google-gemini/gemini-cli/pull/21561)
- merge duplicate imports packages/cli/src subtask2 by @Nixxx19 in
[#22051](https://github.com/google-gemini/gemini-cli/pull/22051)
- fix(core): silently retry API errors up to 3 times before halting session by
@spencer426 in
[#21989](https://github.com/google-gemini/gemini-cli/pull/21989)
- feat(core): simplify subagent success UI and improve early termination display
by @abhipatel12 in
[#21917](https://github.com/google-gemini/gemini-cli/pull/21917)
- merge duplicate imports packages/cli/src subtask3 by @Nixxx19 in
[#22056](https://github.com/google-gemini/gemini-cli/pull/22056)
- fix(hooks): fix BeforeAgent/AfterAgent inconsistencies (#18514) by @krishdef7
in [#21383](https://github.com/google-gemini/gemini-cli/pull/21383)
- feat(core): implement SandboxManager interface and config schema by @galz10 in
[#21774](https://github.com/google-gemini/gemini-cli/pull/21774)
- docs: document npm deprecation warnings as safe to ignore by @h30s in
[#20692](https://github.com/google-gemini/gemini-cli/pull/20692)
- fix: remove status/need-triage from maintainer-only issues by @SandyTao520 in
[#22044](https://github.com/google-gemini/gemini-cli/pull/22044)
- fix(core): propagate subagent context to policy engine by @NTaylorMullen in
[#22086](https://github.com/google-gemini/gemini-cli/pull/22086)
- fix(cli): resolve skill uninstall failure when skill name is updated by
@NTaylorMullen in
[#21081](https://github.com/google-gemini/gemini-cli/pull/21081)
- fix: preserve prompt text when cancelling streaming by @Nixxx19 in
[#21103](https://github.com/google-gemini/gemini-cli/pull/21103)
- fix: robust UX for remote agent errors by @Shyam-Raghuwanshi in
[#20307](https://github.com/google-gemini/gemini-cli/pull/20307)
- feat: implement background process logging and cleanup by @galz10 in
[#21189](https://github.com/google-gemini/gemini-cli/pull/21189)
- Changelog for v0.33.0-preview.14 by @gemini-cli-robot in
[#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
[#22085](https://github.com/google-gemini/gemini-cli/pull/22085)
- docs(plan): clarify interactive plan editing with Ctrl+X by @Adib234 in
[#22076](https://github.com/google-gemini/gemini-cli/pull/22076)
- fix(policy): ensure user policies are loaded when policyPaths is empty by
@NTaylorMullen in
[#22090](https://github.com/google-gemini/gemini-cli/pull/22090)
- Docs: Add documentation for model steering (experimental). by @jkcinouye in
[#21154](https://github.com/google-gemini/gemini-cli/pull/21154)
- Add issue for automated changelogs by @g-samroberts in
[#21912](https://github.com/google-gemini/gemini-cli/pull/21912)
- fix(core): secure argsPattern and revert WEB_FETCH_TOOL_NAME escalation by
@spencer426 in
[#22104](https://github.com/google-gemini/gemini-cli/pull/22104)
- feat(core): differentiate User-Agent for a2a-server and ACP clients by
@bdmorgan in [#22059](https://github.com/google-gemini/gemini-cli/pull/22059)
- refactor(core): extract ExecutionLifecycleService for tool backgrounding by
@adamfweidman in
[#21717](https://github.com/google-gemini/gemini-cli/pull/21717)
- feat: Display pending and confirming tool calls by @sripasg in
[#22106](https://github.com/google-gemini/gemini-cli/pull/22106)
- feat(browser): implement input blocker overlay during automation by
@kunal-10-cloud in
[#21132](https://github.com/google-gemini/gemini-cli/pull/21132)
- fix: register themes on extension load not start by @jackwotherspoon in
[#22148](https://github.com/google-gemini/gemini-cli/pull/22148)
- feat(ui): Do not show Ultra users /upgrade hint (#22154) by @sehoon38 in
[#22156](https://github.com/google-gemini/gemini-cli/pull/22156)
- chore: remove unnecessary log for themes by @jackwotherspoon in
[#22165](https://github.com/google-gemini/gemini-cli/pull/22165)
- fix(core): resolve MCP tool FQN validation, schema export, and wildcards in
subagents by @abhipatel12 in
[#22069](https://github.com/google-gemini/gemini-cli/pull/22069)
- fix(cli): validate --model argument at startup by @JaisalJain in
[#21393](https://github.com/google-gemini/gemini-cli/pull/21393)
- fix(core): handle policy ALLOW for exit_plan_mode by @backnotprop in
[#21802](https://github.com/google-gemini/gemini-cli/pull/21802)
- feat(telemetry): add Clearcut instrumentation for AI credits billing events by
@gsquared94 in
[#22153](https://github.com/google-gemini/gemini-cli/pull/22153)
- feat(core): add google credentials provider for remote agents by @adamfweidman
in [#21024](https://github.com/google-gemini/gemini-cli/pull/21024)
- test(cli): add integration test for node deprecation warnings by @Nixxx19 in
[#20215](https://github.com/google-gemini/gemini-cli/pull/20215)
- feat(cli): allow safe tools to execute concurrently while agent is busy by
@spencer426 in
[#21988](https://github.com/google-gemini/gemini-cli/pull/21988)
- feat(core): implement model-driven parallel tool scheduler by @abhipatel12 in
[#21933](https://github.com/google-gemini/gemini-cli/pull/21933)
- update vulnerable deps by @scidomino in
[#22180](https://github.com/google-gemini/gemini-cli/pull/22180)
- fix(core): fix startup stats to use int values for timestamps and durations by
@yunaseoul in [#22201](https://github.com/google-gemini/gemini-cli/pull/22201)
- fix(core): prevent duplicate tool schemas for instantiated tools by
@abhipatel12 in
[#22204](https://github.com/google-gemini/gemini-cli/pull/22204)
- fix(core): add proxy routing support for remote A2A subagents by @adamfweidman
in [#22199](https://github.com/google-gemini/gemini-cli/pull/22199)
- fix(core/ide): add Antigravity CLI fallbacks by @apfine in
[#22030](https://github.com/google-gemini/gemini-cli/pull/22030)
- fix(browser): fix duplicate function declaration error in browser agent by
@gsquared94 in
[#22207](https://github.com/google-gemini/gemini-cli/pull/22207)
- feat(core): implement Stage 1 improvements for webfetch tool by @aishaneeshah
in [#21313](https://github.com/google-gemini/gemini-cli/pull/21313)
- Changelog for v0.34.0-preview.1 by @gemini-cli-robot in
[#22194](https://github.com/google-gemini/gemini-cli/pull/22194)
- perf(cli): enable code splitting and deferred UI loading by @sehoon38 in
[#22117](https://github.com/google-gemini/gemini-cli/pull/22117)
- fix: remove unused img.png from project root by @SandyTao520 in
[#22222](https://github.com/google-gemini/gemini-cli/pull/22222)
- docs(local model routing): add docs on how to use Gemma for local model
routing by @douglas-reid in
[#21365](https://github.com/google-gemini/gemini-cli/pull/21365)
- feat(a2a): enable native gRPC support and protocol routing by @alisa-alisa in
[#21403](https://github.com/google-gemini/gemini-cli/pull/21403)
- fix(cli): escape @ symbols on paste to prevent unintended file expansion by
@krishdef7 in [#21239](https://github.com/google-gemini/gemini-cli/pull/21239)
- feat(core): add trajectoryId to ConversationOffered telemetry by @yunaseoul in
[#22214](https://github.com/google-gemini/gemini-cli/pull/22214)
- docs: clarify that tools.core is an allowlist for ALL built-in tools by
@hobostay in [#18813](https://github.com/google-gemini/gemini-cli/pull/18813)
- docs(plan): document hooks with plan mode by @ruomengz in
[#22197](https://github.com/google-gemini/gemini-cli/pull/22197)
- Changelog for v0.33.1 by @gemini-cli-robot in
[#22235](https://github.com/google-gemini/gemini-cli/pull/22235)
- build(ci): fix false positive evals trigger on merge commits by @gundermanc in
[#22237](https://github.com/google-gemini/gemini-cli/pull/22237)
- fix(core): explicitly pass messageBus to policy engine for MCP tool saves by
@abhipatel12 in
[#22255](https://github.com/google-gemini/gemini-cli/pull/22255)
- feat(core): Fully migrate packages/core to AgentLoopContext. by @joshualitt in
[#22115](https://github.com/google-gemini/gemini-cli/pull/22115)
- feat(core): increase sub-agent turn and time limits by @bdmorgan in
[#22196](https://github.com/google-gemini/gemini-cli/pull/22196)
- feat(core): instrument file system tools for JIT context discovery by
@SandyTao520 in
[#22082](https://github.com/google-gemini/gemini-cli/pull/22082)
- refactor(ui): extract pure session browser utilities by @abhipatel12 in
[#22256](https://github.com/google-gemini/gemini-cli/pull/22256)
- fix(plan): Fix AskUser evals by @Adib234 in
[#22074](https://github.com/google-gemini/gemini-cli/pull/22074)
- fix(settings): prevent j/k navigation keys from intercepting edit buffer input
by @student-ankitpandit in
[#21865](https://github.com/google-gemini/gemini-cli/pull/21865)
- feat(skills): improve async-pr-review workflow and logging by @mattKorwel in
[#21790](https://github.com/google-gemini/gemini-cli/pull/21790)
- refactor(cli): consolidate getErrorMessage utility to core by @scidomino in
[#22190](https://github.com/google-gemini/gemini-cli/pull/22190)
- fix(core): show descriptive error messages when saving settings fails by
@afarber in [#18095](https://github.com/google-gemini/gemini-cli/pull/18095)
- docs(core): add authentication guide for remote subagents by @adamfweidman in
[#22178](https://github.com/google-gemini/gemini-cli/pull/22178)
- docs: overhaul subagents documentation and add /agents command by @abhipatel12
in [#22345](https://github.com/google-gemini/gemini-cli/pull/22345)
- refactor(ui): extract SessionBrowser static ui components by @abhipatel12 in
[#22348](https://github.com/google-gemini/gemini-cli/pull/22348)
- test: add Object.create context regression test and tool confirmation
integration test by @gsquared94 in
[#22356](https://github.com/google-gemini/gemini-cli/pull/22356)
- feat(tracker): return TodoList display for tracker tools by @anj-s in
[#22060](https://github.com/google-gemini/gemini-cli/pull/22060)
- feat(agent): add allowed domain restrictions for browser agent by
@cynthialong0-0 in
[#21775](https://github.com/google-gemini/gemini-cli/pull/21775)
- chore/release: bump version to 0.35.0-nightly.20260313.bb060d7a9 by
@gemini-cli-robot in
[#22251](https://github.com/google-gemini/gemini-cli/pull/22251)
- Move keychain fallback to keychain service by @chrstnb in
[#22332](https://github.com/google-gemini/gemini-cli/pull/22332)
- feat(core): integrate SandboxManager to sandbox all process-spawning tools by
@galz10 in [#22231](https://github.com/google-gemini/gemini-cli/pull/22231)
- fix(cli): support CJK input and full Unicode scalar values in terminal
protocols by @scidomino in
[#22353](https://github.com/google-gemini/gemini-cli/pull/22353)
- Promote stable tests. by @gundermanc in
[#22253](https://github.com/google-gemini/gemini-cli/pull/22253)
- feat(tracker): add tracker policy by @anj-s in
[#22379](https://github.com/google-gemini/gemini-cli/pull/22379)
- feat(security): add disableAlwaysAllow setting to disable auto-approvals by
@galz10 in [#21941](https://github.com/google-gemini/gemini-cli/pull/21941)
- Revert "fix(cli): validate --model argument at startup" by @sehoon38 in
[#22378](https://github.com/google-gemini/gemini-cli/pull/22378)
- fix(mcp): handle equivalent root resource URLs in OAuth validation by @galz10
in [#20231](https://github.com/google-gemini/gemini-cli/pull/20231)
- fix(core): use session-specific temp directory for task tracker by @anj-s in
[#22382](https://github.com/google-gemini/gemini-cli/pull/22382)
- Fix issue where config was undefined. by @gundermanc in
[#22397](https://github.com/google-gemini/gemini-cli/pull/22397)
- fix(core): deduplicate project memory when JIT context is enabled by
@SandyTao520 in
[#22234](https://github.com/google-gemini/gemini-cli/pull/22234)
- feat(prompts): implement Topic-Action-Summary model for verbosity reduction by
@Abhijit-2592 in
[#21503](https://github.com/google-gemini/gemini-cli/pull/21503)
- fix(core): fix manual deletion of subagent histories by @abhipatel12 in
[#22407](https://github.com/google-gemini/gemini-cli/pull/22407)
- Add registry var by @kevinjwang1 in
[#22224](https://github.com/google-gemini/gemini-cli/pull/22224)
- Add ModelDefinitions to ModelConfigService by @kevinjwang1 in
[#22302](https://github.com/google-gemini/gemini-cli/pull/22302)
- fix(cli): improve command conflict handling for skills by @NTaylorMullen in
[#21942](https://github.com/google-gemini/gemini-cli/pull/21942)
- fix(core): merge user settings with extension-provided MCP servers by
@abhipatel12 in
[#22484](https://github.com/google-gemini/gemini-cli/pull/22484)
- fix(core): skip discovery for incomplete MCP configs and resolve merge race
condition by @abhipatel12 in
[#22494](https://github.com/google-gemini/gemini-cli/pull/22494)
- fix(automation): harden stale PR closer permissions and maintainer detection
by @bdmorgan in
[#22558](https://github.com/google-gemini/gemini-cli/pull/22558)
- fix(automation): evaluate staleness before checking protected labels by
@bdmorgan in [#22561](https://github.com/google-gemini/gemini-cli/pull/22561)
- feat(agent): replace the runtime npx for browser agent chrome devtool mcp with
pre-built bundle by @cynthialong0-0 in
[#22213](https://github.com/google-gemini/gemini-cli/pull/22213)
- perf: optimize TrackerService dependency checks by @anj-s in
[#22384](https://github.com/google-gemini/gemini-cli/pull/22384)
- docs(policy): remove trailing space from commandPrefix examples by @kawasin73
in [#22264](https://github.com/google-gemini/gemini-cli/pull/22264)
- fix(a2a-server): resolve unsafe assignment lint errors by @ehedlund in
[#22661](https://github.com/google-gemini/gemini-cli/pull/22661)
- fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled
tool calls. by @sripasg in
[#22230](https://github.com/google-gemini/gemini-cli/pull/22230)
- Disallow Object.create() and reflect. by @gundermanc in
[#22408](https://github.com/google-gemini/gemini-cli/pull/22408)
- Guard pro model usage by @sehoon38 in
[#22665](https://github.com/google-gemini/gemini-cli/pull/22665)
- refactor(core): Creates AgentSession abstraction for consolidated agent
interface. by @mbleigh in
[#22270](https://github.com/google-gemini/gemini-cli/pull/22270)
- docs(changelog): remove internal commands from release notes by
@jackwotherspoon in
[#22529](https://github.com/google-gemini/gemini-cli/pull/22529)
- feat: enable subagents by @abhipatel12 in
[#22386](https://github.com/google-gemini/gemini-cli/pull/22386)
- feat(extensions): implement cryptographic integrity verification for extension
updates by @ehedlund in
[#21772](https://github.com/google-gemini/gemini-cli/pull/21772)
- feat(tracker): polish UI sorting and formatting by @anj-s in
[#22437](https://github.com/google-gemini/gemini-cli/pull/22437)
- Changelog for v0.34.0-preview.2 by @gemini-cli-robot in
[#22220](https://github.com/google-gemini/gemini-cli/pull/22220)
- fix(core): fix three JIT context bugs in read_file, read_many_files, and
memoryDiscovery by @SandyTao520 in
[#22679](https://github.com/google-gemini/gemini-cli/pull/22679)
- refactor(core): introduce InjectionService with source-aware injection and
backend-native background completions by @adamfweidman in
[#22544](https://github.com/google-gemini/gemini-cli/pull/22544)
- Linux sandbox bubblewrap by @DavidAPierce in
[#22680](https://github.com/google-gemini/gemini-cli/pull/22680)
- feat(core): increase thought signature retry resilience by @bdmorgan in
[#22202](https://github.com/google-gemini/gemini-cli/pull/22202)
- feat(core): implement Stage 2 security and consistency improvements for
web_fetch by @aishaneeshah in
[#22217](https://github.com/google-gemini/gemini-cli/pull/22217)
- refactor(core): replace positional execute params with ExecuteOptions bag by
@adamfweidman in
[#22674](https://github.com/google-gemini/gemini-cli/pull/22674)
- feat(config): enable JIT context loading by default by @SandyTao520 in
[#22736](https://github.com/google-gemini/gemini-cli/pull/22736)
- fix(config): ensure discoveryMaxDirs is passed to global config during
initialization by @kevin-ramdass in
[#22744](https://github.com/google-gemini/gemini-cli/pull/22744)
- fix(plan): allowlist get_internal_docs in Plan Mode by @Adib234 in
[#22668](https://github.com/google-gemini/gemini-cli/pull/22668)
- Changelog for v0.34.0-preview.3 by @gemini-cli-robot in
[#22393](https://github.com/google-gemini/gemini-cli/pull/22393)
- feat(core): add foundation for subagent tool isolation by @akh64bit in
[#22708](https://github.com/google-gemini/gemini-cli/pull/22708)
- fix(core): handle surrogate pairs in truncateString by @sehoon38 in
[#22754](https://github.com/google-gemini/gemini-cli/pull/22754)
- fix(cli): override j/k navigation in settings dialog to fix search input
conflict by @sehoon38 in
[#22800](https://github.com/google-gemini/gemini-cli/pull/22800)
- feat(plan): add 'All the above' option to multi-select AskUser questions by
@Adib234 in [#22365](https://github.com/google-gemini/gemini-cli/pull/22365)
- docs: distribute package-specific GEMINI.md context to each package by
@SandyTao520 in
[#22734](https://github.com/google-gemini/gemini-cli/pull/22734)
- fix(cli): clean up stale pasted placeholder metadata after word/line deletions
by @Jomak-x in
[#20375](https://github.com/google-gemini/gemini-cli/pull/20375)
- refactor(core): align JIT memory placement with tiered context model by
@SandyTao520 in
[#22766](https://github.com/google-gemini/gemini-cli/pull/22766)
- Linux sandbox seccomp by @DavidAPierce in
[#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.1
https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.1
+22 -1
View File
@@ -120,7 +120,8 @@ These are the only allowed tools:
[`list_directory`](../tools/file-system.md#1-list_directory-readfolder),
[`glob`](../tools/file-system.md#4-glob-findfiles)
- **Search:** [`grep_search`](../tools/file-system.md#5-grep_search-searchtext),
[`google_web_search`](../tools/web-search.md)
[`google_web_search`](../tools/web-search.md),
[`get_internal_docs`](../tools/internal-docs.md)
- **Research Subagents:**
[`codebase_investigator`](../core/subagents.md#codebase-investigator),
[`cli_help`](../core/subagents.md#cli-help-agent)
@@ -459,6 +460,26 @@ Manual deletion also removes all associated artifacts:
If you use a [custom plans directory](#custom-plan-directory-and-policies),
those files are not automatically deleted and must be managed manually.
## Non-interactive execution
When running Gemini CLI in non-interactive environments (such as headless
scripts or CI/CD pipelines), Plan Mode optimizes for automated workflows:
- **Automatic transitions:** The policy engine automatically approves the
`enter_plan_mode` and `exit_plan_mode` tools without prompting for user
confirmation.
- **Automated implementation:** When exiting Plan Mode to execute the plan,
Gemini CLI automatically switches to
[YOLO mode](../reference/policy-engine.md#approval-modes) instead of the
standard Default mode. This allows the CLI to execute the implementation steps
automatically without hanging on interactive tool approvals.
**Example:**
```bash
gemini --approval-mode plan -p "Analyze telemetry and suggest improvements"
```
[`plan.toml`]:
https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml
[Conductor]: https://github.com/gemini-cli-extensions/conductor
+1 -1
View File
@@ -52,7 +52,7 @@ You tell Gemini about new servers by editing your `settings.json`.
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/modelcontextprotocol/servers/github:latest"
"ghcr.io/github/github-mcp-server:latest"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
+2 -8
View File
@@ -7,20 +7,14 @@ the main agent's context or toolset.
> **Note: Subagents are currently an experimental feature.**
>
> To use custom subagents, you must explicitly enable them in your
> `settings.json`:
> To use custom subagents, you must ensure they are enabled in your
> `settings.json` (enabled by default):
>
> ```json
> {
> "experimental": { "enableAgents": true }
> }
> ```
>
> **Warning:** Subagents currently operate in
> ["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`.
## What are subagents?
+194 -12
View File
@@ -688,7 +688,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -698,6 +698,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
"isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -707,7 +708,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -717,7 +718,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash",
"family": "gemini-3",
"isPreview": true,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": true
@@ -727,7 +728,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-2.5",
"isPreview": false,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -737,7 +738,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash",
"family": "gemini-2.5",
"isPreview": false,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -747,7 +748,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash-lite",
"family": "gemini-2.5",
"isPreview": false,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -756,6 +757,7 @@ their corresponding top-level category object in your `settings.json` file.
"auto": {
"tier": "auto",
"isPreview": true,
"isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": false
@@ -764,6 +766,7 @@ their corresponding top-level category object in your `settings.json` file.
"pro": {
"tier": "pro",
"isPreview": false,
"isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": false
@@ -772,6 +775,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash": {
"tier": "flash",
"isPreview": false,
"isVisible": false,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -780,6 +784,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash-lite": {
"tier": "flash-lite",
"isPreview": false,
"isVisible": false,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -789,7 +794,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 3)",
"tier": "auto",
"isPreview": true,
"dialogLocation": "main",
"isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
"features": {
"thinking": true,
@@ -800,7 +805,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 2.5)",
"tier": "auto",
"isPreview": false,
"dialogLocation": "main",
"isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
"features": {
"thinking": false,
@@ -812,6 +817,184 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`modelConfigs.modelIdResolutions`** (object):
- **Description:** Rules for resolving requested model names to concrete model
IDs based on context.
- **Default:**
```json
{
"gemini-3-pro-preview": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-3": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"pro": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-2.5": {
"default": "gemini-2.5-pro"
},
"flash": {
"default": "gemini-3-flash-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-flash"
}
]
},
"flash-lite": {
"default": "gemini-2.5-flash-lite"
}
}
```
- **Requires restart:** Yes
- **`modelConfigs.classifierIdResolutions`** (object):
- **Description:** Rules for resolving classifier tiers (flash, pro) to
concrete model IDs.
- **Default:**
```json
{
"flash": {
"default": "gemini-3-flash-preview",
"contexts": [
{
"condition": {
"requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-flash"
},
{
"condition": {
"requestedModels": ["auto-gemini-3", "gemini-3-pro-preview"]
},
"target": "gemini-3-flash-preview"
}
]
},
"pro": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
}
}
```
- **Requires restart:** Yes
#### `agents`
- **`agents.overrides`** (object):
@@ -1158,9 +1341,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents. Warning: Experimental
feature, uses YOLO mode for subagents
- **Default:** `false`
- **Description:** Enable local and remote subagents.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.extensionManagement`** (boolean):
@@ -1191,7 +1373,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading.
- **Default:** `false`
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
+20 -3
View File
@@ -60,7 +60,7 @@ command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git "
commandPrefix = "git"
decision = "ask_user"
priority = 100
```
@@ -90,6 +90,17 @@ 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 rule does not apply.
#### Execution environment
If `interactive` is specified, the rule will only apply if the CLI's execution
environment matches the specified boolean value:
- `true`: The rule applies only in interactive mode.
- `false`: The rule applies only in non-interactive (headless) mode.
If omitted, the rule applies to both interactive and non-interactive
environments.
### Decisions
There are three possible decisions a rule can enforce:
@@ -264,7 +275,7 @@ argsPattern = '"command":"(git|npm)'
# (Optional) A string or array of strings that a shell command must start with.
# This is syntactic sugar for `toolName = "run_shell_command"` and an `argsPattern`.
commandPrefix = "git "
commandPrefix = "git"
# (Optional) A regex to match against the entire shell command.
# This is also syntactic sugar for `toolName = "run_shell_command"`.
@@ -286,6 +297,10 @@ deny_message = "Deletion is permanent"
# (Optional) An array of approval modes where this rule is active.
modes = ["autoEdit"]
# (Optional) A boolean to restrict the rule to interactive (true) or non-interactive (false) environments.
# If omitted, the rule applies to both.
interactive = true
```
### Using arrays (lists)
@@ -321,7 +336,7 @@ This rule will ask for user confirmation before executing any `git` command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git "
commandPrefix = "git"
decision = "ask_user"
priority = 100
```
@@ -360,6 +375,8 @@ priority = 200
Specify only the `mcpName` to apply a rule to every tool provided by that
server.
**Note:** This applies to all decision types (`allow`, `deny`, `ask_user`).
```toml
# Denies all tools from the `untrusted-server` MCP
[[rule]]
+13
View File
@@ -58,6 +58,19 @@ your total token usage using the `/stats` command in Gemini CLI.
## Installation and updates
### How do I check which version of Gemini CLI I'm currently running?
You can check your current Gemini CLI version using one of these methods:
- Run `gemini --version` or `gemini -v` from your terminal
- Check the globally installed version using your package manager:
- npm: `npm list -g @google/gemini-cli`
- pnpm: `pnpm list -g @google/gemini-cli`
- yarn: `yarn global list @google/gemini-cli`
- bun: `bun pm ls -g @google/gemini-cli`
- homebrew: `brew list --versions gemini-cli`
- Inside an active Gemini CLI session, use the `/about` command
### How do I update Gemini CLI to the latest version?
If you installed it globally via `npm`, update it using the command
+2 -1
View File
@@ -25,7 +25,8 @@ confirmation.
- `label` (string, required): Display text (1-5 words).
- `description` (string, required): Brief explanation.
- `multiSelect` (boolean, optional): For `'choice'` type, allows selecting
multiple options.
multiple options. Automatically adds an "All the above" option if there
are multiple standard options.
- `placeholder` (string, optional): Hint text for input fields.
- **Behavior:**
+2 -1
View File
@@ -13,7 +13,8 @@ updates to the CLI interface.
- `todos` (array of objects, required): The complete list of tasks. Each object
includes:
- `description` (string): Technical description of the task.
- `status` (enum): `pending`, `in_progress`, `completed`, or `cancelled`.
- `status` (enum): `pending`, `in_progress`, `completed`, `cancelled`, or
`blocked`.
## Technical behavior
+18 -2
View File
@@ -51,6 +51,7 @@ export default tseslint.config(
'evals/**',
'packages/test-utils/**',
'.gemini/skills/**',
'**/*.d.ts',
],
},
eslint.configs.recommended,
@@ -206,11 +207,26 @@ export default tseslint.config(
{
// Rules that only apply to product code
files: ['packages/*/src/**/*.{ts,tsx}'],
ignores: ['**/*.test.ts', '**/*.test.tsx'],
ignores: ['**/*.test.ts', '**/*.test.tsx', 'packages/*/src/test-utils/**'],
rules: {
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'no-restricted-syntax': [
'error',
...commonRestrictedSyntaxRules,
{
selector:
'CallExpression[callee.object.name="Object"][callee.property.name="create"]',
message:
'Avoid using Object.create() in product code. Use object spread {...obj}, explicit class instantiation, structuredClone(), or copy constructors instead.',
},
{
selector: 'Identifier[name="Reflect"]',
message:
'Avoid using Reflect namespace in product code. Do not use reflection to make copies. Instead, use explicit object copying or cloning (structuredClone() for values, new instance/clone function for classes).',
},
],
},
},
{
@@ -303,7 +319,7 @@ export default tseslint.config(
},
},
{
files: ['./scripts/**/*.js', 'esbuild.config.js'],
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
languageOptions: {
globals: {
...globals.node,
+91 -26
View File
@@ -18,6 +18,18 @@ describe('plan_mode', () => {
experimental: { plan: true },
};
const getWriteTargets = (logs: any[]) =>
logs
.filter((log) => ['write_file', 'replace'].includes(log.toolRequest.name))
.map((log) => {
try {
return JSON.parse(log.toolRequest.args).file_path as string;
} catch {
return '';
}
})
.filter(Boolean);
evalTest('ALWAYS_PASSES', {
name: 'should refuse file modification when in plan mode',
approvalMode: ApprovalMode.PLAN,
@@ -32,27 +44,23 @@ describe('plan_mode', () => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeTargets = toolLogs
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
)
.map((log) => {
try {
return JSON.parse(log.toolRequest.args).file_path;
} catch {
return null;
}
});
const exitPlanIndex = toolLogs.findIndex(
(log) => log.toolRequest.name === 'exit_plan_mode',
);
const writeTargetsBeforeExitPlan = getWriteTargets(
toolLogs.slice(0, exitPlanIndex !== -1 ? exitPlanIndex : undefined),
);
expect(
writeTargets,
writeTargetsBeforeExitPlan,
'Should not attempt to modify README.md in plan mode',
).not.toContain('README.md');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/plan mode|read-only|cannot modify|refuse|exiting/i],
testName: `${TEST_PREFIX}should refuse file modification`,
testName: `${TEST_PREFIX}should refuse file modification in plan mode`,
});
},
});
@@ -69,24 +77,20 @@ describe('plan_mode', () => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeTargets = toolLogs
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
)
.map((log) => {
try {
return JSON.parse(log.toolRequest.args).file_path;
} catch {
return null;
}
});
const exitPlanIndex = toolLogs.findIndex(
(log) => log.toolRequest.name === 'exit_plan_mode',
);
const writeTargetsBeforeExit = getWriteTargets(
toolLogs.slice(0, exitPlanIndex !== -1 ? exitPlanIndex : undefined),
);
// It should NOT write to the docs folder or any other repo path
const hasRepoWrite = writeTargets.some(
const hasRepoWriteBeforeExit = writeTargetsBeforeExit.some(
(path) => path && !path.includes('/plans/'),
);
expect(
hasRepoWrite,
hasRepoWriteBeforeExit,
'Should not attempt to create files in the repository while in plan mode',
).toBe(false);
@@ -166,4 +170,65 @@ describe('plan_mode', () => {
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should create a plan in plan mode and implement it for a refactoring task',
params: {
settings,
},
files: {
'src/mathUtils.ts':
'export const sum = (a: number, b: number) => a + b;\nexport const multiply = (a: number, b: number) => a * b;',
'src/main.ts':
'import { sum } from "./mathUtils";\nconsole.log(sum(1, 2));',
},
prompt:
'I want to refactor our math utilities. Move the `sum` function from `src/mathUtils.ts` to a new file `src/basicMath.ts` and update `src/main.ts` to use the new file. Please create a detailed implementation plan first, then execute it.',
assert: async (rig, result) => {
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
expect(
enterPlanCalled,
'Expected enter_plan_mode tool to be called',
).toBe(true);
const exitPlanCalled = await rig.waitForToolCall('exit_plan_mode');
expect(exitPlanCalled, 'Expected exit_plan_mode tool to be called').toBe(
true,
);
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
// Check if plan was written
const planWrite = toolLogs.find(
(log) =>
log.toolRequest.name === 'write_file' &&
log.toolRequest.args.includes('/plans/'),
);
expect(
planWrite,
'Expected a plan file to be written in the plans directory',
).toBeDefined();
// Check for implementation files
const newFileWrite = toolLogs.find(
(log) =>
log.toolRequest.name === 'write_file' &&
log.toolRequest.args.includes('src/basicMath.ts'),
);
expect(
newFileWrite,
'Expected src/basicMath.ts to be created',
).toBeDefined();
const mainUpdate = toolLogs.find(
(log) =>
['write_file', 'replace'].includes(log.toolRequest.name) &&
log.toolRequest.args.includes('src/main.ts'),
);
expect(mainUpdate, 'Expected src/main.ts to be updated').toBeDefined();
assertModelHasOutput(result);
},
});
});
@@ -0,0 +1,5 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you with that."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and check if there is a heading"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"new_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"success":true,"summary":"SUCCESS_POLICY_TEST_COMPLETED"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Task completed successfully. The page has the heading \"Example Domain\"."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
+178
View File
@@ -0,0 +1,178 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll } from './test-helper.js';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
import stripAnsi from 'strip-ansi';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const chromeAvailable = (() => {
try {
if (process.platform === 'darwin') {
execSync(
'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"',
{
stdio: 'ignore',
},
);
} else if (process.platform === 'linux') {
execSync(
'which google-chrome || which chromium-browser || which chromium',
{ stdio: 'ignore' },
);
} else if (process.platform === 'win32') {
const chromePaths = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
`${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`,
];
const found = chromePaths.some((p) => existsSync(p));
if (!found) {
execSync('where chrome || where chromium', { stdio: 'ignore' });
}
} else {
return false;
}
return true;
} catch {
return false;
}
})();
describe.skipIf(!chromeAvailable)('browser-policy', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
agents: {
overrides: {
browser_agent: {
enabled: true,
},
},
browser: {
headless: true,
sessionMode: 'isolated',
allowedDomains: ['example.com'],
},
},
},
});
// Manually trust the folder to avoid the dialog and enable option 3
const geminiDir = join(rig.homeDir!, '.gemini');
mkdirSync(geminiDir, { recursive: true });
// Write to trustedFolders.json
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
const trustedFolders = {
[rig.testDir!]: 'TRUST_FOLDER',
};
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
// Force confirmation for browser agent.
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
// a rule in the user tier (4.x) like the one from this TOML.
// By removing the explicit mcp rule, the first MCP tool will still prompt
// due to default approvalMode = 'default', and then "Allow all" will correctly
// bypass subsequent tools.
const policyFile = join(rig.testDir!, 'force-confirm.toml');
writeFileSync(
policyFile,
`
[[rule]]
name = "Force confirm browser_agent"
toolName = "browser_agent"
decision = "ask_user"
priority = 200
`,
);
// Update settings.json in both project and home directories to point to the policy file
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
const settingsPath = join(baseDir, '.gemini', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
settings.policyPaths = [policyFile];
// Ensure folder trust is enabled
settings.security = settings.security || {};
settings.security.folderTrust = settings.security.folderTrust || {};
settings.security.folderTrust.enabled = true;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
}
}
const run = await rig.runInteractive({
approvalMode: 'default',
env: {
GEMINI_CLI_INTEGRATION_TEST: 'true',
},
});
await run.sendKeys(
'Open https://example.com and check if there is a heading\r',
);
await run.sendKeys('\r');
// Handle confirmations.
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
await poll(
() => stripAnsi(run.output).toLowerCase().includes('action required'),
60000,
1000,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 2000));
// Handle privacy notice
await poll(
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
5000,
100,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 5000));
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
await poll(
() => {
const stripped = stripAnsi(run.output).toLowerCase();
return (
stripped.includes('new_page') &&
stripped.includes('allow all server tools for this session')
);
},
60000,
1000,
);
// Select "Allow all server tools for this session" (option 3)
await run.sendKeys('3\r');
await new Promise((r) => setTimeout(r, 30000));
const output = stripAnsi(run.output).toLowerCase();
expect(output).toContain('browser_agent');
expect(output).toContain('completed successfully');
});
});
+4 -5
View File
@@ -42,11 +42,10 @@ describe('extension install', () => {
const listResult = await rig.runCommand(['extensions', 'list']);
expect(listResult).toContain('test-extension-install');
writeFileSync(testServerPath, extensionUpdate);
const updateResult = await rig.runCommand([
'extensions',
'update',
`test-extension-install`,
]);
const updateResult = await rig.runCommand(
['extensions', 'update', `test-extension-install`],
{ stdin: 'y\n' },
);
expect(updateResult).toContain('0.0.2');
} finally {
await rig.runCommand([
+550 -47
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260313.bb060d7a9",
"version": "0.36.0-nightly.20260317.2f90b4653",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260313.bb060d7a9"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -43,6 +43,7 @@
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts && npm run test:sea-launch",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
"test:sea-launch": "vitest run sea/sea-launch.test.js",
"posttest": "npm run build",
"test:always_passing_evals": "vitest run --config evals/vitest.config.ts",
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
+22
View File
@@ -0,0 +1,22 @@
# Gemini CLI A2A Server (`@google/gemini-cli-a2a-server`)
Experimental Agent-to-Agent (A2A) server that exposes Gemini CLI capabilities
over HTTP for inter-agent communication.
## Architecture
- `src/agent/`: Agent session management for A2A interactions.
- `src/commands/`: CLI command definitions for the A2A server binary.
- `src/config/`: Server configuration.
- `src/http/`: HTTP server and route handlers.
- `src/persistence/`: Session and state persistence.
- `src/utils/`: Shared utility functions.
- `src/types.ts`: Shared type definitions.
## Running
- Binary entry point: `gemini-cli-a2a-server`
## Testing
- Run tests: `npm test -w @google/gemini-cli-a2a-server`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.35.0-nightly.20260313.bb060d7a9",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -177,10 +177,13 @@ describe('a2a-server memory commands', () => {
expect.any(AbortSignal),
undefined,
{
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
shellExecutionConfig: {
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
sandboxManager: undefined,
},
},
);
+4 -2
View File
@@ -103,8 +103,10 @@ export class AddMemoryCommand implements Command {
const abortController = new AbortController();
const signal = abortController.signal;
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.config.sandboxManager,
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: loopContext.sandboxManager,
},
});
await refreshMemory(context.config);
return {
@@ -19,6 +19,8 @@ import {
AuthType,
isHeadlessMode,
FatalAuthenticationError,
PolicyDecision,
PRIORITY_YOLO_ALLOW_ALL,
} from '@google/gemini-cli-core';
// Mock dependencies
@@ -325,6 +327,29 @@ describe('loadConfig', () => {
);
});
it('should pass enableAgents to Config constructor', async () => {
const settings: Settings = {
experimental: {
enableAgents: false,
},
};
await loadConfig(settings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
enableAgents: false,
}),
);
});
it('should default enableAgents to true when not provided', async () => {
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
enableAgents: true,
}),
);
});
describe('interactivity', () => {
it('should set interactive true when not headless', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(false);
@@ -349,6 +374,41 @@ describe('loadConfig', () => {
});
});
describe('YOLO mode', () => {
it('should enable YOLO mode and add policy rule when GEMINI_YOLO_MODE is true', async () => {
vi.stubEnv('GEMINI_YOLO_MODE', 'true');
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: 'yolo',
policyEngineConfig: expect.objectContaining({
rules: expect.arrayContaining([
expect.objectContaining({
decision: PolicyDecision.ALLOW,
priority: PRIORITY_YOLO_ALLOW_ALL,
modes: ['yolo'],
allowRedirection: true,
}),
]),
}),
}),
);
});
it('should use default approval mode and empty rules when GEMINI_YOLO_MODE is not true', async () => {
vi.stubEnv('GEMINI_YOLO_MODE', 'false');
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: 'default',
policyEngineConfig: expect.objectContaining({
rules: [],
}),
}),
);
});
});
describe('authentication fallback', () => {
beforeEach(() => {
vi.stubEnv('USE_CCPA', 'true');
+22 -4
View File
@@ -26,6 +26,8 @@ import {
isHeadlessMode,
FatalAuthenticationError,
isCloudShell,
PolicyDecision,
PRIORITY_YOLO_ALLOW_ALL,
type TelemetryTarget,
type ConfigParameters,
type ExtensionLoader,
@@ -60,6 +62,11 @@ export async function loadConfig(
}
}
const approvalMode =
process.env['GEMINI_YOLO_MODE'] === 'true'
? ApprovalMode.YOLO
: ApprovalMode.DEFAULT;
const configParams: ConfigParameters = {
sessionId: taskId,
clientName: 'a2a-server',
@@ -74,10 +81,20 @@ export async function loadConfig(
excludeTools: settings.excludeTools || settings.tools?.exclude || undefined,
allowedTools: settings.allowedTools || settings.tools?.allowed || undefined,
showMemoryUsage: settings.showMemoryUsage || false,
approvalMode:
process.env['GEMINI_YOLO_MODE'] === 'true'
? ApprovalMode.YOLO
: ApprovalMode.DEFAULT,
approvalMode,
policyEngineConfig: {
rules:
approvalMode === ApprovalMode.YOLO
? [
{
decision: PolicyDecision.ALLOW,
priority: PRIORITY_YOLO_ALLOW_ALL,
modes: [ApprovalMode.YOLO],
allowRedirection: true,
},
]
: [],
},
mcpServers: settings.mcpServers,
cwd: workspaceDir,
telemetry: {
@@ -110,6 +127,7 @@ export async function loadConfig(
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
ptyInfo: 'auto',
enableAgents: settings.experimental?.enableAgents ?? true,
};
const fileService = new FileDiscoveryService(workspaceDir, {
@@ -112,6 +112,18 @@ describe('loadSettings', () => {
expect(result.fileFiltering?.respectGitIgnore).toBe(true);
});
it('should load experimental settings correctly', () => {
const settings = {
experimental: {
enableAgents: true,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.experimental?.enableAgents).toBe(true);
});
it('should overwrite top-level settings from workspace (shallow merge)', () => {
const userSettings = {
showMemoryUsage: false,
@@ -48,6 +48,9 @@ export interface Settings {
enableRecursiveFileSearch?: boolean;
customIgnoreFilePaths?: string[];
};
experimental?: {
enableAgents?: boolean;
};
}
export interface SettingsError {
@@ -23,6 +23,7 @@ import {
type Storage,
NoopSandboxManager,
type ToolRegistry,
type SandboxManager,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
import { expect, vi } from 'vitest';
@@ -99,7 +100,8 @@ export function createMockConfig(
getGitService: vi.fn(),
validatePathAccess: vi.fn().mockReturnValue(undefined),
getShellExecutionConfig: vi.fn().mockReturnValue({
sandboxManager: new NoopSandboxManager(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
sandboxManager: new NoopSandboxManager() as unknown as SandboxManager,
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
+1 -1
View File
@@ -5,7 +5,7 @@
- Always fix react-hooks/exhaustive-deps lint errors by adding the missing
dependencies.
- **Shortcuts**: only define keyboard shortcuts in
`packages/cli/src/config/keyBindings.ts`
`packages/cli/src/ui/key/keyBindings.ts`
- Do not implement any logic performing custom string measurement or string
truncation. Use Ink layout instead leveraging ResizeObserver as needed.
- Avoid prop drilling when at all possible.
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260313.bb060d7a9",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -20,13 +20,14 @@
"format": "prettier --write .",
"test": "vitest run",
"test:ci": "vitest run",
"posttest": "npm run build",
"typecheck": "tsc --noEmit"
},
"files": [
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260313.bb060d7a9"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
+84
View File
@@ -177,6 +177,9 @@ describe('GeminiAgent', () => {
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
mockSettings = {
merged: {
@@ -656,6 +659,12 @@ describe('Session', () => {
getGitService: vi.fn().mockResolvedValue({} as GitService),
waitForMcpInit: vi.fn(),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
get toolRegistry() {
return mockToolRegistry;
},
} as unknown as Mocked<Config>;
mockConnection = {
sessionUpdate: vi.fn(),
@@ -894,6 +903,9 @@ describe('Session', () => {
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'completed',
title: 'Test Tool',
locations: [],
kind: 'read',
}),
}),
);
@@ -1306,6 +1318,18 @@ describe('Session', () => {
expect(path.resolve).toHaveBeenCalled();
expect(fs.stat).toHaveBeenCalled();
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'completed',
title: 'Read files',
locations: [],
kind: 'read',
}),
}),
);
// Verify ReadManyFilesTool was used (implicitly by checking if sendMessageStream was called with resolved content)
// Since we mocked ReadManyFilesTool to return specific content, we can check the args passed to sendMessageStream
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
@@ -1321,6 +1345,65 @@ describe('Session', () => {
);
});
it('should handle @path resolution error', async () => {
(path.resolve as unknown as Mock).mockReturnValue('/tmp/error.txt');
(fs.stat as unknown as Mock).mockResolvedValue({
isDirectory: () => false,
});
(isWithinRoot as unknown as Mock).mockReturnValue(true);
const MockReadManyFilesTool = ReadManyFilesTool as unknown as Mock;
MockReadManyFilesTool.mockImplementationOnce(() => ({
name: 'read_many_files',
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Read files',
toolLocations: () => [],
execute: vi.fn().mockRejectedValue(new Error('File read failed')),
}),
}));
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await expect(
session.prompt({
sessionId: 'session-1',
prompt: [
{ type: 'text', text: 'Read' },
{
type: 'resource_link',
uri: 'file://error.txt',
mimeType: 'text/plain',
name: 'error.txt',
},
],
}),
).rejects.toThrow('File read failed');
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'failed',
content: expect.arrayContaining([
expect.objectContaining({
content: expect.objectContaining({
text: expect.stringMatching(/File read failed/),
}),
}),
]),
kind: 'read',
}),
}),
);
});
it('should handle cancellation during prompt', async () => {
let streamController: ReadableStreamDefaultController<unknown>;
const stream = new ReadableStream({
@@ -1434,6 +1517,7 @@ describe('Session', () => {
content: expect.objectContaining({ text: 'Tool failed' }),
}),
]),
kind: 'read',
}),
}),
);
+51 -34
View File
@@ -47,6 +47,7 @@ import {
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
getDisplayString,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { AcpFileSystemService } from './fileSystemService.js';
@@ -104,7 +105,7 @@ export class GeminiAgent {
private customHeaders: Record<string, string> | undefined;
constructor(
private config: Config,
private context: AgentLoopContext,
private settings: LoadedSettings,
private argv: CliArgs,
private connection: acp.AgentSideConnection,
@@ -148,7 +149,7 @@ export class GeminiAgent {
},
];
await this.config.initialize();
await this.context.config.initialize();
const version = await getVersion();
return {
protocolVersion: acp.PROTOCOL_VERSION,
@@ -220,7 +221,7 @@ export class GeminiAgent {
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.config.refreshAuth(
await this.context.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
@@ -537,7 +538,7 @@ export class Session {
constructor(
private readonly id: string,
private readonly chat: GeminiChat,
private readonly config: Config,
private readonly context: AgentLoopContext,
private readonly connection: acp.AgentSideConnection,
private readonly settings: LoadedSettings,
) {}
@@ -552,13 +553,15 @@ export class Session {
}
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
const availableModes = buildAvailableModes(this.config.isPlanEnabled());
const availableModes = buildAvailableModes(
this.context.config.isPlanEnabled(),
);
const mode = availableModes.find((m) => m.id === modeId);
if (!mode) {
throw new Error(`Invalid or unavailable mode: ${modeId}`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this.config.setApprovalMode(mode.id as ApprovalMode);
this.context.config.setApprovalMode(mode.id as ApprovalMode);
return {};
}
@@ -579,7 +582,7 @@ export class Session {
}
setModel(modelId: acp.ModelId): acp.SetSessionModelResponse {
this.config.setModel(modelId);
this.context.config.setModel(modelId);
return {};
}
@@ -634,7 +637,7 @@ export class Session {
}
}
const tool = this.config.getToolRegistry().getTool(toolCall.name);
const tool = this.context.toolRegistry.getTool(toolCall.name);
await this.sendUpdate({
sessionUpdate: 'tool_call',
@@ -658,7 +661,7 @@ export class Session {
const pendingSend = new AbortController();
this.pendingPrompt = pendingSend;
await this.config.waitForMcpInit();
await this.context.config.waitForMcpInit();
const promptId = Math.random().toString(16).slice(2);
const chat = this.chat;
@@ -712,8 +715,8 @@ export class Session {
try {
const model = resolveModel(
this.config.getModel(),
(await this.config.getGemini31Launched?.()) ?? false,
this.context.config.getModel(),
(await this.context.config.getGemini31Launched?.()) ?? false,
);
const responseStream = await chat.sendMessageStream(
{ model },
@@ -804,9 +807,9 @@ export class Session {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
parts: Part[],
): Promise<boolean> {
const gitService = await this.config.getGitService();
const gitService = await this.context.config.getGitService();
const commandContext = {
config: this.config,
agentContext: this.context,
settings: this.settings,
git: gitService,
sendMessage: async (text: string) => {
@@ -842,7 +845,7 @@ export class Session {
const errorResponse = (error: Error) => {
const durationMs = Date.now() - startTime;
logToolCall(
this.config,
this.context.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -872,7 +875,7 @@ export class Session {
return errorResponse(new Error('Missing function name'));
}
const toolRegistry = this.config.getToolRegistry();
const toolRegistry = this.context.toolRegistry;
const tool = toolRegistry.getTool(fc.name);
if (!tool) {
@@ -908,7 +911,10 @@ export class Session {
const params: acp.RequestPermissionRequest = {
sessionId: this.id,
options: toPermissionOptions(confirmationDetails, this.config),
options: toPermissionOptions(
confirmationDetails,
this.context.config,
),
toolCall: {
toolCallId: callId,
status: 'pending',
@@ -966,12 +972,15 @@ export class Session {
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
locations: invocation.toolLocations(),
kind: toAcpToolKind(tool.kind),
});
const durationMs = Date.now() - startTime;
logToolCall(
this.config,
this.context.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -985,7 +994,7 @@ export class Session {
),
);
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
{
status: CoreToolCallStatus.Success,
request: {
@@ -1003,8 +1012,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
this.context.config.getActiveModel(),
this.context.config,
),
resultDisplay: toolResult.returnDisplay,
error: undefined,
@@ -1017,8 +1026,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
this.context.config.getActiveModel(),
this.context.config,
);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
@@ -1030,9 +1039,10 @@ export class Session {
content: [
{ type: 'content', content: { type: 'text', text: error.message } },
],
kind: toAcpToolKind(tool.kind),
});
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
{
status: CoreToolCallStatus.Error,
request: {
@@ -1118,18 +1128,18 @@ export class Session {
const atPathToResolvedSpecMap = new Map<string, string>();
// Get centralized file discovery service
const fileDiscovery = this.config.getFileService();
const fileDiscovery = this.context.config.getFileService();
const fileFilteringOptions: FilterFilesOptions =
this.config.getFileFilteringOptions();
this.context.config.getFileFilteringOptions();
const pathSpecsToRead: string[] = [];
const contentLabelsForDisplay: string[] = [];
const ignoredPaths: string[] = [];
const toolRegistry = this.config.getToolRegistry();
const toolRegistry = this.context.toolRegistry;
const readManyFilesTool = new ReadManyFilesTool(
this.config,
this.config.getMessageBus(),
this.context.config,
this.context.messageBus,
);
const globTool = toolRegistry.getTool('glob');
@@ -1148,8 +1158,11 @@ export class Session {
let currentPathSpec = pathName;
let resolvedSuccessfully = false;
try {
const absolutePath = path.resolve(this.config.getTargetDir(), pathName);
if (isWithinRoot(absolutePath, this.config.getTargetDir())) {
const absolutePath = path.resolve(
this.context.config.getTargetDir(),
pathName,
);
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
const stats = await fs.stat(absolutePath);
if (stats.isDirectory()) {
currentPathSpec = pathName.endsWith('/')
@@ -1169,7 +1182,7 @@ export class Session {
}
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
if (this.config.getEnableRecursiveFileSearch() && globTool) {
if (this.context.config.getEnableRecursiveFileSearch() && globTool) {
this.debug(
`Path ${pathName} not found directly, attempting glob search.`,
);
@@ -1177,7 +1190,7 @@ export class Session {
const globResult = await globTool.buildAndExecute(
{
pattern: `**/*${pathName}*`,
path: this.config.getTargetDir(),
path: this.context.config.getTargetDir(),
},
abortSignal,
);
@@ -1191,7 +1204,7 @@ export class Session {
if (lines.length > 1 && lines[1]) {
const firstMatchAbsolute = lines[1].trim();
currentPathSpec = path.relative(
this.config.getTargetDir(),
this.context.config.getTargetDir(),
firstMatchAbsolute,
);
this.debug(
@@ -1324,7 +1337,10 @@ export class Session {
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
locations: invocation.toolLocations(),
kind: toAcpToolKind(readManyFilesTool.kind),
});
if (Array.isArray(result.llmContent)) {
const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
@@ -1368,6 +1384,7 @@ export class Session {
},
},
],
kind: toAcpToolKind(readManyFilesTool.kind),
});
throw error;
@@ -1402,7 +1419,7 @@ export class Session {
}
debug(msg: string) {
if (this.config.getDebugMode()) {
if (this.context.config.getDebugMode()) {
debugLogger.warn(msg);
}
}
+6 -2
View File
@@ -97,6 +97,9 @@ describe('GeminiAgent Session Resume', () => {
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
get config() {
return this;
},
} as unknown as Mocked<Config>;
mockSettings = {
merged: {
@@ -158,9 +161,10 @@ describe('GeminiAgent Session Resume', () => {
],
};
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockConfig as any).toolRegistry = {
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
});
};
(SessionSelector as unknown as Mock).mockImplementation(() => ({
resolveSession: vi.fn().mockResolvedValue({
+10 -9
View File
@@ -53,7 +53,7 @@ export class ListExtensionsCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(context.config);
const extensions = listExtensions(context.agentContext.config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
@@ -134,7 +134,7 @@ export class EnableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
context.agentContext.config,
args,
'enable',
);
@@ -156,7 +156,8 @@ export class EnableExtensionCommand implements Command {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.config.getMcpClientManager();
const mcpClientManager =
context.agentContext.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers),
);
@@ -191,7 +192,7 @@ export class DisableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
context.agentContext.config,
args,
'disable',
);
@@ -223,7 +224,7 @@ export class InstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -268,7 +269,7 @@ export class LinkExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -313,7 +314,7 @@ export class UninstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -369,7 +370,7 @@ export class RestartExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot restart extensions.' };
}
@@ -424,7 +425,7 @@ export class UpdateExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot update extensions.' };
}
+1 -1
View File
@@ -22,7 +22,7 @@ export class InitCommand implements Command {
context: CommandContext,
_args: string[] = [],
): Promise<CommandExecutionResponse> {
const targetDir = context.config.getTargetDir();
const targetDir = context.agentContext.config.getTargetDir();
if (!targetDir) {
throw new Error('Command requires a workspace.');
}
+9 -7
View File
@@ -49,7 +49,7 @@ export class ShowMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = showMemory(context.config);
const result = showMemory(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -63,7 +63,7 @@ export class RefreshMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = await refreshMemory(context.config);
const result = await refreshMemory(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -76,7 +76,7 @@ export class ListMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = listMemoryFiles(context.config);
const result = listMemoryFiles(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -95,7 +95,7 @@ export class AddMemoryCommand implements Command {
return { name: this.name, data: result.content };
}
const toolRegistry = context.config.getToolRegistry();
const toolRegistry = context.agentContext.toolRegistry;
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
@@ -104,10 +104,12 @@ export class AddMemoryCommand implements Command {
await context.sendMessage(`Saving memory via ${result.toolName}...`);
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.config.sandboxManager,
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.agentContext.sandboxManager,
},
});
await refreshMemory(context.config);
await refreshMemory(context.agentContext.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
+3 -2
View File
@@ -29,7 +29,8 @@ export class RestoreCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const { config, git: gitService } = context;
const { agentContext: agentContext, git: gitService } = context;
const { config } = agentContext;
const argsStr = args.join(' ');
try {
@@ -116,7 +117,7 @@ export class ListCheckpointsCommand implements Command {
readonly description = 'Lists all available checkpoints.';
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
const { config } = context;
const { config } = context.agentContext;
try {
if (!config.getCheckpointingEnabled()) {
+2 -2
View File
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, GitService } from '@google/gemini-cli-core';
import type { AgentLoopContext, GitService } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
export interface CommandContext {
config: Config;
agentContext: AgentLoopContext;
settings: LoadedSettings;
git?: GitService;
sendMessage: (text: string) => Promise<void>;
@@ -137,6 +137,7 @@ describe('handleInstall', () => {
mcps: [],
hooks: [],
skills: [],
agents: [],
settings: [],
securityWarnings: [],
discoveryErrors: [],
@@ -379,6 +380,7 @@ describe('handleInstall', () => {
mcps: [],
hooks: [],
skills: ['cool-skill'],
agents: ['cool-agent'],
settings: [],
securityWarnings: ['Security risk!'],
discoveryErrors: ['Read error'],
@@ -408,6 +410,10 @@ describe('handleInstall', () => {
expect.stringContaining('cool-skill'),
false,
);
expect(mockPromptForConsentNonInteractive).toHaveBeenCalledWith(
expect.stringContaining('cool-agent'),
false,
);
expect(mockPromptForConsentNonInteractive).toHaveBeenCalledWith(
expect.stringContaining('Security Warnings:'),
false,
@@ -99,11 +99,15 @@ export async function handleInstall(args: InstallArgs) {
if (hasDiscovery) {
promptLines.push(chalk.bold('This folder contains:'));
const groups = [
{ label: 'Commands', items: discoveryResults.commands },
{ label: 'MCP Servers', items: discoveryResults.mcps },
{ label: 'Hooks', items: discoveryResults.hooks },
{ label: 'Skills', items: discoveryResults.skills },
{ label: 'Setting overrides', items: discoveryResults.settings },
{ label: 'Commands', items: discoveryResults.commands ?? [] },
{ label: 'MCP Servers', items: discoveryResults.mcps ?? [] },
{ label: 'Hooks', items: discoveryResults.hooks ?? [] },
{ label: 'Skills', items: discoveryResults.skills ?? [] },
{ label: 'Agents', items: discoveryResults.agents ?? [] },
{
label: 'Setting overrides',
items: discoveryResults.settings ?? [],
},
].filter((g) => g.items.length > 0);
for (const group of groups) {
+53 -2
View File
@@ -763,6 +763,48 @@ describe('loadCliConfig', () => {
});
});
it('should add IDE workspace folders from GEMINI_CLI_IDE_WORKSPACE_PATH to include directories', async () => {
vi.stubEnv(
'GEMINI_CLI_IDE_WORKSPACE_PATH',
['/project/folderA', '/project/folderB'].join(path.delimiter),
);
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings();
const config = await loadCliConfig(settings, 'test-session', argv);
const dirs = config.getPendingIncludeDirectories();
expect(dirs).toContain('/project/folderA');
expect(dirs).toContain('/project/folderB');
});
it('should skip inaccessible workspace folders from GEMINI_CLI_IDE_WORKSPACE_PATH', async () => {
const resolveToRealPathSpy = vi
.spyOn(ServerConfig, 'resolveToRealPath')
.mockImplementation((p) => {
if (p.toString().includes('restricted')) {
const err = new Error('EACCES: permission denied');
(err as NodeJS.ErrnoException).code = 'EACCES';
throw err;
}
return p.toString();
});
vi.stubEnv(
'GEMINI_CLI_IDE_WORKSPACE_PATH',
['/project/folderA', '/nonexistent/restricted/folder'].join(
path.delimiter,
),
);
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings();
const config = await loadCliConfig(settings, 'test-session', argv);
const dirs = config.getPendingIncludeDirectories();
expect(dirs).toContain('/project/folderA');
expect(dirs).not.toContain('/nonexistent/restricted/folder');
resolveToRealPathSpy.mockRestore();
});
it('should use default fileFilter options when unconfigured', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
@@ -798,6 +840,7 @@ describe('loadCliConfig', () => {
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '');
// Restore ExtensionManager mocks that were reset
ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]);
ExtensionManager.prototype.loadExtensions = vi
@@ -809,12 +852,15 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings();
const settings = createTestMergedSettings({
experimental: { jitContext: false },
});
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
path: '/path/to/ext1',
@@ -865,6 +911,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
process.argv = ['node', 'script.js'];
const includeDir = path.resolve(path.sep, 'path', 'to', 'include');
const settings = createTestMergedSettings({
experimental: { jitContext: false },
context: {
includeDirectories: [includeDir],
loadMemoryFromIncludeDirectories: true,
@@ -892,6 +939,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { jitContext: false },
context: {
includeDirectories: ['/path/to/include'],
loadMemoryFromIncludeDirectories: false,
@@ -3343,7 +3391,10 @@ describe('Policy Engine Integration in loadCliConfig', () => {
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
policyPaths: [
path.normalize('/path/to/policy1.toml'),
path.normalize('/path/to/policy2.toml'),
],
}),
expect.anything(),
);
+35 -8
View File
@@ -244,10 +244,11 @@ export async function parseArguments(
// When --resume passed without a value (`gemini --resume`): value = "" (string)
// When --resume not passed at all: this `coerce` function is not called at all, and
// `yargsInstance.argv.resume` is undefined.
if (value === '') {
const trimmed = value.trim();
if (trimmed === '') {
return RESUME_LATEST;
}
return value;
return trimmed;
},
})
.option('list-sessions', {
@@ -429,8 +430,6 @@ export async function loadCliConfig(
const { cwd = process.cwd(), projectHooks } = options;
const debugMode = isDebugMode(argv);
const loadedSettings = loadSettings(cwd);
if (argv.sandbox) {
process.env['GEMINI_SANDBOX'] = 'true';
}
@@ -474,10 +473,32 @@ export async function loadCliConfig(
...settings.context?.fileFiltering,
};
//changes the includeDirectories to be absolute paths based on the cwd, and also include any additional directories specified via CLI args
const includeDirectories = (settings.context?.includeDirectories || [])
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
// When running inside VSCode with multiple workspace folders,
// automatically add the other folders as include directories
// so Gemini has context of all open folders, not just the cwd.
const ideWorkspacePath = process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'];
if (ideWorkspacePath) {
const realCwd = resolveToRealPath(cwd);
const ideFolders = ideWorkspacePath.split(path.delimiter).filter((p) => {
const trimmedPath = p.trim();
if (!trimmedPath) return false;
try {
return resolveToRealPath(trimmedPath) !== realCwd;
} catch (e) {
debugLogger.debug(
`[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${e instanceof Error ? e.message : String(e)})`,
);
return false;
}
});
includeDirectories.push(...ideFolders);
}
const extensionManager = new ExtensionManager({
settings,
requestConsent: requestConsentNonInteractive,
@@ -494,7 +515,7 @@ export async function loadCliConfig(
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const experimentalJitContext = settings.experimental?.jitContext ?? false;
const experimentalJitContext = settings.experimental.jitContext;
let extensionRegistryURI =
process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ??
@@ -650,8 +671,12 @@ export async function loadCliConfig(
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
},
policyPaths: argv.policy ?? settings.policyPaths,
adminPolicyPaths: argv.adminPolicy ?? settings.adminPolicyPaths,
policyPaths: (argv.policy ?? settings.policyPaths)?.map((p) =>
resolvePath(p),
),
adminPolicyPaths: (argv.adminPolicy ?? settings.adminPolicyPaths)?.map(
(p) => resolvePath(p),
),
};
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
@@ -737,6 +762,8 @@ export async function loadCliConfig(
includeDirectories,
loadMemoryFromIncludeDirectories:
settings.context?.loadMemoryFromIncludeDirectories || false,
discoveryMaxDirs: settings.context?.discoveryMaxDirs,
importFormat: settings.context?.importFormat,
debugMode,
question,
@@ -857,7 +884,7 @@ export async function loadCliConfig(
hooks: settings.hooks || {},
disabledHooks: settings.hooksConfig?.disabled || [],
projectHooks: projectHooks || {},
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
onModelChange: (model: string) => saveModelChange(loadSettings(cwd), model),
onReload: async () => {
const refreshedSettings = loadSettings(cwd);
return {
@@ -18,9 +18,17 @@ import {
loadTrustedFolders,
isWorkspaceTrusted,
} from './trustedFolders.js';
import { getRealPath, type CustomTheme } from '@google/gemini-cli-core';
import {
getRealPath,
type CustomTheme,
IntegrityDataStatus,
} from '@google/gemini-cli-core';
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
const mockIntegrityManager = vi.hoisted(() => ({
verify: vi.fn().mockResolvedValue('verified'),
store: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
@@ -36,6 +44,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...actual,
homedir: mockHomedir,
ExtensionIntegrityManager: vi
.fn()
.mockImplementation(() => mockIntegrityManager),
};
});
@@ -82,6 +93,7 @@ describe('ExtensionManager', () => {
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
});
@@ -245,6 +257,7 @@ describe('ExtensionManager', () => {
} as unknown as MergedSettings,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
// Trust the workspace to allow installation
@@ -290,6 +303,7 @@ describe('ExtensionManager', () => {
settings,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
const installMetadata = {
@@ -324,6 +338,7 @@ describe('ExtensionManager', () => {
settings,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
const installMetadata = {
@@ -353,6 +368,7 @@ describe('ExtensionManager', () => {
settings: settingsOnlySymlink,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
// This should FAIL because it checks the real path against the pattern
@@ -507,6 +523,80 @@ describe('ExtensionManager', () => {
});
});
describe('extension integrity', () => {
it('should store integrity data during installation', async () => {
const storeSpy = vi.spyOn(extensionManager, 'storeExtensionIntegrity');
const extDir = path.join(tempHomeDir, 'new-integrity-ext');
fs.mkdirSync(extDir, { recursive: true });
fs.writeFileSync(
path.join(extDir, 'gemini-extension.json'),
JSON.stringify({ name: 'integrity-ext', version: '1.0.0' }),
);
const installMetadata = {
source: extDir,
type: 'local' as const,
};
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension(installMetadata);
expect(storeSpy).toHaveBeenCalledWith('integrity-ext', installMetadata);
});
it('should store integrity data during first update', async () => {
const storeSpy = vi.spyOn(extensionManager, 'storeExtensionIntegrity');
const verifySpy = vi.spyOn(extensionManager, 'verifyExtensionIntegrity');
// Setup existing extension
const extName = 'update-integrity-ext';
const extDir = path.join(userExtensionsDir, extName);
fs.mkdirSync(extDir, { recursive: true });
fs.writeFileSync(
path.join(extDir, 'gemini-extension.json'),
JSON.stringify({ name: extName, version: '1.0.0' }),
);
fs.writeFileSync(
path.join(extDir, 'metadata.json'),
JSON.stringify({ type: 'local', source: extDir }),
);
await extensionManager.loadExtensions();
// Ensure no integrity data exists for this extension
verifySpy.mockResolvedValueOnce(IntegrityDataStatus.MISSING);
const initialStatus = await extensionManager.verifyExtensionIntegrity(
extName,
{ type: 'local', source: extDir },
);
expect(initialStatus).toBe('missing');
// Create new version of the extension
const newSourceDir = fs.mkdtempSync(
path.join(tempHomeDir, 'new-source-'),
);
fs.writeFileSync(
path.join(newSourceDir, 'gemini-extension.json'),
JSON.stringify({ name: extName, version: '1.1.0' }),
);
const installMetadata = {
source: newSourceDir,
type: 'local' as const,
};
// Perform update and verify integrity was stored
await extensionManager.installOrUpdateExtension(installMetadata, {
name: extName,
version: '1.0.0',
});
expect(storeSpy).toHaveBeenCalledWith(extName, installMetadata);
});
});
describe('early theme registration', () => {
it('should register themes with ThemeManager during loadExtensions for active extensions', async () => {
createExtension({
@@ -547,4 +637,64 @@ describe('ExtensionManager', () => {
);
});
});
describe('orphaned extension cleanup', () => {
it('should remove broken extension metadata on startup to allow re-installation', async () => {
const extName = 'orphaned-ext';
const sourceDir = path.join(tempHomeDir, 'valid-source');
fs.mkdirSync(sourceDir, { recursive: true });
fs.writeFileSync(
path.join(sourceDir, 'gemini-extension.json'),
JSON.stringify({ name: extName, version: '1.0.0' }),
);
// Link an extension successfully.
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceDir,
type: 'link',
});
const destinationPath = path.join(userExtensionsDir, extName);
const metadataPath = path.join(
destinationPath,
'.gemini-extension-install.json',
);
expect(fs.existsSync(metadataPath)).toBe(true);
// Simulate metadata corruption (e.g., pointing to a non-existent source).
fs.writeFileSync(
metadataPath,
JSON.stringify({ source: '/NON_EXISTENT_PATH', type: 'link' }),
);
// Simulate CLI startup. The manager should detect the broken link
// and proactively delete the orphaned metadata directory.
const newManager = new ExtensionManager({
settings: createTestMergedSettings(),
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
integrityManager: mockIntegrityManager,
});
await newManager.loadExtensions();
// Verify the extension failed to load and was proactively cleaned up.
expect(newManager.getExtensions().some((e) => e.name === extName)).toBe(
false,
);
expect(fs.existsSync(destinationPath)).toBe(false);
// Verify the system is self-healed and allows re-linking to the valid source.
await newManager.installOrUpdateExtension({
source: sourceDir,
type: 'link',
});
expect(newManager.getExtensions().some((e) => e.name === extName)).toBe(
true,
);
});
});
});
+40 -12
View File
@@ -41,6 +41,9 @@ import {
loadSkillsFromDir,
loadAgentsFromDirectory,
homedir,
ExtensionIntegrityManager,
type IExtensionIntegrity,
type IntegrityDataStatus,
type ExtensionEvents,
type MCPServerConfig,
type ExtensionInstallMetadata,
@@ -89,6 +92,7 @@ interface ExtensionManagerParams {
workspaceDir: string;
eventEmitter?: EventEmitter<ExtensionEvents>;
clientVersion?: string;
integrityManager?: IExtensionIntegrity;
}
/**
@@ -98,6 +102,7 @@ interface ExtensionManagerParams {
*/
export class ExtensionManager extends ExtensionLoader {
private extensionEnablementManager: ExtensionEnablementManager;
private integrityManager: IExtensionIntegrity;
private settings: MergedSettings;
private requestConsent: (consent: string) => Promise<boolean>;
private requestSetting:
@@ -127,12 +132,28 @@ export class ExtensionManager extends ExtensionLoader {
});
this.requestConsent = options.requestConsent;
this.requestSetting = options.requestSetting ?? undefined;
this.integrityManager =
options.integrityManager ?? new ExtensionIntegrityManager();
}
getEnablementManager(): ExtensionEnablementManager {
return this.extensionEnablementManager;
}
async verifyExtensionIntegrity(
extensionName: string,
metadata: ExtensionInstallMetadata | undefined,
): Promise<IntegrityDataStatus> {
return this.integrityManager.verify(extensionName, metadata);
}
async storeExtensionIntegrity(
extensionName: string,
metadata: ExtensionInstallMetadata,
): Promise<void> {
return this.integrityManager.store(extensionName, metadata);
}
setRequestConsent(
requestConsent: (consent: string) => Promise<boolean>,
): void {
@@ -159,10 +180,7 @@ export class ExtensionManager extends ExtensionLoader {
previousExtensionConfig?: ExtensionConfig,
requestConsentOverride?: (consent: string) => Promise<boolean>,
): Promise<GeminiCLIExtension> {
if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
if ((this.settings.security?.allowedExtensions?.length ?? 0) > 0) {
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
@@ -421,6 +439,12 @@ Would you like to attempt to install via "git clone" instead?`,
);
await fs.promises.writeFile(metadataPath, metadataString);
// Establish trust at point of installation
await this.storeExtensionIntegrity(
newExtensionConfig.name,
installMetadata,
);
// TODO: Gracefully handle this call failing, we should back up the old
// extension prior to overwriting it and then restore and restart it.
extension = await this.loadExtension(destinationPath);
@@ -693,10 +717,7 @@ Would you like to attempt to install via "git clone" instead?`,
const installMetadata = loadInstallMetadata(extensionDir);
let effectiveExtensionPath = extensionDir;
if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
if ((this.settings.security?.allowedExtensions?.length ?? 0) > 0) {
if (!installMetadata?.source) {
throw new Error(
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
@@ -961,11 +982,18 @@ Would you like to attempt to install via "git clone" instead?`,
plan: config.plan,
};
} catch (e) {
debugLogger.error(
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
e,
)}`,
const extName = path.basename(extensionDir);
debugLogger.warn(
`Warning: Removing broken extension ${extName}: ${getErrorMessage(e)}`,
);
try {
await fs.promises.rm(extensionDir, { recursive: true, force: true });
} catch (rmError) {
debugLogger.error(
`Failed to remove broken extension directory ${extensionDir}:`,
rmError,
);
}
return null;
}
}
+31 -20
View File
@@ -103,6 +103,10 @@ const mockLogExtensionInstallEvent = vi.hoisted(() => vi.fn());
const mockLogExtensionUninstall = vi.hoisted(() => vi.fn());
const mockLogExtensionUpdateEvent = vi.hoisted(() => vi.fn());
const mockLogExtensionDisable = vi.hoisted(() => vi.fn());
const mockIntegrityManager = vi.hoisted(() => ({
verify: vi.fn().mockResolvedValue('verified'),
store: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
@@ -118,6 +122,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
ExtensionInstallEvent: vi.fn(),
ExtensionUninstallEvent: vi.fn(),
ExtensionDisableEvent: vi.fn(),
ExtensionIntegrityManager: vi
.fn()
.mockImplementation(() => mockIntegrityManager),
KeychainTokenStorage: vi.fn().mockImplementation(() => ({
getSecret: vi.fn(),
setSecret: vi.fn(),
@@ -214,6 +221,7 @@ describe('extension tests', () => {
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings,
integrityManager: mockIntegrityManager,
});
resetTrustedFoldersForTesting();
});
@@ -241,10 +249,8 @@ describe('extension tests', () => {
expect(extensions[0].name).toBe('test-extension');
});
it('should throw an error if a context file path is outside the extension directory', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should log a warning and remove the extension if a context file path is outside the extension directory', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'traversal-extension',
@@ -654,10 +660,8 @@ name = "yolo-checker"
expect(serverConfig.env!['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
});
it('should skip extensions with invalid JSON and log a warning', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should remove an extension with invalid JSON config and log a warning', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Good extension
createExtension({
@@ -678,17 +682,15 @@ name = "yolo-checker"
expect(extensions[0].name).toBe('good-ext');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
`Warning: Removing broken extension bad-ext: Failed to load extension config from ${badConfigPath}`,
),
);
consoleSpy.mockRestore();
});
it('should skip extensions with missing name and log a warning', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should remove an extension with missing "name" in config and log a warning', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Good extension
createExtension({
@@ -709,7 +711,7 @@ name = "yolo-checker"
expect(extensions[0].name).toBe('good-ext');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
`Warning: Removing broken extension bad-ext-no-name: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
),
);
@@ -735,10 +737,8 @@ name = "yolo-checker"
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
});
it('should throw an error for invalid extension names', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should log a warning for invalid extension names during loading', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'bad_name',
@@ -754,7 +754,7 @@ name = "yolo-checker"
consoleSpy.mockRestore();
});
it('should not load github extensions if blockGitExtensions is set', async () => {
it('should not load github extensions and log a warning if blockGitExtensions is set', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
@@ -774,6 +774,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: blockGitExtensionsSetting,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'my-ext');
@@ -807,6 +808,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: extensionAllowlistSetting,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
@@ -814,7 +816,7 @@ name = "yolo-checker"
expect(extensions[0].name).toBe('my-ext');
});
it('should not load disallowed extensions if the allowlist is set.', async () => {
it('should not load disallowed extensions and log a warning if the allowlist is set.', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
@@ -835,6 +837,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: extensionAllowlistSetting,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'my-ext');
@@ -862,6 +865,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: loadedSettings,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
@@ -885,6 +889,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: loadedSettings,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
@@ -909,6 +914,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: loadedSettings,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
@@ -1047,6 +1053,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
@@ -1082,6 +1089,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings,
integrityManager: mockIntegrityManager,
});
const extensions = await extensionManager.loadExtensions();
@@ -1306,6 +1314,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: blockGitExtensionsSetting,
integrityManager: mockIntegrityManager,
});
await extensionManager.loadExtensions();
await expect(
@@ -1330,6 +1339,7 @@ name = "yolo-checker"
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: allowedExtensionsSetting,
integrityManager: mockIntegrityManager,
});
await extensionManager.loadExtensions();
await expect(
@@ -1677,6 +1687,7 @@ ${INSTALL_WARNING_MESSAGE}`,
requestConsent: mockRequestConsent,
requestSetting: null,
settings: loadSettings(tempWorkspaceDir).merged,
integrityManager: mockIntegrityManager,
});
await extensionManager.loadExtensions();
@@ -16,21 +16,14 @@ import {
} from '@google/gemini-cli-core';
import { ExtensionManager } from '../extension-manager.js';
import { createTestMergedSettings } from '../settings.js';
import { isWorkspaceTrusted } from '../trustedFolders.js';
// --- Mocks ---
vi.mock('node:fs', async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actual = await importOriginal<any>();
const actual = await importOriginal<typeof import('node:fs')>();
return {
...actual,
default: {
...actual.default,
existsSync: vi.fn(),
statSync: vi.fn(),
lstatSync: vi.fn(),
realpathSync: vi.fn((p) => p),
},
existsSync: vi.fn(),
statSync: vi.fn(),
lstatSync: vi.fn(),
@@ -38,6 +31,7 @@ vi.mock('node:fs', async (importOriginal) => {
promises: {
...actual.promises,
mkdir: vi.fn(),
readdir: vi.fn(),
writeFile: vi.fn(),
rm: vi.fn(),
cp: vi.fn(),
@@ -75,6 +69,20 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
Config: vi.fn().mockImplementation(() => ({
getEnableExtensionReloading: vi.fn().mockReturnValue(true),
})),
KeychainService: class {
isAvailable = vi.fn().mockResolvedValue(true);
getPassword = vi.fn().mockResolvedValue('test-key');
setPassword = vi.fn().mockResolvedValue(undefined);
},
ExtensionIntegrityManager: class {
verify = vi.fn().mockResolvedValue('verified');
store = vi.fn().mockResolvedValue(undefined);
},
IntegrityDataStatus: {
VERIFIED: 'verified',
MISSING: 'missing',
INVALID: 'invalid',
},
};
});
@@ -134,13 +142,21 @@ describe('extensionUpdates', () => {
vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.promises.rm).mockResolvedValue(undefined);
vi.mocked(fs.promises.cp).mockResolvedValue(undefined);
vi.mocked(fs.promises.readdir).mockResolvedValue([]);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
vi.mocked(getMissingSettings).mockResolvedValue([]);
// Allow directories to exist by default to satisfy Config/WorkspaceContext checks
vi.mocked(fs.existsSync).mockReturnValue(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as any);
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => true,
} as unknown as fs.Stats);
vi.mocked(fs.lstatSync).mockReturnValue({
isDirectory: () => true,
} as unknown as fs.Stats);
vi.mocked(fs.realpathSync).mockImplementation((p) => p as string);
tempWorkspaceDir = '/mock/workspace';
@@ -202,11 +218,10 @@ describe('extensionUpdates', () => {
]);
vi.spyOn(manager, 'uninstallExtension').mockResolvedValue(undefined);
// Mock loadExtension to return something so the method doesn't crash at the end
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.spyOn(manager as any, 'loadExtension').mockResolvedValue({
vi.spyOn(manager, 'loadExtension').mockResolvedValue({
name: 'test-ext',
version: '1.1.0',
} as GeminiCLIExtension);
} as unknown as GeminiCLIExtension);
// 4. Mock External Helpers
// This is the key fix: we explicitly mock `getMissingSettings` to return
@@ -235,5 +250,52 @@ describe('extensionUpdates', () => {
),
);
});
it('should store integrity data after update', async () => {
const newConfig: ExtensionConfig = {
name: 'test-ext',
version: '1.1.0',
};
const previousConfig: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
};
const installMetadata: ExtensionInstallMetadata = {
source: '/mock/source',
type: 'local',
};
const manager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
settings: createTestMergedSettings(),
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
});
await manager.loadExtensions();
vi.spyOn(manager, 'loadExtensionConfig').mockResolvedValue(newConfig);
vi.spyOn(manager, 'getExtensions').mockReturnValue([
{
name: 'test-ext',
version: '1.0.0',
installMetadata,
path: '/mock/extensions/test-ext',
isActive: true,
} as unknown as GeminiCLIExtension,
]);
vi.spyOn(manager, 'uninstallExtension').mockResolvedValue(undefined);
vi.spyOn(manager, 'loadExtension').mockResolvedValue({
name: 'test-ext',
version: '1.1.0',
} as unknown as GeminiCLIExtension);
const storeSpy = vi.spyOn(manager, 'storeExtensionIntegrity');
await manager.installOrUpdateExtension(installMetadata, previousConfig);
expect(storeSpy).toHaveBeenCalledWith('test-ext', installMetadata);
});
});
});
@@ -15,13 +15,16 @@ import {
type ExtensionUpdateStatus,
} from '../../ui/state/extensions.js';
import { ExtensionStorage } from './storage.js';
import { copyExtension, type ExtensionManager } from '../extension-manager.js';
import { type ExtensionManager, copyExtension } from '../extension-manager.js';
import { checkForExtensionUpdate } from './github.js';
import { loadInstallMetadata } from '../extension.js';
import * as fs from 'node:fs';
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
import {
type GeminiCLIExtension,
type ExtensionInstallMetadata,
IntegrityDataStatus,
} from '@google/gemini-cli-core';
// Mock dependencies
vi.mock('./storage.js', () => ({
ExtensionStorage: {
createTmpDir: vi.fn(),
@@ -64,8 +67,18 @@ describe('Extension Update Logic', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExtensionManager = {
loadExtensionConfig: vi.fn(),
installOrUpdateExtension: vi.fn(),
loadExtensionConfig: vi.fn().mockResolvedValue({
name: 'test-extension',
version: '1.0.0',
}),
installOrUpdateExtension: vi.fn().mockResolvedValue({
...mockExtension,
version: '1.1.0',
}),
verifyExtensionIntegrity: vi
.fn()
.mockResolvedValue(IntegrityDataStatus.VERIFIED),
storeExtensionIntegrity: vi.fn().mockResolvedValue(undefined),
} as unknown as ExtensionManager;
mockDispatch = vi.fn();
@@ -92,7 +105,7 @@ describe('Extension Update Logic', () => {
it('should throw error and set state to ERROR if install metadata type is unknown', async () => {
vi.mocked(loadInstallMetadata).mockReturnValue({
type: undefined,
} as unknown as import('@google/gemini-cli-core').ExtensionInstallMetadata);
} as unknown as ExtensionInstallMetadata);
await expect(
updateExtension(
@@ -295,6 +308,77 @@ describe('Extension Update Logic', () => {
});
expect(fs.promises.rm).toHaveBeenCalled();
});
describe('Integrity Verification', () => {
it('should fail update with security alert if integrity is invalid', async () => {
vi.mocked(
mockExtensionManager.verifyExtensionIntegrity,
).mockResolvedValue(IntegrityDataStatus.INVALID);
await expect(
updateExtension(
mockExtension,
mockExtensionManager,
ExtensionUpdateState.UPDATE_AVAILABLE,
mockDispatch,
),
).rejects.toThrow(
'Extension test-extension cannot be updated. Extension integrity cannot be verified.',
);
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_STATE',
payload: {
name: mockExtension.name,
state: ExtensionUpdateState.ERROR,
},
});
});
it('should establish trust on first update if integrity data is missing', async () => {
vi.mocked(
mockExtensionManager.verifyExtensionIntegrity,
).mockResolvedValue(IntegrityDataStatus.MISSING);
await updateExtension(
mockExtension,
mockExtensionManager,
ExtensionUpdateState.UPDATE_AVAILABLE,
mockDispatch,
);
// Verify updateExtension delegates to installOrUpdateExtension,
// which is responsible for establishing trust internally.
expect(
mockExtensionManager.installOrUpdateExtension,
).toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_STATE',
payload: {
name: mockExtension.name,
state: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
},
});
});
it('should throw if integrity manager throws', async () => {
vi.mocked(
mockExtensionManager.verifyExtensionIntegrity,
).mockRejectedValue(new Error('Verification failed'));
await expect(
updateExtension(
mockExtension,
mockExtensionManager,
ExtensionUpdateState.UPDATE_AVAILABLE,
mockDispatch,
),
).rejects.toThrow(
'Extension test-extension cannot be updated. Verification failed',
);
});
});
});
describe('updateAllUpdatableExtensions', () => {
@@ -15,6 +15,7 @@ import {
debugLogger,
getErrorMessage,
type GeminiCLIExtension,
IntegrityDataStatus,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import { copyExtension, type ExtensionManager } from '../extension-manager.js';
@@ -51,6 +52,26 @@ export async function updateExtension(
`Extension ${extension.name} cannot be updated, type is unknown.`,
);
}
try {
const status = await extensionManager.verifyExtensionIntegrity(
extension.name,
installMetadata,
);
if (status === IntegrityDataStatus.INVALID) {
throw new Error('Extension integrity cannot be verified');
}
} catch (e) {
dispatchExtensionStateUpdate({
type: 'SET_STATE',
payload: { name: extension.name, state: ExtensionUpdateState.ERROR },
});
throw new Error(
`Extension ${extension.name} cannot be updated. ${getErrorMessage(e)}. To fix this, reinstall the extension.`,
);
}
if (installMetadata?.type === 'link') {
dispatchExtensionStateUpdate({
type: 'SET_STATE',
@@ -346,6 +346,12 @@ describe('Policy Engine Integration Tests', () => {
expect(
(await engine.check({ name: 'list_directory' }, undefined)).decision,
).toBe(PolicyDecision.ALLOW);
expect(
(await engine.check({ name: 'get_internal_docs' }, undefined)).decision,
).toBe(PolicyDecision.ALLOW);
expect(
(await engine.check({ name: 'cli_help' }, undefined)).decision,
).toBe(PolicyDecision.ALLOW);
// Other tools should be denied via catch all
expect(
@@ -400,12 +400,10 @@ describe('SettingsSchema', () => {
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(false);
expect(setting.default).toBe(true);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(false);
expect(setting.description).toBe(
'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents',
);
expect(setting.description).toBe('Enable local and remote subagents.');
});
it('should have skills setting enabled by default', () => {
+60 -5
View File
@@ -1053,6 +1053,34 @@ const SETTINGS_SCHEMA = {
ref: 'ModelDefinition',
},
},
modelIdResolutions: {
type: 'object',
label: 'Model ID Resolutions',
category: 'Model',
requiresRestart: true,
default: DEFAULT_MODEL_CONFIGS.modelIdResolutions,
description:
'Rules for resolving requested model names to concrete model IDs based on context.',
showInDialog: false,
additionalProperties: {
type: 'object',
ref: 'ModelResolution',
},
},
classifierIdResolutions: {
type: 'object',
label: 'Classifier ID Resolutions',
category: 'Model',
requiresRestart: true,
default: DEFAULT_MODEL_CONFIGS.classifierIdResolutions,
description:
'Rules for resolving classifier tiers (flash, pro) to concrete model IDs.',
showInDialog: false,
additionalProperties: {
type: 'object',
ref: 'ModelResolution',
},
},
},
},
@@ -1838,9 +1866,8 @@ const SETTINGS_SCHEMA = {
label: 'Enable Agents',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents',
default: true,
description: 'Enable local and remote subagents.',
showInDialog: false,
},
extensionManagement: {
@@ -1895,7 +1922,7 @@ const SETTINGS_SCHEMA = {
label: 'JIT Context Loading',
category: 'Experimental',
requiresRestart: true,
default: false,
default: true,
description: 'Enable Just-In-Time (JIT) context loading.',
showInDialog: false,
},
@@ -2801,7 +2828,7 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] },
family: { type: 'string' },
isPreview: { type: 'boolean' },
dialogLocation: { enum: ['main', 'manual'] },
isVisible: { type: 'boolean' },
dialogDescription: { type: 'string' },
features: {
type: 'object',
@@ -2812,6 +2839,34 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
},
},
},
ModelResolution: {
type: 'object',
description: 'Model resolution rule.',
properties: {
default: { type: 'string' },
contexts: {
type: 'array',
items: {
type: 'object',
properties: {
condition: {
type: 'object',
properties: {
useGemini3_1: { type: 'boolean' },
useCustomTools: { type: 'boolean' },
hasAccessToPreview: { type: 'boolean' },
requestedModels: {
type: 'array',
items: { type: 'string' },
},
},
},
target: { type: 'string' },
},
},
},
},
},
};
export function getSettingsSchema(): SettingsSchemaType {
+9 -1
View File
@@ -419,6 +419,14 @@ export async function main() {
}
}
if (settings.merged.advanced.autoConfigureMemory) {
const heapStats = v8.getHeapStatistics();
const currentMaxOldSpaceSizeMb = Math.floor(
heapStats.heap_size_limit / 1024 / 1024,
);
writeToStderr(`Allocated memory: ${currentMaxOldSpaceSizeMb} MB\n`);
}
// We are now past the logic handling potentially launching a child process
// to run Gemini CLI. It is now safe to perform expensive initialization that
// may have side effects.
@@ -647,7 +655,7 @@ export async function main() {
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}
const prompt_id = Math.random().toString(16).slice(2);
const prompt_id = sessionId;
logUserPrompt(
config,
new UserPromptEvent(
@@ -29,7 +29,7 @@ describe('Model Steering Integration', () => {
configOverrides: { modelSteering: true },
});
await rig.initialize();
rig.render();
await rig.render();
await rig.waitForIdle();
rig.setToolPolicy('list_directory', PolicyDecision.ASK_USER);
+2 -12
View File
@@ -101,18 +101,8 @@ export async function startInteractiveUI(
return (
<SettingsContext.Provider value={settings}>
<KeyMatchersProvider value={matchers}>
<KeypressProvider
config={config}
debugKeystrokeLogging={
settings.merged.general.debugKeystrokeLogging
}
>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
debugKeystrokeLogging={
settings.merged.general.debugKeystrokeLogging
}
>
<KeypressProvider config={config}>
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
<TerminalProvider>
<ScrollProvider>
<OverflowProvider>
@@ -65,9 +65,9 @@ export const handleSlashCommand = async (
const logger = new Logger(config?.getSessionId() || '', config?.storage);
const context: CommandContext = {
const commandContext: CommandContext = {
services: {
config,
agentContext: config,
settings,
git: undefined,
logger,
@@ -84,7 +84,7 @@ export const handleSlashCommand = async (
},
};
const result = await commandToExecute.action(context, args);
const result = await commandToExecute.action(commandContext, args);
if (result) {
switch (result.type) {
@@ -31,11 +31,14 @@ describe('AtFileProcessor', () => {
mockConfig = {
// The processor only passes the config through, so we don't need a full mock.
get config() {
return this;
},
} as unknown as Config;
context = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig,
},
});
@@ -60,7 +63,7 @@ describe('AtFileProcessor', () => {
const prompt: PartUnion[] = [{ text: 'Analyze @{file.txt}' }];
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
const result = await processor.process(prompt, contextWithoutConfig);
@@ -25,7 +25,7 @@ export class AtFileProcessor implements IPromptProcessor {
input: PromptPipelineContent,
context: CommandContext,
): Promise<PromptPipelineContent> {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
return input;
}
@@ -89,6 +89,9 @@ describe('ShellProcessor', () => {
getPolicyEngine: vi.fn().mockReturnValue({
check: mockPolicyEngineCheck,
}),
get config() {
return this as unknown as Config;
},
};
context = createMockCommandContext({
@@ -98,7 +101,7 @@ describe('ShellProcessor', () => {
args: 'default args',
},
services: {
config: mockConfig as Config,
agentContext: mockConfig as Config,
},
session: {
sessionShellAllowlist: new Set(),
@@ -120,7 +123,7 @@ describe('ShellProcessor', () => {
const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}');
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -74,7 +74,7 @@ export class ShellProcessor implements IPromptProcessor {
];
}
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
throw new Error(
`Security configuration not loaded. Cannot verify shell command permissions for '${this.commandName}'. Aborting.`,
+2 -8
View File
@@ -5,7 +5,6 @@
*/
import { describe, it, afterEach, expect } from 'vitest';
import { act } from 'react';
import { AppRig } from './AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -31,7 +30,7 @@ describe('AppRig', () => {
configOverrides: { modelSteering: true },
});
await rig.initialize();
rig.render();
await rig.render();
await rig.waitForIdle();
// Set breakpoints on the canonical tool names
@@ -69,12 +68,7 @@ describe('AppRig', () => {
);
rig = new AppRig({ fakeResponsesPath });
await rig.initialize();
await act(async () => {
rig!.render();
// Allow async initializations (like banners) to settle within the act boundary
await new Promise((resolve) => setTimeout(resolve, 0));
});
await rig.render();
// Wait for initial render
await rig.waitForIdle();
+22 -12
View File
@@ -11,7 +11,7 @@ import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { AppContainer } from '../ui/AppContainer.js';
import { renderWithProviders } from './render.js';
import { renderWithProviders, type RenderInstance } from './render.js';
import {
makeFakeConfig,
type Config,
@@ -30,6 +30,7 @@ import {
IdeClient,
debugLogger,
CoreToolCallStatus,
IntegrityDataStatus,
} from '@google/gemini-cli-core';
import {
type MockShellCommand,
@@ -118,6 +119,12 @@ class MockExtensionManager extends ExtensionLoader {
getExtensions = vi.fn().mockReturnValue([]);
setRequestConsent = vi.fn();
setRequestSetting = vi.fn();
integrityManager = {
verifyExtensionIntegrity: vi
.fn()
.mockResolvedValue(IntegrityDataStatus.VERIFIED),
storeExtensionIntegrity: vi.fn().mockResolvedValue(undefined),
};
}
// Mock GeminiRespondingSpinner to disable animations (avoiding 'act()' warnings) without triggering screen reader mode.
@@ -148,7 +155,7 @@ export interface PendingConfirmation {
}
export class AppRig {
private renderResult: ReturnType<typeof renderWithProviders> | undefined;
private renderResult: RenderInstance | undefined;
private config: Config | undefined;
private settings: LoadedSettings | undefined;
private testDir: string;
@@ -197,6 +204,7 @@ export class AppRig {
enableEventDrivenScheduler: true,
extensionLoader: new MockExtensionManager(),
excludeTools: this.options.configOverrides?.excludeTools,
useAlternateBuffer: false,
...this.options.configOverrides,
};
this.config = makeFakeConfig(configParams);
@@ -268,19 +276,22 @@ export class AppRig {
enabled: false,
hasSeenNudge: true,
},
ui: {
useAlternateBuffer: false,
},
},
});
}
private stubRefreshAuth() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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',
};
@@ -382,12 +393,12 @@ export class AppRig {
return isAnyToolActive || isAwaitingConfirmation;
}
render() {
async render() {
if (!this.config || !this.settings)
throw new Error('AppRig not initialized');
act(() => {
this.renderResult = renderWithProviders(
await act(async () => {
this.renderResult = await renderWithProviders(
<AppContainer
config={this.config!}
version="test-version"
@@ -403,7 +414,6 @@ export class AppRig {
config: this.config!,
settings: this.settings!,
width: this.options.terminalWidth ?? 120,
useAlternateBuffer: false,
uiState: {
terminalHeight: this.options.terminalHeight ?? 40,
},
@@ -449,7 +459,7 @@ export class AppRig {
const actualToolName = toolName === '*' ? undefined : toolName;
this.config
.getPolicyEngine()
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
.removeRulesForTool(actualToolName as string, source);
this.breakpointTools.delete(toolName);
}
@@ -617,7 +627,7 @@ export class AppRig {
async addUserHint(hint: string) {
if (!this.config) throw new Error('AppRig not initialized');
await act(async () => {
this.config!.userHintService.addUserHint(hint);
this.config!.injectionService.addInjection(hint, 'user_steering');
});
}
@@ -722,7 +732,7 @@ export class AppRig {
.getGeminiClient()
?.getChatRecordingService();
if (recordingService) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(recordingService as any).conversationFile = null;
}
}
@@ -742,7 +752,7 @@ export class AppRig {
MockShellExecutionService.reset();
ideContextStore.clear();
// Forcefully clear IdeClient singleton promise
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(IdeClient as any).instancePromise = null;
vi.clearAllMocks();
@@ -46,15 +46,19 @@ describe('createMockCommandContext', () => {
const overrides = {
services: {
config: mockConfig,
agentContext: { config: mockConfig },
},
};
const context = createMockCommandContext(overrides);
expect(context.services.config).toBeDefined();
expect(context.services.config?.getModel()).toBe('gemini-pro');
expect(context.services.config?.getProjectRoot()).toBe('/test/project');
expect(context.services.agentContext).toBeDefined();
expect(context.services.agentContext?.config?.getModel()).toBe(
'gemini-pro',
);
expect(context.services.agentContext?.config?.getProjectRoot()).toBe(
'/test/project',
);
// Verify a default property on the same nested object is still there
expect(context.services.logger).toBeDefined();
@@ -36,15 +36,15 @@ export const createMockCommandContext = (
args: '',
},
services: {
config: null,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
agentContext: null,
settings: {
merged: defaultMergedSettings,
setValue: vi.fn(),
forScope: vi.fn().mockReturnValue({ settings: {} }),
} as unknown as LoadedSettings,
git: undefined as GitService | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
logger: {
log: vi.fn(),
logMessage: vi.fn(),
@@ -53,7 +53,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, @typescript-eslint/no-unsafe-assignment
ui: {
addItem: vi.fn(),
clear: vi.fn(),
@@ -72,7 +72,7 @@ export const createMockCommandContext = (
} as any,
session: {
sessionShellAllowlist: new Set<string>(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stats: {
sessionStartTime: new Date(),
lastPromptTokenCount: 0,
@@ -93,14 +93,12 @@ 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 (
@@ -108,11 +106,10 @@ 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;
}
}
@@ -120,6 +117,5 @@ export const createMockCommandContext = (
return output;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return merge(defaultMocks, overrides);
};
+2 -3
View File
@@ -17,7 +17,6 @@ import {
* Creates a mocked Config object with default values and allows overrides.
*/
export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
({
getSandbox: vi.fn(() => undefined),
getQuestion: vi.fn(() => ''),
@@ -79,6 +78,8 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getFileService: vi.fn().mockReturnValue({}),
getGitService: vi.fn().mockResolvedValue({}),
getUserMemory: vi.fn().mockReturnValue(''),
getSystemInstructionMemory: vi.fn().mockReturnValue(''),
getSessionMemory: vi.fn().mockReturnValue(''),
getGeminiMdFilePaths: vi.fn().mockReturnValue([]),
getShowMemoryUsage: vi.fn().mockReturnValue(false),
getAccessibility: vi.fn().mockReturnValue({}),
@@ -182,11 +183,9 @@ export function createMockSettings(
overrides: Record<string, unknown> = {},
): LoadedSettings {
const merged = createTestMergedSettings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(overrides['merged'] as Partial<Settings>) || {},
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
system: { settings: {} },
systemDefaults: { settings: {} },
+45 -119
View File
@@ -16,9 +16,7 @@ import { vi } from 'vitest';
import stripAnsi from 'strip-ansi';
import type React from 'react';
import { act, useState } from 'react';
import os from 'node:os';
import path from 'node:path';
import { LoadedSettings } from '../config/settings.js';
import type { LoadedSettings } from '../config/settings.js';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { SettingsContext } from '../ui/contexts/SettingsContext.js';
import { ShellFocusContext } from '../ui/contexts/ShellFocusContext.js';
@@ -44,7 +42,7 @@ import {
type OverflowState,
} from '../ui/contexts/OverflowContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
import { createMockSettings } from './settings.js';
@@ -53,6 +51,7 @@ import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
import { generateSvgForTerminal } from './svg.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
export const persistentStateMock = new FakePersistentState();
@@ -66,7 +65,9 @@ if (process.env['NODE_ENV'] === 'test') {
}
vi.mock('../utils/persistentState.js', () => ({
persistentState: persistentStateMock,
get persistentState() {
return persistentStateMock;
},
}));
vi.mock('../ui/utils/terminalUtils.js', () => ({
@@ -416,11 +417,10 @@ export const render = (
stdout.clear();
act(() => {
instance = inkRenderDirect(tree, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stdout: stdout as unknown as NodeJS.WriteStream,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stderr: stderr as unknown as NodeJS.WriteStream,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stdin: stdin as unknown as NodeJS.ReadStream,
debug: false,
exitOnCtrlC: false,
@@ -487,60 +487,7 @@ export const simulateClick = async (
});
};
let mockConfigInternal: Config | undefined;
const getMockConfigInternal = (): Config => {
if (!mockConfigInternal) {
mockConfigInternal = makeFakeConfig({
targetDir: os.tmpdir(),
enableEventDrivenScheduler: true,
});
}
return mockConfigInternal;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const configProxy = new Proxy({} as Config, {
get(_target, prop) {
if (prop === 'getTargetDir') {
return () =>
path.join(
path.parse(process.cwd()).root,
'Users',
'test',
'project',
'foo',
'bar',
'and',
'some',
'more',
'directories',
'to',
'make',
'it',
'long',
);
}
if (prop === 'getUseBackgroundColor') {
return () => true;
}
const internal = getMockConfigInternal();
if (prop in internal) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return internal[prop as keyof typeof internal];
}
throw new Error(`mockConfig does not have property ${String(prop)}`);
},
});
export const mockSettings = new LoadedSettings(
{ path: '', settings: {}, originalSettings: {} },
{ path: '', settings: {}, originalSettings: {} },
{ path: '', settings: {}, originalSettings: {} },
{ path: '', settings: {}, originalSettings: {} },
true,
[],
);
export const mockSettings = createMockSettings();
// A minimal mock UIState to satisfy the context provider.
// Tests that need specific UIState values should provide their own.
@@ -649,7 +596,7 @@ const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
return <>{children}</>;
};
export const renderWithProviders = (
export const renderWithProviders = async (
component: React.ReactElement,
{
shellFocus = true,
@@ -657,9 +604,7 @@ export const renderWithProviders = (
uiState: providedUiState,
width,
mouseEventsEnabled = false,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
config = configProxy as unknown as Config,
useAlternateBuffer = true,
config,
uiActions,
persistentState,
appState = mockAppState,
@@ -670,7 +615,6 @@ export const renderWithProviders = (
width?: number;
mouseEventsEnabled?: boolean;
config?: Config;
useAlternateBuffer?: boolean;
uiActions?: Partial<UIActions>;
persistentState?: {
get?: typeof persistentStateMock.get;
@@ -678,27 +622,26 @@ export const renderWithProviders = (
};
appState?: AppState;
} = {},
): RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
} => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
): Promise<
RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
}
> => {
const baseState: UIState = new Proxy(
{ ...baseMockUiState, ...providedUiState },
{
get(target, prop) {
if (prop in target) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return target[prop as keyof typeof target];
}
// For properties not in the base mock or provided state,
// we'll check the original proxy to see if it's a defined but
// unprovided property, and if not, throw.
if (prop in baseMockUiState) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return baseMockUiState[prop as keyof typeof baseMockUiState];
}
throw new Error(`mockUiState does not have property ${String(prop)}`);
@@ -716,30 +659,14 @@ export const renderWithProviders = (
persistentStateMock.mockClear();
const terminalWidth = width ?? baseState.terminalWidth;
let finalSettings = settings;
if (useAlternateBuffer !== undefined) {
finalSettings = createMockSettings({
...settings.merged,
ui: {
...settings.merged.ui,
useAlternateBuffer,
},
});
}
// Wrap config in a Proxy so useAlternateBuffer hook (which reads from Config) gets the correct value,
// without replacing the entire config object and its other values.
let finalConfig = config;
if (useAlternateBuffer !== undefined) {
finalConfig = new Proxy(config, {
get(target, prop, receiver) {
if (prop === 'getUseAlternateBuffer') {
return () => useAlternateBuffer;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Reflect.get(target, prop, receiver);
},
});
if (!config) {
config = await loadCliConfig(
settings.merged,
'random-session-id',
{} as unknown as CliArgs,
{ cwd: '/' },
);
}
const mainAreaWidth = terminalWidth;
@@ -768,10 +695,10 @@ export const renderWithProviders = (
capturedOverflowState = undefined;
capturedOverflowActions = undefined;
const renderResult = render(
const wrapWithProviders = (comp: React.ReactElement) => (
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}>
<ConfigContext.Provider value={config}>
<SettingsContext.Provider value={settings}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider>
<ShellFocusContext.Provider value={shellFocus}>
@@ -782,7 +709,7 @@ export const renderWithProviders = (
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={finalConfig}
config={config}
toolCalls={allToolCalls}
>
<AskUserActionsProvider
@@ -803,7 +730,7 @@ export const renderWithProviders = (
flexGrow={0}
flexDirection="column"
>
{component}
{comp}
</Box>
</ContextCapture>
</ScrollProvider>
@@ -821,12 +748,16 @@ export const renderWithProviders = (
</UIStateContext.Provider>
</SettingsContext.Provider>
</ConfigContext.Provider>
</AppContext.Provider>,
terminalWidth,
</AppContext.Provider>
);
const renderResult = render(wrapWithProviders(component), terminalWidth);
return {
...renderResult,
rerender: (newComponent: React.ReactElement) => {
renderResult.rerender(wrapWithProviders(newComponent));
},
capturedOverflowState,
capturedOverflowActions,
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
@@ -847,9 +778,8 @@ export function renderHook<Result, Props>(
waitUntilReady: () => Promise<void>;
generateSvg: () => string;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
let currentProps = options?.initialProps as Props;
function TestComponent({
@@ -884,7 +814,6 @@ export function renderHook<Result, Props>(
function rerender(props?: Props) {
if (arguments.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
currentProps = props as Props;
}
act(() => {
@@ -899,7 +828,7 @@ export function renderHook<Result, Props>(
return { result, rerender, unmount, waitUntilReady, generateSvg };
}
export function renderHookWithProviders<Result, Props>(
export async function renderHookWithProviders<Result, Props>(
renderCallback: (props: Props) => Result,
options: {
initialProps?: Props;
@@ -911,16 +840,14 @@ export function renderHookWithProviders<Result, Props>(
width?: number;
mouseEventsEnabled?: boolean;
config?: Config;
useAlternateBuffer?: boolean;
} = {},
): {
): Promise<{
result: { current: Result };
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
generateSvg: () => string;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
}> {
const result = { current: undefined as unknown as Result };
let setPropsFn: ((props: Props) => void) | undefined;
@@ -939,10 +866,10 @@ export function renderHookWithProviders<Result, Props>(
let renderResult: ReturnType<typeof render>;
act(() => {
renderResult = renderWithProviders(
await act(async () => {
renderResult = await renderWithProviders(
<Wrapper>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */}
{}
<TestComponent initialProps={options.initialProps as Props} />
</Wrapper>,
options,
@@ -952,7 +879,6 @@ export function renderHookWithProviders<Result, Props>(
function rerender(newProps?: Props) {
act(() => {
if (arguments.length > 0 && setPropsFn) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
setPropsFn(newProps as Props);
} else if (forceUpdateFn) {
forceUpdateFn();
+4 -6
View File
@@ -46,23 +46,22 @@ export const createMockSettings = (
workspace,
isTrusted,
errors,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
merged: mergedOverride,
...settingsOverrides
} = overrides;
const loaded = new LoadedSettings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(system as any) || { path: '', settings: {}, originalSettings: {} },
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(systemDefaults as any) || { path: '', settings: {}, originalSettings: {} },
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(user as any) || {
path: '',
settings: settingsOverrides,
originalSettings: settingsOverrides,
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(workspace as any) || { path: '', settings: {}, originalSettings: {} },
isTrusted ?? true,
errors || [],
@@ -76,7 +75,6 @@ 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, @typescript-eslint/no-unsafe-assignment
(loaded as any)[key] = overrides[key];
}
}
+24 -15
View File
@@ -7,6 +7,7 @@
import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest';
import type React from 'react';
import { renderWithProviders } from '../test-utils/render.js';
import { createMockSettings } from '../test-utils/settings.js';
import { Text, useIsScreenReaderEnabled, type DOMElement } from 'ink';
import { App } from './App.js';
import { type UIState } from './contexts/UIStateContext.js';
@@ -93,11 +94,11 @@ describe('App', () => {
};
it('should render main content and composer when not quitting', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
useAlternateBuffer: false,
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
@@ -114,11 +115,11 @@ describe('App', () => {
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: quittingUIState,
useAlternateBuffer: false,
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
@@ -135,11 +136,11 @@ describe('App', () => {
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: quittingUIState,
useAlternateBuffer: true,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -155,10 +156,11 @@ describe('App', () => {
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: dialogUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -181,10 +183,11 @@ describe('App', () => {
[stateKey]: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -197,10 +200,11 @@ describe('App', () => {
it('should render ScreenReaderAppLayout when screen reader is enabled', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -215,10 +219,11 @@ describe('App', () => {
it('should render DefaultAppLayout when screen reader is not enabled', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -265,15 +270,16 @@ describe('App', () => {
],
} as UIState;
const configWithExperiment = makeFakeConfig();
const configWithExperiment = makeFakeConfig({ useAlternateBuffer: true });
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: stateWithConfirmingTool,
config: configWithExperiment,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -289,10 +295,11 @@ describe('App', () => {
describe('Snapshots', () => {
it('renders default layout correctly', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -302,10 +309,11 @@ describe('App', () => {
it('renders screen reader layout correctly', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -318,10 +326,11 @@ describe('App', () => {
...mockUIState,
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: dialogUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
+100 -216
View File
@@ -95,7 +95,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
};
});
import ansiEscapes from 'ansi-escapes';
import { mergeSettings, type LoadedSettings } from '../config/settings.js';
import { type LoadedSettings } from '../config/settings.js';
import { createMockSettings } from '../test-utils/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { StreamingState } from './types.js';
@@ -211,7 +212,7 @@ import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
@@ -293,7 +294,7 @@ describe('AppContainer State Management', () => {
const mockedUseSettingsCommand = useSettingsCommand as Mock;
const mockedUseModelCommand = useModelCommand as Mock;
const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock;
const mockedUseConsoleMessages = useConsoleMessages as Mock;
const mockedUseConsoleMessages = useErrorCount as Mock;
const mockedUseGeminiStream = useGeminiStream as Mock;
const mockedUseVim = useVim as Mock;
const mockedUseFolderTrust = useFolderTrust as Mock;
@@ -395,9 +396,9 @@ describe('AppContainer State Management', () => {
confirmationRequest: null,
});
mockedUseConsoleMessages.mockReturnValue({
consoleMessages: [],
errorCount: 0,
handleNewMessage: vi.fn(),
clearConsoleMessages: vi.fn(),
clearErrorCount: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
@@ -484,23 +485,18 @@ describe('AppContainer State Management', () => {
);
// Mock LoadedSettings
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
mockSettings = {
merged: {
...defaultMergedSettings,
hideBanner: false,
hideFooter: false,
hideTips: false,
showMemoryUsage: false,
theme: 'default',
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
useAlternateBuffer: false,
},
mockSettings = createMockSettings({
hideBanner: false,
hideFooter: false,
hideTips: false,
showMemoryUsage: false,
theme: 'default',
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
useAlternateBuffer: false,
},
} as unknown as LoadedSettings;
});
// Mock InitializationResult
mockInitResult = {
@@ -1008,16 +1004,12 @@ describe('AppContainer State Management', () => {
describe('Settings Integration', () => {
it('handles settings with all display options disabled', async () => {
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const settingsAllHidden = {
merged: {
...defaultMergedSettings,
hideBanner: true,
hideFooter: true,
hideTips: true,
showMemoryUsage: false,
},
} as unknown as LoadedSettings;
const settingsAllHidden = createMockSettings({
hideBanner: true,
hideFooter: true,
hideTips: true,
showMemoryUsage: false,
});
let unmount: () => void;
await act(async () => {
@@ -1029,16 +1021,9 @@ describe('AppContainer State Management', () => {
});
it('handles settings with memory usage enabled', async () => {
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const settingsWithMemory = {
merged: {
...defaultMergedSettings,
hideBanner: false,
hideFooter: false,
hideTips: false,
showMemoryUsage: true,
},
} as unknown as LoadedSettings;
const settingsWithMemory = createMockSettings({
showMemoryUsage: true,
});
let unmount: () => void;
await act(async () => {
@@ -1078,9 +1063,7 @@ describe('AppContainer State Management', () => {
});
it('handles undefined settings gracefully', async () => {
const undefinedSettings = {
merged: mergeSettings({}, {}, {}, {}, true),
} as LoadedSettings;
const undefinedSettings = createMockSettings();
let unmount: () => void;
await act(async () => {
@@ -1498,18 +1481,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title with Working… when showStatusInTitle is false', () => {
// Arrange: Set up mock settings with showStatusInTitle disabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithShowStatusFalse = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
},
const mockSettingsWithShowStatusFalse = createMockSettings({
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state as Active
mockedUseGeminiStream.mockReturnValue({
@@ -1537,17 +1514,12 @@ describe('AppContainer State Management', () => {
it('should use legacy terminal title when dynamicWindowTitle is false', () => {
// Arrange: Set up mock settings with dynamicWindowTitle disabled
const mockSettingsWithDynamicTitleFalse = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
dynamicWindowTitle: false,
hideWindowTitle: false,
},
const mockSettingsWithDynamicTitleFalse = createMockSettings({
ui: {
dynamicWindowTitle: false,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state
mockedUseGeminiStream.mockReturnValue({
@@ -1575,18 +1547,12 @@ describe('AppContainer State Management', () => {
it('should not update terminal title when hideWindowTitle is true', () => {
// Arrange: Set up mock settings with hideWindowTitle enabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithHideTitleTrue = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: true,
},
const mockSettingsWithHideTitleTrue = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: true,
},
} as unknown as LoadedSettings;
});
// Act: Render the container
const { unmount } = renderAppContainer({
@@ -1604,18 +1570,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title with thought subject when in active state', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state and thought
const thoughtSubject = 'Processing request';
@@ -1644,18 +1604,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title with default text when in Idle state and no thought subject', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state as Idle with no thought
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
@@ -1679,18 +1633,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title when in WaitingForConfirmation state with thought subject', async () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state and thought
const thoughtSubject = 'Confirm tool execution';
@@ -1742,17 +1690,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock an active shell pty but not focused
mockedUseGeminiStream.mockReturnValue({
@@ -1801,17 +1744,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock an active shell pty with redirection active
mockedUseGeminiStream.mockReturnValue({
@@ -1871,17 +1809,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock an active shell pty with NO output since operation started (silent)
mockedUseGeminiStream.mockReturnValue({
@@ -1921,17 +1854,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock an active shell pty but not focused
let lastOutputTime = startTime + 1000;
@@ -2005,18 +1933,12 @@ describe('AppContainer State Management', () => {
it('should pad title to exactly 80 characters', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state and thought with a short subject
const shortTitle = 'Short';
@@ -2046,18 +1968,12 @@ describe('AppContainer State Management', () => {
it('should use correct ANSI escape code format', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithTitleEnabled = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
},
const mockSettingsWithTitleEnabled = createMockSettings({
ui: {
showStatusInTitle: true,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock the streaming state and thought
const title = 'Test Title';
@@ -2085,17 +2001,12 @@ describe('AppContainer State Management', () => {
it('should use CLI_TITLE environment variable when set', () => {
// Arrange: Set up mock settings with showStatusInTitle disabled (so it shows suffix)
const mockSettingsWithTitleDisabled = {
...mockSettings,
merged: {
...mockSettings.merged,
ui: {
...mockSettings.merged.ui,
showStatusInTitle: false,
hideWindowTitle: false,
},
const mockSettingsWithTitleDisabled = createMockSettings({
ui: {
showStatusInTitle: false,
hideWindowTitle: false,
},
} as unknown as LoadedSettings;
});
// Mock CLI_TITLE environment variable
vi.stubEnv('CLI_TITLE', 'Custom Gemini Title');
@@ -2664,17 +2575,9 @@ describe('AppContainer State Management', () => {
);
// Update settings for this test run
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const testSettings = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
useAlternateBuffer: isAlternateMode,
},
},
} as unknown as LoadedSettings;
const testSettings = createMockSettings({
ui: { useAlternateBuffer: isAlternateMode },
});
function TestChild() {
useKeypress(childHandler || (() => {}), {
@@ -3384,13 +3287,7 @@ describe('AppContainer State Management', () => {
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
settings: {
...mockSettings,
merged: {
...mockSettings.merged,
ui: { ...mockSettings.merged.ui, useAlternateBuffer: false },
},
} as LoadedSettings,
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
}).unmount;
});
@@ -3426,13 +3323,7 @@ describe('AppContainer State Management', () => {
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
settings: {
...mockSettings,
merged: {
...mockSettings.merged,
ui: { ...mockSettings.merged.ui, useAlternateBuffer: true },
},
} as LoadedSettings,
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
}).unmount;
});
@@ -3701,16 +3592,9 @@ describe('AppContainer State Management', () => {
});
it('DOES 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;
const settingsWithAlternateBuffer = createMockSettings({
ui: { useAlternateBuffer: true },
});
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
+13 -34
View File
@@ -85,6 +85,7 @@ import {
buildUserSteeringHintPrompt,
logBillingEvent,
ApiKeyUpdatedEvent,
type InjectionSource,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -102,7 +103,7 @@ import {
useOverflowActions,
useOverflowState,
} from './contexts/OverflowContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
import { calculateMainAreaWidth } from './utils/ui-sizing.js';
@@ -551,8 +552,7 @@ export const AppContainer = (props: AppContainerProps) => {
};
}, [settings]);
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
useConsoleMessages();
const { errorCount, clearErrorCount } = useErrorCount();
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config);
// Derive widths for InputPrompt using shared helper
@@ -1089,13 +1089,16 @@ Logging in with Google... Restarting Gemini CLI to continue.
}, []);
useEffect(() => {
const hintListener = (hint: string) => {
pendingHintsRef.current.push(hint);
const hintListener = (text: string, source: InjectionSource) => {
if (source !== 'user_steering') {
return;
}
pendingHintsRef.current.push(text);
setPendingHintCount((prev) => prev + 1);
};
config.userHintService.onUserHint(hintListener);
config.injectionService.onInjection(hintListener);
return () => {
config.userHintService.offUserHint(hintListener);
config.injectionService.offInjection(hintListener);
};
}, [config]);
@@ -1259,7 +1262,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (!trimmed) {
return;
}
config.userHintService.addUserHint(trimmed);
config.injectionService.addInjection(trimmed, 'user_steering');
// Render hints with a distinct style.
historyManager.addItem({
type: 'hint',
@@ -1368,11 +1371,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
triggerExpandHint(null);
historyManager.clearItems();
clearConsoleMessagesState();
clearErrorCount();
refreshStatic();
}, [
historyManager,
clearConsoleMessagesState,
clearErrorCount,
refreshStatic,
reset,
triggerExpandHint,
@@ -1673,11 +1676,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
const handleGlobalKeypress = useCallback(
(key: Key): boolean => {
// Debug log keystrokes if enabled
if (settings.merged.general.debugKeystrokeLogging) {
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
}
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
setShortcutsHelpVisible(false);
}
@@ -1856,7 +1854,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
activePtyId,
handleSuspend,
embeddedShellFocused,
settings.merged.general.debugKeystrokeLogging,
refreshStatic,
setCopyModeEnabled,
tabFocusTimeoutRef,
@@ -1985,22 +1982,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
};
}, [historyManager]);
const filteredConsoleMessages = useMemo(() => {
if (config.getDebugMode()) {
return consoleMessages;
}
return consoleMessages.filter((msg) => msg.type !== 'debug');
}, [consoleMessages, config]);
// Computed values
const errorCount = useMemo(
() =>
filteredConsoleMessages
.filter((msg) => msg.type === 'error')
.reduce((total, msg) => total + msg.count, 0),
[filteredConsoleMessages],
);
const nightly = props.version.includes('nightly');
const dialogsVisible =
@@ -2235,7 +2216,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
ctrlCPressedOnce: ctrlCPressCount >= 1,
@@ -2363,7 +2343,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
ctrlCPressCount,
@@ -5,10 +5,9 @@
*/
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { render } from '../test-utils/render.js';
import { renderWithProviders } from '../test-utils/render.js';
import { act } from 'react';
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
import { debugLogger } from '@google/gemini-cli-core';
// Mock debugLogger
@@ -54,10 +53,8 @@ describe('IdeIntegrationNudge', () => {
});
it('renders correctly with default options', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} />
</KeypressProvider>,
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} />,
);
await waitUntilReady();
const frame = lastFrame();
@@ -71,10 +68,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "Yes" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -94,10 +89,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "No" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -122,10 +115,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "Dismiss" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -155,10 +146,8 @@ describe('IdeIntegrationNudge', () => {
it('handles Escape key press', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
@@ -184,11 +173,10 @@ describe('IdeIntegrationNudge', () => {
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp');
const onComplete = vi.fn();
const { lastFrame, stdin, waitUntilReady, unmount } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
);
const { lastFrame, stdin, waitUntilReady, unmount } =
await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
+17 -17
View File
@@ -143,7 +143,7 @@ describe('AuthDialog', () => {
for (const [key, value] of Object.entries(env)) {
vi.stubEnv(key, value as string);
}
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -161,7 +161,7 @@ describe('AuthDialog', () => {
it('filters auth types when enforcedType is set', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -173,7 +173,7 @@ describe('AuthDialog', () => {
it('sets initial index to 0 when enforcedType is set', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -213,7 +213,7 @@ describe('AuthDialog', () => {
},
])('selects initial auth type $desc', async ({ setup, expected }) => {
setup();
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -226,7 +226,7 @@ describe('AuthDialog', () => {
describe('handleAuthSelect', () => {
it('calls onAuthError if validation fails', async () => {
mockedValidateAuthMethod.mockReturnValue('Invalid method');
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -245,7 +245,7 @@ describe('AuthDialog', () => {
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -261,7 +261,7 @@ describe('AuthDialog', () => {
it('sets auth context with empty object for other auth types', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -278,7 +278,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -297,7 +297,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
// props.settings.merged.security.auth.selectedType is undefined here
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -316,7 +316,7 @@ describe('AuthDialog', () => {
// process.env['GEMINI_API_KEY'] is not set
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -337,7 +337,7 @@ describe('AuthDialog', () => {
props.settings.merged.security.auth.selectedType =
AuthType.LOGIN_WITH_GOOGLE;
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -360,7 +360,7 @@ describe('AuthDialog', () => {
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -383,7 +383,7 @@ describe('AuthDialog', () => {
it('displays authError when provided', async () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -429,7 +429,7 @@ describe('AuthDialog', () => {
},
])('$desc', async ({ setup, expectations }) => {
setup();
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -442,7 +442,7 @@ describe('AuthDialog', () => {
describe('Snapshots', () => {
it('renders correctly with default props', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -452,7 +452,7 @@ describe('AuthDialog', () => {
it('renders correctly with auth error', async () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -462,7 +462,7 @@ describe('AuthDialog', () => {
it('renders correctly with enforced auth type', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -73,7 +73,7 @@ describe('BannedAccountDialog', () => {
});
it('renders the suspension message from accountSuspensionInfo', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -89,7 +89,7 @@ describe('BannedAccountDialog', () => {
});
it('renders menu options with appeal link text from response', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -109,7 +109,7 @@ describe('BannedAccountDialog', () => {
const infoWithoutUrl: AccountSuspensionInfo = {
message: 'Account suspended.',
};
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={infoWithoutUrl}
onExit={onExit}
@@ -129,7 +129,7 @@ describe('BannedAccountDialog', () => {
message: 'Account suspended.',
appealUrl: 'https://example.com/appeal',
};
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={infoWithoutLinkText}
onExit={onExit}
@@ -143,7 +143,7 @@ describe('BannedAccountDialog', () => {
});
it('opens browser when appeal option is selected', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -162,7 +162,7 @@ describe('BannedAccountDialog', () => {
it('shows URL when browser cannot be launched', async () => {
mockedShouldLaunchBrowser.mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -180,7 +180,7 @@ describe('BannedAccountDialog', () => {
});
it('calls onExit when "Exit" is selected', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -196,7 +196,7 @@ describe('BannedAccountDialog', () => {
});
it('calls onChangeAuth when "Change authentication" is selected', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -212,7 +212,7 @@ describe('BannedAccountDialog', () => {
});
it('exits on escape key', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -227,7 +227,7 @@ describe('BannedAccountDialog', () => {
});
it('renders snapshot correctly', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -36,10 +36,12 @@ describe('aboutCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
agentContext: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
},
},
settings: {
merged: {
@@ -57,9 +59,10 @@ describe('aboutCommand', () => {
} as unknown as CommandContext);
vi.mocked(getVersion).mockResolvedValue('test-version');
vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
'test-model',
);
vi.spyOn(
mockContext.services.agentContext!.config,
'getModel',
).mockReturnValue('test-model');
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
Object.defineProperty(process, 'platform', {
value: 'test-os',
@@ -160,9 +163,9 @@ describe('aboutCommand', () => {
});
it('should display the tier when getUserTierName returns a value', async () => {
vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue(
'Enterprise Tier',
);
vi.mocked(
mockContext.services.agentContext!.config.getUserTierName,
).mockReturnValue('Enterprise Tier');
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
+4 -3
View File
@@ -34,7 +34,8 @@ export const aboutCommand: SlashCommand = {
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion = context.services.config?.getModel() || 'Unknown';
const modelVersion =
context.services.agentContext?.config.getModel() || 'Unknown';
const cliVersion = await getVersion();
const selectedAuthType =
context.services.settings.merged.security.auth.selectedType || '';
@@ -48,7 +49,7 @@ export const aboutCommand: SlashCommand = {
});
const userEmail = cachedAccount ?? undefined;
const tier = context.services.config?.getUserTierName();
const tier = context.services.agentContext?.config.getUserTierName();
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
type: MessageType.ABOUT,
@@ -68,7 +69,7 @@ export const aboutCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
if (!context.services.agentContext?.config.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -26,6 +26,7 @@ describe('agentsCommand', () => {
let mockContext: ReturnType<typeof createMockCommandContext>;
let mockConfig: {
getAgentRegistry: ReturnType<typeof vi.fn>;
config: Config;
};
beforeEach(() => {
@@ -37,11 +38,14 @@ describe('agentsCommand', () => {
getAllAgentNames: vi.fn().mockReturnValue([]),
reload: vi.fn(),
}),
get config() {
return this as unknown as Config;
},
};
mockContext = createMockCommandContext({
services: {
config: mockConfig as unknown as Config,
agentContext: mockConfig as unknown as Config,
settings: {
workspace: { path: '/mock/path' },
merged: { agents: { overrides: {} } },
@@ -53,7 +57,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -226,7 +230,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for enable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const enableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'enable',
@@ -332,7 +336,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for disable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const disableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'disable',
@@ -433,7 +437,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const configCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'config',
+11 -8
View File
@@ -21,7 +21,7 @@ const agentsListCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -61,7 +61,8 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -137,7 +138,8 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -216,7 +218,7 @@ async function configAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -266,7 +268,8 @@ async function configAction(
}
function completeAgentsToEnable(context: CommandContext, partialArg: string) {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) return [];
const overrides = settings.merged.agents.overrides;
@@ -278,7 +281,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
}
function completeAgentsToDisable(context: CommandContext, partialArg: string) {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -287,7 +290,7 @@ function completeAgentsToDisable(context: CommandContext, partialArg: string) {
}
function completeAllAgents(context: CommandContext, partialArg: string) {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -328,7 +331,7 @@ const agentsReloadCommand: SlashCommand = {
description: 'Reload the agent registry',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
const agentRegistry = config?.getAgentRegistry();
if (!agentRegistry) {
return {
@@ -9,6 +9,7 @@ import { authCommand } from './authCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { SettingScope } from '../../config/settings.js';
import type { GeminiClient } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
@@ -24,8 +25,10 @@ describe('authCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: vi.fn(),
agentContext: {
geminiClient: {
stripThoughtsFromHistory: vi.fn(),
},
},
},
});
@@ -101,17 +104,19 @@ describe('authCommand', () => {
const mockStripThoughts = vi.fn();
const mockClient = {
stripThoughtsFromHistory: mockStripThoughts,
} as unknown as ReturnType<
NonNullable<typeof mockContext.services.config>['getGeminiClient']
>;
if (mockContext.services.config) {
mockContext.services.config.getGeminiClient = vi.fn(() => mockClient);
} as unknown as GeminiClient;
if (mockContext.services.agentContext?.config) {
mockContext.services.agentContext.config.getGeminiClient = vi.fn(
() => mockClient,
);
}
await logoutCommand!.action!(mockContext, '');
expect(mockStripThoughts).toHaveBeenCalled();
expect(
mockContext.services.agentContext?.geminiClient
.stripThoughtsFromHistory,
).toHaveBeenCalled();
});
it('should return logout action to signal explicit state change', async () => {
@@ -123,7 +128,7 @@ describe('authCommand', () => {
it('should handle missing config gracefully', async () => {
const logoutCommand = authCommand.subCommands?.[1];
mockContext.services.config = null;
mockContext.services.agentContext = null;
const result = await logoutCommand!.action!(mockContext, '');
+1 -1
View File
@@ -39,7 +39,7 @@ const authLogoutCommand: SlashCommand = {
undefined,
);
// Strip thoughts from history instead of clearing completely
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
context.services.agentContext?.geminiClient.stripThoughtsFromHistory();
// Return logout action to signal explicit state change
return {
type: 'logout',
+29 -23
View File
@@ -83,16 +83,18 @@ describe('bugCommand', () => {
it('should generate the default GitHub issue URL', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
geminiClient: {
getChat: () => ({
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
},
},
});
@@ -126,18 +128,20 @@ describe('bugCommand', () => {
];
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
geminiClient: {
getChat: () => ({
getHistory: () => history,
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
},
@@ -172,16 +176,18 @@ describe('bugCommand', () => {
'https://internal.bug-tracker.com/new?desc={title}&details={info}';
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getGeminiClient: () => ({
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
geminiClient: {
getChat: () => ({
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
},
},
});
+4 -4
View File
@@ -32,8 +32,8 @@ export const bugCommand: SlashCommand = {
autoExecute: false,
action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim();
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const osVersion = `${process.platform} ${process.version}`;
let sandboxEnv = 'no sandbox';
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
@@ -73,7 +73,7 @@ export const bugCommand: SlashCommand = {
info += `* **IDE Client:** ${ideClient}\n`;
}
const chat = config?.getGeminiClient()?.getChat();
const chat = agentContext?.geminiClient?.getChat();
const history = chat?.getHistory() || [];
let historyFileMessage = '';
let problemValue = bugDescription;
@@ -134,7 +134,7 @@ export const bugCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
if (!context.services.agentContext?.config.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -70,18 +70,19 @@ describe('chatCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getProjectRoot: () => '/project/root',
getGeminiClient: () =>
({
getChat: mockGetChat,
}) as unknown as GeminiClient,
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
agentContext: {
config: {
getProjectRoot: () => '/project/root',
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
},
},
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
geminiClient: {
getChat: mockGetChat,
} as unknown as GeminiClient,
},
logger: {
saveCheckpoint: mockSaveCheckpoint,
@@ -698,7 +699,11 @@ Hi there!`;
beforeEach(() => {
mockGetLatestApiRequest = vi.fn();
mockContext.services.config!.getLatestApiRequest =
if (!mockContext.services.agentContext!.config) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext!.config as any) = {};
}
mockContext.services.agentContext!.config.getLatestApiRequest =
mockGetLatestApiRequest;
vi.spyOn(process, 'cwd').mockReturnValue('/project/root');
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
+8 -6
View File
@@ -35,7 +35,7 @@ const getSavedChatTags = async (
context: CommandContext,
mtSortDesc: boolean,
): Promise<ChatDetail[]> => {
const cfg = context.services.config;
const cfg = context.services.agentContext?.config;
const geminiDir = cfg?.storage?.getProjectTempDir();
if (!geminiDir) {
return [];
@@ -103,7 +103,8 @@ const saveCommand: SlashCommand = {
};
}
const { logger, config } = context.services;
const { logger } = context.services;
const config = context.services.agentContext?.config;
await logger.initialize();
if (!context.overwriteConfirmed) {
@@ -125,7 +126,7 @@ const saveCommand: SlashCommand = {
}
}
const chat = config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
if (!chat) {
return {
type: 'message',
@@ -172,7 +173,8 @@ const resumeCheckpointCommand: SlashCommand = {
};
}
const { logger, config } = context.services;
const { logger } = context.services;
const config = context.services.agentContext?.config;
await logger.initialize();
const checkpoint = await logger.loadCheckpoint(tag);
const conversation = checkpoint.history;
@@ -298,7 +300,7 @@ const shareCommand: SlashCommand = {
};
}
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
if (!chat) {
return {
type: 'message',
@@ -344,7 +346,7 @@ export const debugCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context): Promise<MessageActionReturn> => {
const req = context.services.config?.getLatestApiRequest();
const req = context.services.agentContext?.config.getLatestApiRequest();
if (!req) {
return {
type: 'message',
@@ -36,24 +36,25 @@ describe('clearCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
}) as unknown as GeminiClient,
setSessionId: vi.fn(),
getEnableHooks: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
userHintService: {
clear: mockHintClear,
agentContext: {
config: {
getEnableHooks: vi.fn().mockReturnValue(false),
setSessionId: vi.fn(),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
injectionService: {
clear: mockHintClear,
},
},
geminiClient: {
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
} as unknown as GeminiClient,
},
},
});
@@ -98,7 +99,7 @@ describe('clearCommand', () => {
const nullConfigContext = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
+3 -3
View File
@@ -20,8 +20,8 @@ export const clearCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args) => {
const geminiClient = context.services.config?.getGeminiClient();
const config = context.services.config;
const geminiClient = context.services.agentContext?.geminiClient;
const config = context.services.agentContext?.config;
// Fire SessionEnd hook before clearing
const hookSystem = config?.getHookSystem();
@@ -30,7 +30,7 @@ export const clearCommand: SlashCommand = {
}
// Reset user steering hints
config?.userHintService.clear();
config?.injectionService.clear();
// Start a new conversation recording with a new session ID
// We MUST do this before calling resetChat() so the new ChatRecordingService
@@ -22,11 +22,10 @@ describe('compressCommand', () => {
mockTryCompressChat = vi.fn();
context = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
agentContext: {
geminiClient: {
tryCompressChat: mockTryCompressChat,
} as unknown as GeminiClient,
},
},
});
@@ -39,9 +39,11 @@ export const compressCommand: SlashCommand = {
try {
ui.setPendingItem(pendingMessage);
const promptId = `compress-${Date.now()}`;
const compressed = await context.services.config
?.getGeminiClient()
?.tryCompressChat(promptId, true);
const compressed =
await context.services.agentContext?.geminiClient?.tryCompressChat(
promptId,
true,
);
if (compressed) {
ui.addItem(
{
@@ -29,10 +29,10 @@ describe('copyCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () => ({
agentContext: {
geminiClient: {
getChat: mockGetChat,
}),
},
},
},
});
@@ -301,7 +301,7 @@ describe('copyCommand', () => {
if (!copyCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const result = await copyCommand.action(nullConfigContext, '');
+1 -1
View File
@@ -18,7 +18,7 @@ export const copyCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
const history = chat?.getHistory();
// Get the last message from the AI (model role)
@@ -85,11 +85,14 @@ describe('directoryCommand', () => {
getFileFilteringOptions: () => ({ ignore: [], include: [] }),
setUserMemory: vi.fn(),
setGeminiMdFileCount: vi.fn(),
get config() {
return this;
},
} as unknown as Config;
mockContext = {
services: {
config: mockConfig,
agentContext: mockConfig,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -60,7 +60,7 @@ async function finishAddingDirectories(
}
if (added.length > 0) {
const gemini = config.getGeminiClient();
const gemini = config.geminiClient;
if (gemini) {
await gemini.addDirectoryContext();
@@ -110,9 +110,9 @@ export const directoryCommand: SlashCommand = {
// Filter out existing directories
let filteredSuggestions = suggestions;
if (context.services.config) {
if (context.services.agentContext?.config) {
const workspaceContext =
context.services.config.getWorkspaceContext();
context.services.agentContext.config.getWorkspaceContext();
const existingDirs = new Set(
workspaceContext.getDirectories().map((dir) => path.resolve(dir)),
);
@@ -144,11 +144,11 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext, args: string) => {
const {
ui: { addItem },
services: { config, settings },
services: { agentContext, settings },
} = context;
const [...rest] = args.split(' ');
if (!config) {
if (!agentContext) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
@@ -156,7 +156,7 @@ export const directoryCommand: SlashCommand = {
return;
}
if (config.isRestrictiveSandbox()) {
if (agentContext.config.isRestrictiveSandbox()) {
return {
type: 'message' as const,
messageType: 'error' as const,
@@ -181,7 +181,7 @@ export const directoryCommand: SlashCommand = {
const errors: string[] = [];
const alreadyAdded: string[] = [];
const workspaceContext = config.getWorkspaceContext();
const workspaceContext = agentContext.config.getWorkspaceContext();
const currentWorkspaceDirs = workspaceContext.getDirectories();
const pathsToProcess: string[] = [];
@@ -252,7 +252,7 @@ export const directoryCommand: SlashCommand = {
trustedDirs={added}
errors={errors}
finishAddingDirectories={finishAddingDirectories}
config={config}
config={agentContext.config}
addItem={addItem}
/>
),
@@ -264,7 +264,12 @@ export const directoryCommand: SlashCommand = {
errors.push(...result.errors);
}
await finishAddingDirectories(config, addItem, added, errors);
await finishAddingDirectories(
agentContext.config,
addItem,
added,
errors,
);
return;
},
},
@@ -275,16 +280,16 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext) => {
const {
ui: { addItem },
services: { config },
services: { agentContext },
} = context;
if (!config) {
if (!agentContext) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
const workspaceContext = config.getWorkspaceContext();
const workspaceContext = agentContext.config.getWorkspaceContext();
const directories = workspaceContext.getDirectories();
const directoryList = directories.map((dir) => `- ${dir}`).join('\n');
addItem({
@@ -161,14 +161,16 @@ describe('extensionsCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
agentContext: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
},
},
},
ui: {
@@ -917,7 +919,7 @@ describe('extensionsCommand', () => {
expect(restartAction).not.toBeNull();
mockRestartExtension = vi.fn();
mockContext.services.config!.getExtensionLoader = vi
mockContext.services.agentContext!.config.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: mockGetExtensions,
@@ -927,7 +929,7 @@ describe('extensionsCommand', () => {
});
it('should show a message if no extensions are installed', async () => {
mockContext.services.config!.getExtensionLoader = vi
mockContext.services.agentContext!.config.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: () => [],
@@ -1017,7 +1019,7 @@ describe('extensionsCommand', () => {
});
it('shows an error if no extension loader is available', async () => {
mockContext.services.config!.getExtensionLoader = vi.fn();
mockContext.services.agentContext!.config.getExtensionLoader = vi.fn();
await restartAction!(mockContext, '--all');
@@ -54,8 +54,8 @@ function showMessageIfNoExtensions(
}
async function listAction(context: CommandContext) {
const extensions = context.services.config
? listExtensions(context.services.config)
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -88,8 +88,8 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
(resolve) => (resolveUpdateComplete = resolve),
);
const extensions = context.services.config
? listExtensions(context.services.config)
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -128,7 +128,7 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
},
});
if (names?.length) {
const extensions = listExtensions(context.services.config!);
const extensions = listExtensions(context.services.agentContext!.config);
for (const name of names) {
const extension = extensions.find(
(extension) => extension.name === name,
@@ -156,7 +156,8 @@ async function restartAction(
context: CommandContext,
args: string,
): Promise<void> {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!extensionLoader) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -235,8 +236,8 @@ async function restartAction(
if (failures.length < extensionsToRestart.length) {
try {
await context.services.config?.reloadSkills();
await context.services.config?.getAgentRegistry()?.reload();
await context.services.agentContext?.config.reloadSkills();
await context.services.agentContext?.config.getAgentRegistry()?.reload();
} catch (error) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -274,7 +275,8 @@ async function exploreAction(
const useRegistryUI = settings.experimental?.extensionRegistry;
if (useRegistryUI) {
const extensionManager = context.services.config?.getExtensionLoader();
const extensionManager =
context.services.agentContext?.config.getExtensionLoader();
if (extensionManager instanceof ExtensionManager) {
return {
type: 'custom_dialog' as const,
@@ -331,7 +333,8 @@ function getEnableDisableContext(
names: string[];
scope: SettingScope;
} | null {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -431,7 +434,8 @@ async function enableAction(context: CommandContext, args: string) {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.services.config?.getMcpClientManager();
const mcpClientManager =
context.services.agentContext?.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers ?? {}),
);
@@ -463,7 +467,8 @@ async function installAction(
args: string,
requestConsentOverride?: (consent: string) => Promise<boolean>,
) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -529,7 +534,8 @@ async function installAction(
}
async function linkAction(context: CommandContext, args: string) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -593,7 +599,8 @@ async function linkAction(context: CommandContext, args: string) {
}
async function uninstallAction(context: CommandContext, args: string) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -692,7 +699,8 @@ async function configAction(context: CommandContext, args: string) {
}
}
const extensionManager = context.services.config?.getExtensionLoader();
const extensionManager =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionManager instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -729,7 +737,7 @@ export function completeExtensions(
context: CommandContext,
partialArg: string,
) {
let extensions = context.services.config?.getExtensions() ?? [];
let extensions = context.services.agentContext?.config.getExtensions() ?? [];
if (context.invocation?.name === 'enable') {
extensions = extensions.filter((ext) => !ext.isActive);
@@ -93,7 +93,7 @@ describe('hooksCommand', () => {
// Create mock context with config and settings
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: { config: mockConfig },
settings: mockSettings,
},
});
@@ -141,7 +141,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -225,7 +225,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -338,7 +338,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -470,7 +470,7 @@ describe('hooksCommand', () => {
it('should return empty array when config is not available', () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -567,7 +567,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -691,7 +691,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
+14 -7
View File
@@ -27,7 +27,8 @@ import { HooksDialog } from '../components/HooksDialog.js';
function panelAction(
context: CommandContext,
): MessageActionReturn | OpenCustomDialogActionReturn {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -55,7 +56,8 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -108,7 +110,8 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -163,7 +166,8 @@ function completeEnabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -183,7 +187,8 @@ function completeDisabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -209,7 +214,8 @@ function getHookDisplayName(hook: HookRegistryEntry): string {
async function enableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -280,7 +286,8 @@ async function enableAllAction(
async function disableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -60,10 +60,12 @@ describe('ideCommand', () => {
settings: {
setValue: vi.fn(),
},
config: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
agentContext: {
config: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
},
},
},
} as unknown as CommandContext;
+15 -5
View File
@@ -217,9 +217,13 @@ export const ideCommand = async (): Promise<SlashCommand> => {
);
// Poll for up to 5 seconds for the extension to activate.
for (let i = 0; i < 10; i++) {
await setIdeModeAndSyncConnection(context.services.config!, true, {
logToConsole: false,
});
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
{
logToConsole: false,
},
);
if (
ideClient.getConnectionStatus().status ===
IDEConnectionStatus.Connected
@@ -262,7 +266,10 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
true,
);
await setIdeModeAndSyncConnection(context.services.config!, true);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -285,7 +292,10 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
false,
);
await setIdeModeAndSyncConnection(context.services.config!, false);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
false,
);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -31,8 +31,10 @@ describe('initCommand', () => {
// Create a fresh mock context for each test
mockContext = createMockCommandContext({
services: {
config: {
getTargetDir: () => targetDir,
agentContext: {
config: {
getTargetDir: () => targetDir,
},
},
},
});
@@ -94,7 +96,7 @@ describe('initCommand', () => {
// Arrange: Create a context without config
const noConfigContext = createMockCommandContext();
if (noConfigContext.services) {
noConfigContext.services.config = null;
noConfigContext.services.agentContext = null;
}
// Act: Run the command's action
+2 -2
View File
@@ -23,14 +23,14 @@ export const initCommand: SlashCommand = {
context: CommandContext,
_args: string,
): Promise<SlashCommandActionReturn> => {
if (!context.services.config) {
if (!context.services.agentContext?.config) {
return {
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
};
}
const targetDir = context.services.config.getTargetDir();
const targetDir = context.services.agentContext.config.getTargetDir();
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
const result = performInit(fs.existsSync(geminiMdPath));

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