Merge branch 'main' into feat/card-component

This commit is contained in:
Mark McLaughlin
2026-02-05 08:27:46 -08:00
committed by GitHub
24 changed files with 2278 additions and 975 deletions
+16
View File
@@ -18,6 +18,22 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. | | [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. | | [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.27.0 - 2026-02-03
- **Event-Driven Architecture:** The CLI now uses a new event-driven scheduler
for tool execution, resulting in a more responsive and performant experience
([#17078](https://github.com/google-gemini/gemini-cli/pull/17078) by
@abhipatel12).
- **Enhanced User Experience:** This release includes queued tool confirmations,
and expandable large text pastes for a smoother workflow.
- **New `/rewind` Command:** Easily navigate your session history with the new
`/rewind` command
([#15720](https://github.com/google-gemini/gemini-cli/pull/15720) by
@Adib234).
- **Linux Clipboard Support:** You can now paste images on Linux with Wayland
and X11 ([#17144](https://github.com/google-gemini/gemini-cli/pull/17144) by
@devr0306).
## Announcements: v0.26.0 - 2026-01-27 ## Announcements: v0.26.0 - 2026-01-27
- **Agents and Skills:** We've introduced a new `skill-creator` skill - **Agents and Skills:** We've introduced a new `skill-creator` skill
+427 -318
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.26.0 # Latest stable release: v0.27.0
Released: January 27, 2026 Released: February 3, 2026
For most users, our latest stable release is the recommended release. Install For most users, our latest stable release is the recommended release. Install
the latest stable version with: the latest stable version with:
@@ -11,328 +11,437 @@ npm install -g @google/gemini-cli
## Highlights ## Highlights
- **Enhanced Agent and Skill Capabilities:** This release introduces the new - **Event-Driven Architecture:** The CLI now uses an event-driven scheduler for
`skill-creator` built-in skill, enables Agent Skills by default, and adds a tool execution, improving performance and responsiveness. This includes
generalist agent to improve task routing. Security for skill installation has migrating non-interactive flows and sub-agents to the new scheduler.
also been enhanced with new consent prompts. - **Enhanced User Experience:** This release introduces several UI/UX
- **Improved UI and UX:** A new "Rewind" feature lets you walk back through improvements, including queued tool confirmations and the ability to expand
conversation history. We've also added an `/introspect` command for debugging and collapse large pasted text blocks. The `Settings` dialog has been improved
and unified various shell confirmation dialogs for a more consistent user to reduce jitter and preserve focus.
experience. - **Agent and Skill Improvements:** Agent Skills have been promoted to a stable
- **Core Stability and Performance:** This release includes significant feature. Sub-agents now use a JSON schema for input and are tracked by an
performance improvements, including a fix for PDF token estimation, `AgentRegistry`.
optimizations for large inputs, and prevention of OOM crashes. Key memory - **New `/rewind` Command:** A new `/rewind` command has been implemented to
management components like `LRUCache` have also been updated. allow users to go back in their session history.
- **Scheduler and Policy Refactoring:** The core tool scheduler has been - **Improved Shell and File Handling:** The shell tool's output format has been
decoupled into distinct orchestration, policy, and confirmation components, optimized, and the CLI now gracefully handles disk-full errors during chat
and we've added an experimental event-driven scheduler to improve performance recording. A bug in detecting already added paths has been fixed.
and reliability. - **Linux Clipboard Support:** Image pasting capabilities for Wayland and X11 on
Linux have been added.
## What's Changed ## What's Changed
- fix: PDF token estimation (#16494) by @korade-krushna in - remove fireAgent and beforeAgent hook by @ishaanxgupta in
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527) [#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by - Remove unused modelHooks and toolHooks by @ved015 in
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
- Update Attempt text to Retry when showing the retry happening to the … by
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
- chore(skills): update pr-creator skill workflow by @sehoon38 in
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
@gemini-cli-robot in @gemini-cli-robot in
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604) [#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
- docs: clarify F12 to open debug console by @jackwotherspoon in - Remove other rewind reference in docs by @chrstnb in
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570) [#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
- docs: Remove .md extension from internal links in architecture.md by - feat(skills): add code-reviewer skill by @sehoon38 in
@medic-code in [#17187](https://github.com/google-gemini/gemini-cli/pull/17187)
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899) - feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
- Add an experimental setting for extension config by @chrstnb in [#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506) - feat(plan): refactor TestRig and eval helper to support configurable approval
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234 modes by @jerop in
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717) [#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by - feat(workflows): support recursive workstream labeling and new IDs by
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541) @bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
- prefactor: add rootCommands as array so it can be used for policy parsing by - Run evals for all models. by @gundermanc in
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123)
- fix(github): improve label-workstream-rollup efficiency with GraphQL by
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217)
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by
@g-samroberts in
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215)
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204)
- fix(github): improve label-workstream-rollup efficiency and fix bugs by
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219)
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192)
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218)
- feat(core): support dynamic variable substitution in system prompt override by
@NTaylorMullen in
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042)
- fix(core,cli): enable recursive directory access for by @galz10 in
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094)
- Docs: Marking for experimental features by @jkcinouye in
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760)
- Support command/ctrl/alt backspace correctly by @scidomino in
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175)
- feat(plan): add approval mode instructions to system prompt by @jerop in
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151)
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223)
- Remove unused slug from sidebar by @chrstnb in
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229)
- drain stdin on exit by @scidomino in
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241)
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by
@abhipatel12 in @abhipatel12 in
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640) [#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
- remove unnecessary \x7f key bindings by @scidomino in - fix(core): update token count and telemetry on /chat resume history load by
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646) @psinha40898 in
- docs(skills): use body-file in pr-creator skill for better reliability by [#16279](https://github.com/google-gemini/gemini-cli/pull/16279)
- fix: /policy to display policies according to mode by @ishaanxgupta in
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772)
- fix(core): simplify replace tool error message by @SandyTao520 in
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246)
- feat(cli): consolidate shell inactivity and redirection monitoring by
@NTaylorMullen in
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086)
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by
@abhipatel12 in @abhipatel12 in
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642) [#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
- chore(automation): recursive labeling for workstream descendants by @bdmorgan - feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609) [#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
- feat: introduce 'skill-creator' built-in skill and CJS management tools by - feat(hooks): enable hooks system by default by @abhipatel12 in
@NTaylorMullen in [#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394) - feat(core): Enable AgentRegistry to track all discovered subagents by
- chore(automation): remove automated PR size and complexity labeler by
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
- Simplify paste handling by @scidomino in
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
- chore(automation): improve scheduled issue triage discovery and throughput by
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
- Remove unused rewind key binding by @scidomino in
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
- Remove sequence binding by @scidomino in
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
- chore: update dependabot configuration by @cosmopax in
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
- Enable & disable agents by @sehoon38 in
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
- cleanup: Improve keybindings by @scidomino in
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
- feat(plan): add experimental plan flag by @jerop in
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
- feat(cli): add security consent prompts for skill installation by
@NTaylorMullen in
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
- chore(automation): ensure status/need-triage is applied and never cleared
automatically by @bdmorgan in
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
- fix: Handle colons in skill description frontmatter by @maru0804 in
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
- feat(skills): add conflict detection and warnings for skill overrides by
@NTaylorMullen in
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
- feat(scheduler): add SchedulerStateManager for reactive tool state by
@abhipatel12 in
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
- chore(automation): enforce 'help wanted' label permissions and update
guidelines by @bdmorgan in
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
@gemini-cli-robot in
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
- fix(automation): correct status/need-issue label matching wildcard by
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
- Add links to supported locations and minor fixes by @g-samroberts in
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
- feat(policy): add source tracking to policy rules by @allenhutchison in
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
- Make merged settings non-nullable and fix all lints related to that. by
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
- Replace relative paths to fix website build by @chrstnb in
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
- Restricting to localhost by @cocosheng-g in
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
- fix(automation): robust label enforcement with permission checks by @bdmorgan
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
- fix(cli): prevent OOM crash by limiting file search traversal and adding
timeout by @galz10 in
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
- Add support for running available commands prior to MCP servers loading by
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
- feat(plan): add experimental 'plan' approval mode by @jerop in
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
- skip simple-mcp-server.test.ts by @scidomino in
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
- Steer outer agent to use expert subagents when present by @gundermanc in
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
- cleanup: Organize key bindings by @scidomino in
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
- feat(core): Add generalist agent. by @joshualitt in
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
- perf(ui): optimize text buffer and highlighting for large inputs by
@NTaylorMullen in
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
- fix(core): fix PTY descriptor shell leak by @galz10 in
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
- feat(plan): enforce strict read-only policy and halt execution on violation by
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
- remove need-triage label from bug_report template by @sehoon38 in
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
- fix(core): truncate large telemetry log entries by @sehoon38 in
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
- docs(extensions): add Agent Skills support and mark feature as experimental by
@NTaylorMullen in
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
- fix(core): surface warnings for invalid hook event names in configuration
(#16788) by @sehoon38 in
[#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
- feat(plan): remove read_many_files from approval mode policies by @jerop in
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
- feat(admin): implement admin controls polling and restart prompt by @skeshive
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
- Remove LRUCache class migrating to mnemoist by @jacob314 in
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
- feat(settings): rename negative settings to positive naming (disable* ->
enable*) by @afarber in
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
- feat(agent): enable agent skills by default by @NTaylorMullen in
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
- refactor(core): foundational truncation refactoring and token estimation
optimization by @NTaylorMullen in
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
- fix(hooks): enable /hooks disable to reliably stop single hooks by
@abhipatel12 in
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
- Don't commit unless user asks us to. by @gundermanc in
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
- fix: Show experiment values in settings UI for compressionThreshold by
@ishaanxgupta in
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
- feat(cli): replace relative keyboard shortcuts link with web URL by
@imaliabbas in
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
- Delete rewind documentation for now by @Adib234 in
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
- Stabilize skill-creator CI and package format by @NTaylorMullen in
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
- Stabilize the git evals by @gundermanc in
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
- fix(core): attempt compression before context overflow check by @NTaylorMullen
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
- Fix inverted logic. by @gundermanc in
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
- chore(scripts): add duplicate issue closer script and fix lint errors by
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
- fix(cli): correct Homebrew installation detection by @kij in
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
- Demote git evals to nightly run. by @gundermanc in
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
- refactor(core): decouple scheduler into orchestration, policy, and
confirmation by @abhipatel12 in
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
- feat: add /introspect slash command by @NTaylorMullen in
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
@abhipatel12 in
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
- fix(workflows): use author_association for maintainer check by @bdmorgan in
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
- feat(core): improve shell redirection transparency and security by
@NTaylorMullen in
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
- docs(themes): remove unsupported DiffModified color key by @jw409 in
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
- feat(core): enhanced anchored iterative context compression with
self-verification by @rmedranollamas in
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
- Fix mcp instructions by @chrstnb in
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
by @skeshive in
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
- Test coverage for hook exit code cases by @gundermanc in
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
- Revert "Revert "Update extension examples"" by @chrstnb in
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
- fix(core): Provide compact, actionable errors for agent delegation failures by
@SandyTao520 in @SandyTao520 in
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493) [#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in - feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599) [#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by - feat: replace large text pastes with [Pasted Text: X lines] placeholder by
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102) @jackwotherspoon in
- fix(core): update telemetry token count after session resume by @psinha40898 [#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491) - security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
- Demote the subagent test to nightly by @gundermanc in in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105) - Enable the ability to queue specific nightly eval tests by @gundermanc in
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in [#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863) - docs(hooks): comprehensive update of hook documentation and specs by
- feat: Add flash lite utility fallback chain by @adamfweidman in @abhipatel12 in
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056) [#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit - refactor: improve large text paste placeholder by @jacob314 in
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757) [#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
- feat(core): Add initial eval for generalist agent. by @joshualitt in - feat: implement /rewind command by @Adib234 in
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856) [#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in - Feature/jetbrains ide detection by @SoLoHiC in
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127) [#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
- fix(core): resolve auto model in default strategy by @sehoon38 in - docs: update typo in mcp-server.md file by @schifferl in
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116) [#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
- docs: update project context and pr-creator workflow by @NTaylorMullen in - Sanitize command names and descriptions by @ehedlund in
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119) [#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
- fix(cli): send gemini-cli version as mcp client version by @dsp in - fix(auth): don't crash when initial auth fails by @skeshive in
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407) [#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in - Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021) [#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
- Remove missing sidebar item by @chrstnb in - feat: add AskUser tool schema by @jackwotherspoon in
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145) [#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
- feat(core): Ensure all properties in hooks object are event names. by - fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
@joshualitt in [#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870) - fix: show whitespace changes in edit tool diffs by @Ujjiyara in
- fix(cli): fix newline support broken in previous PR by @scidomino in [#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159) - Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors. now handles the caching internally by @jacob314 in
by @gsquared94 in [#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231) - ci: allow failure in evals-nightly run step by @gundermanc in
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in [#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131) - feat(cli): Add state management and plumbing for agent configuration dialog by
- Avoid spurious warnings about unexpected renders triggered by appEvents and @SandyTao520 in
coreEvents. by @jacob314 in [#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160) - bug: fix ide-client connection to ide-companion when inside docker via
- fix(cli): resolve home/end keybinding conflict by @scidomino in ssh/devcontainer by @kapsner in
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124) [#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
- fix(cli): display 'http' type on mcp list by @pamanta in - Emit correct newline type return by @scidomino in
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915) [#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
- fix bad fallback logic external editor logic by @scidomino in - New skill: docs-writer by @g-samroberts in
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166) [#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
- Fix bug where System scopes weren't migrated. by @jacob314 in - fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174) @spencer426 in
- Fix mcp tool lookup in tool registry by @werdnum in [#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054) - Disable tips after 10 runs by @Adib234 in
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101)
- Fix so rewind starts at the bottom and loadHistory refreshes static content.
by @jacob314 in
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335)
- feat(core): Remove legacy settings. by @joshualitt in
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244)
- feat(plan): add 'communicate' tool kind by @jerop in
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341)
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by
@mattKorwel in
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041)
- feat(plan): update UI Theme for Plan Mode by @Adib234 in
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243)
- fix(ui): stabilize rendering during terminal resize in alternate buffer by
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783)
- feat(cli): add /agents config command and improve agent discovery by
@SandyTao520 in
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342)
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299)
- fix(cli)!: Default to interactive mode for positional arguments by
@ishaanxgupta in
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329)
- Fix issue #17080 by @jacob314 in
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100)
- feat(core): Refresh agents after loading an extension. by @joshualitt in
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355)
- fix(cli): include source in policy rule display by @allenhutchison in
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358)
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by
@gsquared94 in
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236)
- Refactor subagent delegation to be one tool per agent by @gundermanc in
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346)
- fix(core): Include MCP server name in OAuth message by @jerop in
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351)
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and
"maintainer only" by @jacob314 in
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324)
- feat(plan): implement simple workflow for planning in main agent by @jerop in
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326)
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967)
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by
@medic-code in
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038)
- fix(core): add alternative command names for Antigravity editor detec… by
@baeseokjae in
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829)
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737)
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390)
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714)
- fix(core): gracefully handle disk full errors in chat recording by
@godwiniheuwa in
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305)
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388)
- fix(core): use RFC 9728 compliant path-based OAuth protected resource
discovery by @vrv in
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756)
- Update Code Wiki README badge by @PatoBeltran in
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229)
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921)
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369)
- fix(cli): preserve input text when declining tool approval (#15624) by
@ManojINaik in
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659)
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403)
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344)
- feat(ui): display user tier in about command by @sehoon38 in
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400)
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574)
- fix(cli): change image paste location to global temp directory (#17396) by
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396)
- Fix line endings issue with Notice file by @scidomino in
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417)
- feat(plan): implement persistent approvalMode setting by @Adib234 in
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350)
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404)
- Allow prompt queueing during MCP initialization by @Adib234 in
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395)
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370)
- fix(agents): default to all tools when tool list is omitted in subagents by
@gundermanc in
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422)
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276)
- fix(core): hide user tier name by @sehoon38 in
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418)
- feat: Enforce unified folder trust for /directory add by @galz10 in
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359)
- migrate fireToolNotificationHook to hookSystem by @ved015 in
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398)
- Clean up dead code by @scidomino in
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443)
- feat(workflow): add stale pull request closer with linked-issue enforcement by
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449)
- feat(workflow): expand stale-exempt labels to include help wanted and Public
Roadmap by @bdmorgan in
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459)
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460)
- Resolves the confusing error message `ripgrep exited with code null that
occurs when a search operation is cancelled or aborted by @maximmasiutin in
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267)
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254)
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412)
- docs(hooks): clarify mandatory 'type' field and update hook schema
documentation by @abhipatel12 in
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499)
- Improve error messages on failed onboarding by @gsquared94 in
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357)
- Follow up to "enableInteractiveShell for external tooling relying on a2a
server" by @DavidAPierce in
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130)
- Fix/issue 17070 by @alih552 in
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242)
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476)
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470)
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538)
- Fix bug in detecting already added paths. by @jacob314 in
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430)
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs
by @abhipatel12 in
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429)
- feat(agents): implement first-run experience for project-level sub-agents by
@gundermanc in
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266)
- Update extensions docs by @chrstnb in
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093)
- Docs: Refactor left nav on the website by @jkcinouye in
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
- feat(plan): add persistent plan file storage by @jerop in
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
- Fix extensions config error by @chrstnb in
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
- fix(plan): remove subagent invocation from plan mode by @jerop in
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
- feat(ui): add solid background color option for input prompt by @jacob314 in
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
- feat(cli): add global setting to disable UI spinners by @galz10 in
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
- fix(security): enforce strict policy directory permissions by @yunaseoul in
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
- test(core): fix tests in windows by @scidomino in
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
- Always map mac keys, even on other platforms by @scidomino in
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
- Ctrl-O by @jacob314 in
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
- feat(plan): update cycling order of approval modes by @Adib234 in
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
- fix(cli): restore 'Modify with editor' option in external terminals by
@abhipatel12 in
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
- Slash command for helping in debugging by @gundermanc in
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
- feat: add double-click to expand/collapse large paste placeholders by
@jackwotherspoon in
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
@abhipatel12 in
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
(regression) by @gsquared94 in
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
- feat(core): enforce server prefixes for MCP tools in agent definitions by
@abhipatel12 in
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
- paste transform followup by @jacob314 in
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
- feat(cli): add oncall command for issue triage by @sehoon38 in
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
- Fix sidebar issue for extensions link by @chrstnb in
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
- Change formatting to prevent UI redressing attacks by @scidomino in
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
- Fix cluster of bugs in the settings dialog. by @jacob314 in
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
- Update sidebar to resolve site build issues by @chrstnb in
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
- fix(admin): fix a few bugs related to admin controls by @skeshive in
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
- revert bad changes to tests by @scidomino in
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
- feat(cli): show candidate issue state reason and duplicate status in triage by
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
- Fix missing slash commands when Gemini CLI is in a project with a package.json
that doesn't follow semantic versioning by @Adib234 in
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
- feat(core): Model family-specific system prompts by @joshualitt in
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
- Sub-agents documentation. by @gundermanc in
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
- Load extension settings for hooks, agents, skills by @chrstnb in
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
- Fix issue where Gemini CLI can make changes when simply asked a question by
@gundermanc in
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
- Update docs-writer skill for editing and add style guide for reference. by
@g-samroberts in
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
- fix(ux): have user message display a short path for pasted images by @devr0306
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
- GEMINI.md polish by @jacob314 in
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
- refactor(core): centralize path validation and allow temp dir access for tools
by @NTaylorMullen in
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
mode. by @jacob314 in
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
- feat(skills): promote skills settings to stable by @abhipatel12 in
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
- feat(ui): add terminal cursor support by @jacob314 in
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
- Add support for an additional exclusion file besides .gitignore and
.geminiignore by @alisa-alisa in
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
- feat: add review-frontend-and-fix command by @galz10 in
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
**Full changelog**: **Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.25.2...v0.26.0 https://github.com/google-gemini/gemini-cli/compare/v0.26.0...v0.27.0
+289 -419
View File
@@ -1,6 +1,6 @@
# Preview release: Release v0.27.0-preview.0 # Preview release: Release v0.28.0-preview.0
Released: January 27, 2026 Released: February 3, 2026
Our preview release includes the latest, new, and experimental features. This Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md). release may not be as stable as our [latest weekly release](latest.md).
@@ -13,425 +13,295 @@ npm install -g @google/gemini-cli@preview
## Highlights ## Highlights
- **Event-Driven Architecture:** The tool execution scheduler is now - **Improved Hooks Management:** Hooks enable/disable functionality now aligns
event-driven, improving performance and reliability. with skills and offers improved completion.
- **System Prompt Override:** Now supports dynamic variable substitution. - **Custom Themes for Extensions:** Extensions can now support custom themes,
- **Rewind Command:** The `/rewind` command has been implemented. allowing for greater personalization.
- **Linux Clipboard:** Image pasting capabilities for Wayland and X11 on Linux. - **User Identity Display:** User identity information (auth, email, tier) is
now displayed on startup and in the `stats` command.
- **Plan Mode Enhancements:** Plan mode has been improved with a generic
`Checklist` component and refactored `Todo`.
- **Background Shell Commands:** Implementation of background shell commands.
## What's Changed ## What's Changed
- remove fireAgent and beforeAgent hook by @ishaanxgupta in - feat(commands): add /prompt-suggest slash command by NTaylorMullen in
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919) [#17264](https://github.com/google-gemini/gemini-cli/pull/17264)
- Remove unused modelHooks and toolHooks by @ved015 in - feat(cli): align hooks enable/disable with skills and improve completion by
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115) sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822)
- feat(cli): sanitize ANSI escape sequences in non-interactive output by - docs: add CLI reference documentation by leochiu-a in
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172) [#17504](https://github.com/google-gemini/gemini-cli/pull/17504)
- Update Attempt text to Retry when showing the retry happening to the … by - chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178) gemini-cli-robot in
- chore(skills): update pr-creator skill workflow by @sehoon38 in [#17725](https://github.com/google-gemini/gemini-cli/pull/17725)
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180) - feat(skills): final stable promotion cleanup by abhipatel12 in
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in [#17726](https://github.com/google-gemini/gemini-cli/pull/17726)
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078) - test(core): mock fetch in OAuth transport fallback tests by jw409 in
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by [#17059](https://github.com/google-gemini/gemini-cli/pull/17059)
@gemini-cli-robot in - feat(cli): include auth method in /bug by erikus in
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181) [#17569](https://github.com/google-gemini/gemini-cli/pull/17569)
- Remove other rewind reference in docs by @chrstnb in - Add a email privacy note to bug_report template by nemyung in
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149) [#17474](https://github.com/google-gemini/gemini-cli/pull/17474)
- feat(skills): add code-reviewer skill by @sehoon38 in - Rewind documentation by Adib234 in
[#17187](httpshttps://github.com/google-gemini/gemini-cli/pull/17187) [#17446](https://github.com/google-gemini/gemini-cli/pull/17446)
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in - fix: verify audio/video MIME types with content check by maru0804 in
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177) [#16907](https://github.com/google-gemini/gemini-cli/pull/16907)
- feat(plan): refactor TestRig and eval helper to support configurable approval - feat(core): add support for positron ide (#15045) by kapsner in
modes by @jerop in [#15047](https://github.com/google-gemini/gemini-cli/pull/15047)
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171) - /oncall dedup - wrap texts to nextlines by sehoon38 in
- feat(workflows): support recursive workstream labeling and new IDs by [#17782](https://github.com/google-gemini/gemini-cli/pull/17782)
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207) - fix(admin): rename advanced features admin setting by skeshive in
- Run evals for all models. by @gundermanc in [#17786](https://github.com/google-gemini/gemini-cli/pull/17786)
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123) - [extension config] Make breaking optional value non-optional by chrstnb in
- fix(github): improve label-workstream-rollup efficiency with GraphQL by [#17785](https://github.com/google-gemini/gemini-cli/pull/17785)
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217) - Fix docs-writer skill issues by g-samroberts in
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by [#17734](https://github.com/google-gemini/gemini-cli/pull/17734)
@g-samroberts in - fix(core): suppress duplicate hook failure warnings during streaming by
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215) abhipatel12 in
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in [#17727](https://github.com/google-gemini/gemini-cli/pull/17727)
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204) - test: add more tests for AskUser by jackwotherspoon in
- fix(github): improve label-workstream-rollup efficiency and fix bugs by [#17720](https://github.com/google-gemini/gemini-cli/pull/17720)
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219) - feat(cli): enable activity logging for non-interactive mode and evals by
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in SandyTao520 in
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192) [#17703](https://github.com/google-gemini/gemini-cli/pull/17703)
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in - feat(core): add support for custom deny messages in policy rules by
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218) allenhutchison in
- feat(core): support dynamic variable substitution in system prompt override by [#17427](https://github.com/google-gemini/gemini-cli/pull/17427)
@NTaylorMullen in - Fix unintended credential exposure to MCP Servers by Adib234 in
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042) [#17311](https://github.com/google-gemini/gemini-cli/pull/17311)
- fix(core,cli): enable recursive directory access for by @galz10 in - feat(extensions): add support for custom themes in extensions by spencer426 in
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094) [#17327](https://github.com/google-gemini/gemini-cli/pull/17327)
- Docs: Marking for experimental features by @jkcinouye in - fix: persist and restore workspace directories on session resume by
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760) korade-krushna in
- Support command/ctrl/alt backspace correctly by @scidomino in [#17454](https://github.com/google-gemini/gemini-cli/pull/17454)
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175) - Update release notes pages for 0.26.0 and 0.27.0-preview. by g-samroberts in
- feat(plan): add approval mode instructions to system prompt by @jerop in [#17744](https://github.com/google-gemini/gemini-cli/pull/17744)
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151) - feat(ux): update cell border color and created test file for table rendering
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in by devr0306 in
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223) [#17798](https://github.com/google-gemini/gemini-cli/pull/17798)
- Remove unused slug from sidebar by @chrstnb in - Change height for the ToolConfirmationQueue. by jacob314 in
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229) [#17799](https://github.com/google-gemini/gemini-cli/pull/17799)
- drain stdin on exit by @scidomino in - feat(cli): add user identity info to stats command by sehoon38 in
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241) [#17612](https://github.com/google-gemini/gemini-cli/pull/17612)
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by - fix(ux): fixed off-by-some wrapping caused by fixed-width characters by
@abhipatel12 in devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816)
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183) - feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and
- fix(core): update token count and telemetry on /chat resume history load by Shift+Cmd+Z/Shift+Alt+Z by scidomino in
@psinha40898 in [#17800](https://github.com/google-gemini/gemini-cli/pull/17800)
[#16279](https://github.com/google-gemini/gemini-cli/pull/16279) - fix(evals): use absolute path for activity log directory by SandyTao520 in
- fix: /policy to display policies according to mode by @ishaanxgupta in [#17830](https://github.com/google-gemini/gemini-cli/pull/17830)
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772) - test: add integration test to verify stdout/stderr routing by ved015 in
- fix(core): simplify replace tool error message by @SandyTao520 in [#17280](https://github.com/google-gemini/gemini-cli/pull/17280)
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246) - fix(cli): list installed extensions when update target missing by tt-a1i in
- feat(cli): consolidate shell inactivity and redirection monitoring by [#17082](https://github.com/google-gemini/gemini-cli/pull/17082)
@NTaylorMullen in - fix(cli): handle PAT tokens and credentials in git remote URL parsing by
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086) afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650)
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by - fix(core): use returnDisplay for error result display by Nubebuster in
@abhipatel12 in [#14994](https://github.com/google-gemini/gemini-cli/pull/14994)
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227) - Fix detection of bun as package manager by Randomblock1 in
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in [#17462](https://github.com/google-gemini/gemini-cli/pull/17462)
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211) - feat(cli): show hooksConfig.enabled in settings dialog by abhipatel12 in
- feat(hooks): enable hooks system by default by @abhipatel12 in [#17810](https://github.com/google-gemini/gemini-cli/pull/17810)
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247) - feat(cli): Display user identity (auth, email, tier) on startup by yunaseoul
- feat(core): Enable AgentRegistry to track all discovered subagents by in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591)
@SandyTao520 in - fix: prevent ghost border for AskUserDialog by jackwotherspoon in
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253) [#17788](https://github.com/google-gemini/gemini-cli/pull/17788)
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in - docs: mark A2A subagents as experimental in subagents.md by adamfweidman in
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152) [#17863](https://github.com/google-gemini/gemini-cli/pull/17863)
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by - Resolve error thrown for sensitive values by chrstnb in
@jackwotherspoon in [#17826](https://github.com/google-gemini/gemini-cli/pull/17826)
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422) - fix(admin): Rename secureModeEnabled to strictModeDisabled by skeshive in
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul [#17789](https://github.com/google-gemini/gemini-cli/pull/17789)
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237) - feat(ux): update truncate dots to be shorter in tables by devr0306 in
- Enable the ability to queue specific nightly eval tests by @gundermanc in [#17825](https://github.com/google-gemini/gemini-cli/pull/17825)
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262) - fix(core): resolve DEP0040 punycode deprecation via patch-package by
- docs(hooks): comprehensive update of hook documentation and specs by ATHARVA262005 in
@abhipatel12 in [#17692](https://github.com/google-gemini/gemini-cli/pull/17692)
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816) - feat(plan): create generic Checklist component and refactor Todo by Adib234 in
- refactor: improve large text paste placeholder by @jacob314 in [#17741](https://github.com/google-gemini/gemini-cli/pull/17741)
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269) - Cleanup post delegate_to_agent removal by gundermanc in
- feat: implement /rewind command by @Adib234 in [#17875](https://github.com/google-gemini/gemini-cli/pull/17875)
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720) - fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration -
- Feature/jetbrains ide detection by @SoLoHiC in Fixes #17877 by cocosheng-g in
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243) [#17803](https://github.com/google-gemini/gemini-cli/pull/17803)
- docs: update typo in mcp-server.md file by @schifferl in - Disable mouse tracking e2e by alisa-alisa in
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099) [#17880](https://github.com/google-gemini/gemini-cli/pull/17880)
- Sanitize command names and descriptions by @ehedlund in - fix(cli): use correct setting key for Cloud Shell auth by sehoon38 in
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228) [#17884](https://github.com/google-gemini/gemini-cli/pull/17884)
- fix(auth): don't crash when initial auth fails by @skeshive in - chore: revert IDE specific ASCII logo by jackwotherspoon in
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308) [#17887](https://github.com/google-gemini/gemini-cli/pull/17887)
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in - Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144) sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898)
- feat: add AskUser tool schema by @jackwotherspoon in - Refactoring of disabling of mouse tracking in e2e tests by alisa-alisa in
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988) [#17902](https://github.com/google-gemini/gemini-cli/pull/17902)
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in - feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by deyim
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256) in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177)
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in - feat(core): Isolate and cleanup truncated tool outputs by SandyTao520 in
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213) [#17594](https://github.com/google-gemini/gemini-cli/pull/17594)
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool() - Create skills page, update commands, refine docs by g-samroberts in
now handles the caching internally by @jacob314 in [#17842](https://github.com/google-gemini/gemini-cli/pull/17842)
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320) - feat: preserve EOL in files by Thomas-Shephard in
- ci: allow failure in evals-nightly run step by @gundermanc in [#16087](https://github.com/google-gemini/gemini-cli/pull/16087)
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319) - Fix HalfLinePaddedBox in screenreader mode. by jacob314 in
- feat(cli): Add state management and plumbing for agent configuration dialog by [#17914](https://github.com/google-gemini/gemini-cli/pull/17914)
@SandyTao520 in - bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259) in vim mode. by jacob314 in
- bug: fix ide-client connection to ide-companion when inside docker via [#17938](https://github.com/google-gemini/gemini-cli/pull/17938)
ssh/devcontainer by @kapsner in - feat(core): implement interactive and non-interactive consent for OAuth by
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049) ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699)
- Emit correct newline type return by @scidomino in - perf(core): optimize token calculation and add support for multimodal tool
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331) responses by abhipatel12 in
- New skill: docs-writer by @g-samroberts in [#17835](https://github.com/google-gemini/gemini-cli/pull/17835)
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268) - refactor(hooks): remove legacy tools.enableHooks setting by abhipatel12 in
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by [#17867](https://github.com/google-gemini/gemini-cli/pull/17867)
@spencer426 in - feat(ci): add npx smoke test to verify installability by bdmorgan in
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735) [#17927](https://github.com/google-gemini/gemini-cli/pull/17927)
- Disable tips after 10 runs by @Adib234 in - feat(core): implement dynamic policy registration for subagents by abhipatel12
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101) in [#17838](https://github.com/google-gemini/gemini-cli/pull/17838)
- Fix so rewind starts at the bottom and loadHistory refreshes static content. - feat: Implement background shell commands by galz10 in
by @jacob314 in [#14849](https://github.com/google-gemini/gemini-cli/pull/14849)
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335) - feat(admin): provide actionable error messages for disabled features by
- feat(core): Remove legacy settings. by @joshualitt in skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815)
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244) - Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by
- feat(plan): add 'communicate' tool kind by @jerop in jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940)
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341) - Fix broken link in docs by chrstnb in
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by [#17959](https://github.com/google-gemini/gemini-cli/pull/17959)
@mattKorwel in - feat(plan): reuse standard tool confirmation for AskUser tool by jerop in
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041) [#17864](https://github.com/google-gemini/gemini-cli/pull/17864)
- feat(plan): update UI Theme for Plan Mode by @Adib234 in - feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243) lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942)
- fix(ui): stabilize rendering during terminal resize in alternate buffer by - run npx pointing to the specific commit SHA by sehoon38 in
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783) [#17970](https://github.com/google-gemini/gemini-cli/pull/17970)
- feat(cli): add /agents config command and improve agent discovery by - Add allowedExtensions setting by kevinjwang1 in
@SandyTao520 in [#17695](https://github.com/google-gemini/gemini-cli/pull/17695)
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342) - feat(plan): refactor ToolConfirmationPayload to union type by jerop in
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb [#17980](https://github.com/google-gemini/gemini-cli/pull/17980)
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299) - lower the default max retries to reduce contention by sehoon38 in
- fix(cli)!: Default to interactive mode for positional arguments by [#17975](https://github.com/google-gemini/gemini-cli/pull/17975)
@ishaanxgupta in - fix(core): ensure YOLO mode auto-approves complex shell commands when parsing
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329) fails by abhipatel12 in
- Fix issue #17080 by @jacob314 in [#17920](https://github.com/google-gemini/gemini-cli/pull/17920)
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100) - Fix broken link. by g-samroberts in
- feat(core): Refresh agents after loading an extension. by @joshualitt in [#17972](https://github.com/google-gemini/gemini-cli/pull/17972)
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355) - Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over
- fix(cli): include source in policy rule display by @allenhutchison in AppContainer for input handling. by jacob314 in
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358) [#17993](https://github.com/google-gemini/gemini-cli/pull/17993)
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by - Fix truncation for AskQuestion by jacob314 in
@gsquared94 in [#18001](https://github.com/google-gemini/gemini-cli/pull/18001)
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236) - fix(workflow): update maintainer check logic to be inclusive and
- Refactor subagent delegation to be one tool per agent by @gundermanc in case-insensitive by bdmorgan in
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346) [#18009](https://github.com/google-gemini/gemini-cli/pull/18009)
- fix(core): Include MCP server name in OAuth message by @jerop in - Fix Esc cancel during streaming by LyalinDotCom in
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351) [#18039](https://github.com/google-gemini/gemini-cli/pull/18039)
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and - feat(acp): add session resume support by bdmorgan in
"maintainer only" by @jacob314 in [#18043](https://github.com/google-gemini/gemini-cli/pull/18043)
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324) - fix(ci): prevent stale PR closer from incorrectly closing new PRs by bdmorgan
- feat(plan): implement simple workflow for planning in main agent by @jerop in in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069)
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326) - chore: delete autoAccept setting unused in production by victorvianna in
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in [#17862](https://github.com/google-gemini/gemini-cli/pull/17862)
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967) - feat(plan): use placeholder for choice question "Other" option by jerop in
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by [#18101](https://github.com/google-gemini/gemini-cli/pull/18101)
@medic-code in - docs: update clearContext to hookSpecificOutput by jackwotherspoon in
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038) [#18024](https://github.com/google-gemini/gemini-cli/pull/18024)
- fix(core): add alternative command names for Antigravity editor detec… by - docs-writer skill: Update docs writer skill by jkcinouye in
@BaeSeokJae in [#17928](https://github.com/google-gemini/gemini-cli/pull/17928)
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829) - Sehoon/oncall filter by sehoon38 in
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in [#18105](https://github.com/google-gemini/gemini-cli/pull/18105)
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737) - feat(core): add setting to disable loop detection by SandyTao520 in
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in [#18008](https://github.com/google-gemini/gemini-cli/pull/18008)
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390) - Docs: Revise docs/index.md by jkcinouye in
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in [#17879](https://github.com/google-gemini/gemini-cli/pull/17879)
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714) - Fix up/down arrow regression and add test. by jacob314 in
- fix(core): gracefully handle disk full errors in chat recording by [#18108](https://github.com/google-gemini/gemini-cli/pull/18108)
@godwiniheuwa in - fix(ui): prevent content leak in MaxSizedBox bottom overflow by jerop in
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305) [#17991](https://github.com/google-gemini/gemini-cli/pull/17991)
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in - refactor: migrate checks.ts utility to core and deduplicate by jerop in
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388) [#18139](https://github.com/google-gemini/gemini-cli/pull/18139)
- fix(core): use RFC 9728 compliant path-based OAuth protected resource - feat(core): implement tool name aliasing for backward compatibility by
discovery by @vrv in SandyTao520 in
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756) [#17974](https://github.com/google-gemini/gemini-cli/pull/17974)
- Update Code Wiki README badge by @PatoBeltran in - docs: fix help-wanted label spelling by pavan-sh in
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229) [#18114](https://github.com/google-gemini/gemini-cli/pull/18114)
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in - feat(cli): implement automatic theme switching based on terminal background by
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921) Abhijit-2592 in
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in [#17976](https://github.com/google-gemini/gemini-cli/pull/17976)
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369) - fix(ide): no-op refactoring that moves the connection logic to helper
- fix(cli): preserve input text when declining tool approval (#15624) by functions by skeshive in
@ManojINaik in [#18118](https://github.com/google-gemini/gemini-cli/pull/18118)
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659) - feat: update review-frontend-and-fix slash command to review-and-fix by galz10
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146)
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403) - fix: improve Ctrl+R reverse search by jackwotherspoon in
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon [#18075](https://github.com/google-gemini/gemini-cli/pull/18075)
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344) - feat(plan): handle inconsistency in schedulers by Adib234 in
- feat(ui): display user tier in about command by @sehoon38 in [#17813](https://github.com/google-gemini/gemini-cli/pull/17813)
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400) - feat(plan): add core logic and exit_plan_mode tool definition by jerop in
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in [#18110](https://github.com/google-gemini/gemini-cli/pull/18110)
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574) - feat(core): rename search_file_content tool to grep_search and add legacy
- fix(cli): change image paste location to global temp directory (#17396) by alias by SandyTao520 in
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396) [#18003](https://github.com/google-gemini/gemini-cli/pull/18003)
- Fix line endings issue with Notice file by @scidomino in - fix(core): prioritize detailed error messages for code assist setup by
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417) gsquared94 in [#17852](https://github.com/google-gemini/gemini-cli/pull/17852)
- feat(plan): implement persistent approvalMode setting by @Adib234 in - fix(cli): resolve environment loading and auth validation issues in ACP mode
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350) by bdmorgan in
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in [#18025](https://github.com/google-gemini/gemini-cli/pull/18025)
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404) - feat(core): add .agents/skills directory alias for skill discovery by
- Allow prompt queueing during MCP initialization by @Adib234 in NTaylorMullen in
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395) [#18151](https://github.com/google-gemini/gemini-cli/pull/18151)
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520 - chore(core): reassign telemetry keys to avoid server conflict by mattKorwel in
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370) [#18161](https://github.com/google-gemini/gemini-cli/pull/18161)
- fix(agents): default to all tools when tool list is omitted in subagents by - Add link to rewind doc in commands.md by Adib234 in
@gundermanc in [#17961](https://github.com/google-gemini/gemini-cli/pull/17961)
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422) - feat(core): add draft-2020-12 JSON Schema support with lenient fallback by
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060)
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276) - refactor(core): robust trimPreservingTrailingNewline and regression test by
- fix(core): hide user tier name by @sehoon38 in adamfweidman in
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418) [#18196](https://github.com/google-gemini/gemini-cli/pull/18196)
- feat: Enforce unified folder trust for /directory add by @galz10 in - Remove MCP servers on extension uninstall by chrstnb in
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359) [#18121](https://github.com/google-gemini/gemini-cli/pull/18121)
- migrate fireToolNotificationHook to hookSystem by @ved015 in - refactor: localize ACP error parsing logic to cli package by bdmorgan in
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398) [#18193](https://github.com/google-gemini/gemini-cli/pull/18193)
- Clean up dead code by @scidomino in - feat(core): Add A2A auth config types by adamfweidman in
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443) [#18205](https://github.com/google-gemini/gemini-cli/pull/18205)
- feat(workflow): add stale pull request closer with linked-issue enforcement by - Set default max attempts to 3 and use the common variable by sehoon38 in
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449) [#18209](https://github.com/google-gemini/gemini-cli/pull/18209)
- feat(workflow): expand stale-exempt labels to include help wanted and Public - feat(plan): add exit_plan_mode ui and prompt by jerop in
Roadmap by @bdmorgan in [#18162](https://github.com/google-gemini/gemini-cli/pull/18162)
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459) - fix(test): improve test isolation and enable subagent evaluations by
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in cocosheng-g in
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460) [#18138](https://github.com/google-gemini/gemini-cli/pull/18138)
- Resolves the confusing error message `ripgrep exited with code null that - feat(plan): use custom deny messages in plan mode policies by Adib234 in
occurs when a search operation is cancelled or aborted by @maximmasiutin in [#18195](https://github.com/google-gemini/gemini-cli/pull/18195)
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267) - Match on extension ID when stopping extensions by chrstnb in
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in [#18218](https://github.com/google-gemini/gemini-cli/pull/18218)
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254) - fix(core): Respect user's .gitignore preference by xyrolle in
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by [#15482](https://github.com/google-gemini/gemini-cli/pull/15482)
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412) - docs: document GEMINI_CLI_HOME environment variable by adamfweidman in
- docs(hooks): clarify mandatory 'type' field and update hook schema [#18219](https://github.com/google-gemini/gemini-cli/pull/18219)
documentation by @abhipatel12 in - chore(core): explicitly state plan storage path in prompt by jerop in
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499) [#18222](https://github.com/google-gemini/gemini-cli/pull/18222)
- Improve error messages on failed onboarding by @gsquared94 in - A2a admin setting by DavidAPierce in
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357) [#17868](https://github.com/google-gemini/gemini-cli/pull/17868)
- Follow up to "enableInteractiveShell for external tooling relying on a2a - feat(a2a): Add pluggable auth provider infrastructure by adamfweidman in
server" by @DavidAPierce in [#17934](https://github.com/google-gemini/gemini-cli/pull/17934)
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130) - Fix handling of empty settings by chrstnb in
- Fix/issue 17070 by @alih552 in [#18131](https://github.com/google-gemini/gemini-cli/pull/18131)
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242) - Reload skills when extensions change by chrstnb in
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin [#18225](https://github.com/google-gemini/gemini-cli/pull/18225)
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476) - feat: Add markdown rendering to ask_user tool by jackwotherspoon in
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in [#18211](https://github.com/google-gemini/gemini-cli/pull/18211)
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470) - Add telemetry to rewind by Adib234 in
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in [#18122](https://github.com/google-gemini/gemini-cli/pull/18122)
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538) - feat(admin): add support for MCP configuration via admin controls (pt1) by
- Fix bug in detecting already added paths. by @jacob314 in skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223)
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430) - feat(core): require user consent before MCP server OAuth by ehedlund in
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs [#18132](https://github.com/google-gemini/gemini-cli/pull/18132)
by @abhipatel12 in - fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429) by skeshive in
- feat(agents): implement first-run experience for project-level sub-agents by [#18231](https://github.com/google-gemini/gemini-cli/pull/18231)
@gundermanc in - feat(ui): move user identity display to header by sehoon38 in
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266) [#18216](https://github.com/google-gemini/gemini-cli/pull/18216)
- Update extensions docs by @chrstnb in - fix: enforce folder trust for workspace settings, skills, and context by
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093) galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596)
- Docs: Refactor left nav on the website by @jkcinouye in
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
- feat(plan): add persistent plan file storage by @jerop in
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
- Fix extensions config error by @chrstnb in
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
- fix(plan): remove subagent invocation from plan mode by @jerop in
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
- feat(ui): add solid background color option for input prompt by @jacob314 in
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
- feat(cli): add global setting to disable UI spinners by @galz10 in
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
- fix(security): enforce strict policy directory permissions by @yunaseoul in
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
- test(core): fix tests in windows by @scidomino in
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
- Always map mac keys, even on other platforms by @scidomino in
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
- Ctrl-O by @jacob314 in
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
- feat(plan): update cycling order of approval modes by @Adib234 in
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
- fix(cli): restore 'Modify with editor' option in external terminals by
@abhipatel12 in
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
- Slash command for helping in debugging by @gundermanc in
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
- feat: add double-click to expand/collapse large paste placeholders by
@jackwotherspoon in
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
@abhipatel12 in
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
(regression) by @gsquared94 in
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
- feat(core): enforce server prefixes for MCP tools in agent definitions by
@abhipatel12 in
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
- paste transform followup by @jacob314 in
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
- feat(cli): add oncall command for issue triage by @sehoon38 in
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
- Fix sidebar issue for extensions link by @chrstnb in
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
- Change formatting to prevent UI redressing attacks by @scidomino in
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
- Fix cluster of bugs in the settings dialog. by @jacob314 in
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
- Update sidebar to resolve site build issues by @chrstnb in
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
- fix(admin): fix a few bugs related to admin controls by @skeshive in
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
- revert bad changes to tests by @scidomino in
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
- feat(cli): show candidate issue state reason and duplicate status in triage by
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
- Fix missing slash commands when Gemini CLI is in a project with a package.json
that doesn't follow semantic versioning by @Adib234 in
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
- feat(core): Model family-specific system prompts by @joshualitt in
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
- Sub-agents documentation. by @gundermanc in
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
- Load extension settings for hooks, agents, skills by @chrstnb in
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
- Fix issue where Gemini CLI can make changes when simply asked a question by
@gundermanc in
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
- Update docs-writer skill for editing and add style guide for reference. by
@g-samroberts in
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
- fix(ux): have user message display a short path for pasted images by @devr0306
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
- GEMINI.md polish by @jacob314 in
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
- refactor(core): centralize path validation and allow temp dir access for tools
by @NTaylorMullen in
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
mode. by @jacob314 in
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
- feat(skills): promote skills settings to stable by @abhipatel12 in
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
- feat(ui): add terminal cursor support by @jacob314 in
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
- Add support for an additional exclusion file besides .gitignore and
.geminiignore by @alisa-alisa in
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
- feat: add review-frontend-and-fix command by @galz10 in
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
**Full changelog**: **Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.26.0-preview.5...v0.27.0-preview.0 https://github.com/google-gemini/gemini-cli/compare/v0.27.0-preview.8...v0.28.0-preview.0
+4
View File
@@ -995,6 +995,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** If false, disallows MCP servers from being used. - **Description:** If false, disallows MCP servers from being used.
- **Default:** `true` - **Default:** `true`
- **`admin.mcp.config`** (object):
- **Description:** Admin-configured MCP servers.
- **Default:** `{}`
- **`admin.skills.enabled`** (boolean): - **`admin.skills.enabled`** (boolean):
- **Description:** If false, disallows agent skills from being used. - **Description:** If false, disallows agent skills from being used.
- **Default:** `true` - **Default:** `true`
+2 -4
View File
@@ -100,10 +100,8 @@ Connect Gemini CLI to external services and other development tools.
the Model Context Protocol. the Model Context Protocol.
- **[IDE integration](./ide-integration/index.md):** Use Gemini CLI alongside VS - **[IDE integration](./ide-integration/index.md):** Use Gemini CLI alongside VS
Code. Code.
- **[Hooks](./hooks/index.md):** (Preview) Write scripts that run on specific - **[Hooks](./hooks/index.md):** Write scripts that run on specific CLI events.
CLI events. - **[Agent skills](./cli/skills.md):** Add specialized expertise and workflows.
- **[Agent skills](./cli/skills.md):** (Preview) Add specialized expertise and
workflows.
- **[Sub-agents](./core/subagents.md):** (Preview) Delegate tasks to specialized - **[Sub-agents](./core/subagents.md):** (Preview) Delegate tasks to specialized
agents. agents.
+8 -8
View File
@@ -123,14 +123,6 @@
"items": [ "items": [
{ "label": "FAQ", "slug": "docs/faq" }, { "label": "FAQ", "slug": "docs/faq" },
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" }, { "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
{
"label": "Releases",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
]
},
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" }, { "label": "Terms and privacy", "slug": "docs/tos-privacy" },
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" }, { "label": "Troubleshooting", "slug": "docs/troubleshooting" },
{ "label": "Uninstall", "slug": "docs/cli/uninstall" } { "label": "Uninstall", "slug": "docs/cli/uninstall" }
@@ -148,5 +140,13 @@
{ "label": "Local development", "slug": "docs/local-development" }, { "label": "Local development", "slug": "docs/local-development" },
{ "label": "NPM package structure", "slug": "docs/npm" } { "label": "NPM package structure", "slug": "docs/npm" }
] ]
},
{
"label": "Releases",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
]
} }
] ]
@@ -17,32 +17,26 @@ import yargs from 'yargs';
import { debugLogger } from '@google/gemini-cli-core'; import { debugLogger } from '@google/gemini-cli-core';
import { import {
updateSetting, updateSetting,
promptForSetting,
getScopedEnvContents, getScopedEnvContents,
type ExtensionSetting, type ExtensionSetting,
} from '../../config/extensions/extensionSettings.js'; } from '../../config/extensions/extensionSettings.js';
import prompts from 'prompts'; import prompts from 'prompts';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
const { const { mockExtensionManager, mockGetExtensionManager, mockLoadSettings } =
mockExtensionManager, vi.hoisted(() => {
mockGetExtensionAndManager, const extensionManager = {
mockGetExtensionManager, loadExtensionConfig: vi.fn(),
mockLoadSettings, getExtensions: vi.fn(),
} = vi.hoisted(() => { loadExtensions: vi.fn(),
const extensionManager = { getSettings: vi.fn(),
loadExtensionConfig: vi.fn(), };
getExtensions: vi.fn(), return {
loadExtensions: vi.fn(), mockExtensionManager: extensionManager,
getSettings: vi.fn(), mockGetExtensionManager: vi.fn(),
}; mockLoadSettings: vi.fn().mockReturnValue({ merged: {} }),
return { };
mockExtensionManager: extensionManager, });
mockGetExtensionAndManager: vi.fn(),
mockGetExtensionManager: vi.fn(),
mockLoadSettings: vi.fn().mockReturnValue({ merged: {} }),
};
});
vi.mock('../../config/extension-manager.js', () => ({ vi.mock('../../config/extension-manager.js', () => ({
ExtensionManager: vi.fn().mockImplementation(() => mockExtensionManager), ExtensionManager: vi.fn().mockImplementation(() => mockExtensionManager),
@@ -62,10 +56,13 @@ vi.mock('../utils.js', () => ({
exitCli: vi.fn(), exitCli: vi.fn(),
})); }));
vi.mock('./utils.js', () => ({ vi.mock('./utils.js', async (importOriginal) => {
getExtensionAndManager: mockGetExtensionAndManager, const actual = await importOriginal<typeof import('./utils.js')>();
getExtensionManager: mockGetExtensionManager, return {
})); ...actual,
getExtensionManager: mockGetExtensionManager,
};
});
vi.mock('prompts'); vi.mock('prompts');
@@ -91,10 +88,6 @@ describe('extensions configure command', () => {
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
// Default behaviors // Default behaviors
mockLoadSettings.mockReturnValue({ merged: {} }); mockLoadSettings.mockReturnValue({ merged: {} });
mockGetExtensionAndManager.mockResolvedValue({
extension: null,
extensionManager: null,
});
mockGetExtensionManager.mockResolvedValue(mockExtensionManager); mockGetExtensionManager.mockResolvedValue(mockExtensionManager);
(ExtensionManager as unknown as Mock).mockImplementation( (ExtensionManager as unknown as Mock).mockImplementation(
() => mockExtensionManager, () => mockExtensionManager,
@@ -117,11 +110,6 @@ describe('extensions configure command', () => {
path = '/test/path', path = '/test/path',
) => { ) => {
const extension = { name, path, id }; const extension = { name, path, id };
mockGetExtensionAndManager.mockImplementation(async (n) => {
if (n === name)
return { extension, extensionManager: mockExtensionManager };
return { extension: null, extensionManager: null };
});
mockExtensionManager.getExtensions.mockReturnValue([extension]); mockExtensionManager.getExtensions.mockReturnValue([extension]);
mockExtensionManager.loadExtensionConfig.mockResolvedValue({ mockExtensionManager.loadExtensionConfig.mockResolvedValue({
@@ -144,17 +132,14 @@ describe('extensions configure command', () => {
expect.objectContaining({ name: 'test-ext' }), expect.objectContaining({ name: 'test-ext' }),
'test-id', 'test-id',
'TEST_VAR', 'TEST_VAR',
promptForSetting, expect.any(Function),
'user', 'user',
tempWorkspaceDir, tempWorkspaceDir,
); );
}); });
it('should handle missing extension', async () => { it('should handle missing extension', async () => {
mockGetExtensionAndManager.mockResolvedValue({ mockExtensionManager.getExtensions.mockReturnValue([]);
extension: null,
extensionManager: null,
});
await runCommand('config missing-ext TEST_VAR'); await runCommand('config missing-ext TEST_VAR');
@@ -190,7 +175,7 @@ describe('extensions configure command', () => {
expect.objectContaining({ name: 'test-ext' }), expect.objectContaining({ name: 'test-ext' }),
'test-id', 'test-id',
'VAR_1', 'VAR_1',
promptForSetting, expect.any(Function),
'user', 'user',
tempWorkspaceDir, tempWorkspaceDir,
); );
@@ -205,7 +190,7 @@ describe('extensions configure command', () => {
return {}; return {};
}, },
); );
(prompts as unknown as Mock).mockResolvedValue({ overwrite: true }); (prompts as unknown as Mock).mockResolvedValue({ confirm: true });
(updateSetting as Mock).mockResolvedValue(undefined); (updateSetting as Mock).mockResolvedValue(undefined);
await runCommand('config test-ext'); await runCommand('config test-ext');
@@ -241,7 +226,7 @@ describe('extensions configure command', () => {
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }]; const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
setupExtension('test-ext', settings); setupExtension('test-ext', settings);
(getScopedEnvContents as Mock).mockResolvedValue({ VAR_1: 'existing' }); (getScopedEnvContents as Mock).mockResolvedValue({ VAR_1: 'existing' });
(prompts as unknown as Mock).mockResolvedValue({ overwrite: false }); (prompts as unknown as Mock).mockResolvedValue({ confirm: false });
await runCommand('config test-ext'); await runCommand('config test-ext');
+20 -150
View File
@@ -5,18 +5,17 @@
*/ */
import type { CommandModule } from 'yargs'; import type { CommandModule } from 'yargs';
import type { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
import { import {
updateSetting, configureAllExtensions,
promptForSetting, configureExtension,
ExtensionSettingScope, configureSpecificSetting,
getScopedEnvContents, getExtensionManager,
} from '../../config/extensions/extensionSettings.js'; } from './utils.js';
import { getExtensionAndManager, getExtensionManager } from './utils.js';
import { loadSettings } from '../../config/settings.js'; import { loadSettings } from '../../config/settings.js';
import { debugLogger, coreEvents } from '@google/gemini-cli-core'; import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js'; import { exitCli } from '../utils.js';
import prompts from 'prompts';
import type { ExtensionConfig } from '../../config/extension.js';
interface ConfigureArgs { interface ConfigureArgs {
name?: string; name?: string;
setting?: string; setting?: string;
@@ -64,9 +63,12 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
} }
} }
const extensionManager = await getExtensionManager();
// Case 1: Configure specific setting for an extension // Case 1: Configure specific setting for an extension
if (name && setting) { if (name && setting) {
await configureSpecificSetting( await configureSpecificSetting(
extensionManager,
name, name,
setting, setting,
scope as ExtensionSettingScope, scope as ExtensionSettingScope,
@@ -74,152 +76,20 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
} }
// Case 2: Configure all settings for an extension // Case 2: Configure all settings for an extension
else if (name) { else if (name) {
await configureExtension(name, scope as ExtensionSettingScope); await configureExtension(
extensionManager,
name,
scope as ExtensionSettingScope,
);
} }
// Case 3: Configure all extensions // Case 3: Configure all extensions
else { else {
await configureAllExtensions(scope as ExtensionSettingScope); await configureAllExtensions(
extensionManager,
scope as ExtensionSettingScope,
);
} }
await exitCli(); await exitCli();
}, },
}; };
async function configureSpecificSetting(
extensionName: string,
settingKey: string,
scope: ExtensionSettingScope,
) {
const { extension, extensionManager } =
await getExtensionAndManager(extensionName);
if (!extension || !extensionManager) {
return;
}
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (!extensionConfig) {
debugLogger.error(
`Could not find configuration for extension "${extensionName}".`,
);
return;
}
await updateSetting(
extensionConfig,
extension.id,
settingKey,
promptForSetting,
scope,
process.cwd(),
);
}
async function configureExtension(
extensionName: string,
scope: ExtensionSettingScope,
) {
const { extension, extensionManager } =
await getExtensionAndManager(extensionName);
if (!extension || !extensionManager) {
return;
}
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
!extensionConfig ||
!extensionConfig.settings ||
extensionConfig.settings.length === 0
) {
debugLogger.log(
`Extension "${extensionName}" has no settings to configure.`,
);
return;
}
debugLogger.log(`Configuring settings for "${extensionName}"...`);
await configureExtensionSettings(extensionConfig, extension.id, scope);
}
async function configureAllExtensions(scope: ExtensionSettingScope) {
const extensionManager = await getExtensionManager();
const extensions = extensionManager.getExtensions();
if (extensions.length === 0) {
debugLogger.log('No extensions installed.');
return;
}
for (const extension of extensions) {
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
extensionConfig &&
extensionConfig.settings &&
extensionConfig.settings.length > 0
) {
debugLogger.log(`\nConfiguring settings for "${extension.name}"...`);
await configureExtensionSettings(extensionConfig, extension.id, scope);
}
}
}
async function configureExtensionSettings(
extensionConfig: ExtensionConfig,
extensionId: string,
scope: ExtensionSettingScope,
) {
const currentScopedSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
scope,
process.cwd(),
);
let workspaceSettings: Record<string, string> = {};
if (scope === ExtensionSettingScope.USER) {
workspaceSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
ExtensionSettingScope.WORKSPACE,
process.cwd(),
);
}
if (!extensionConfig.settings) return;
for (const setting of extensionConfig.settings) {
const currentValue = currentScopedSettings[setting.envVar];
const workspaceValue = workspaceSettings[setting.envVar];
if (workspaceValue !== undefined) {
debugLogger.log(
`Note: Setting "${setting.name}" is already configured in the workspace scope.`,
);
}
if (currentValue !== undefined) {
const response = await prompts({
type: 'confirm',
name: 'overwrite',
message: `Setting "${setting.name}" (${setting.envVar}) is already set. Overwrite?`,
initial: false,
});
if (!response.overwrite) {
continue;
}
}
await updateSetting(
extensionConfig,
extensionId,
setting.envVar,
promptForSetting,
scope,
process.cwd(),
);
}
}
+219 -8
View File
@@ -1,17 +1,54 @@
/** /**
* @license * @license
* Copyright 2025 Google LLC * Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { ExtensionManager } from '../../config/extension-manager.js'; import { ExtensionManager } from '../../config/extension-manager.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { loadSettings } from '../../config/settings.js'; import { loadSettings } from '../../config/settings.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js'; import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { import {
debugLogger, debugLogger,
type ResolvedExtensionSetting, type ResolvedExtensionSetting,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { ExtensionConfig } from '../../config/extension.js';
import prompts from 'prompts';
import {
promptForSetting,
updateSetting,
type ExtensionSetting,
getScopedEnvContents,
ExtensionSettingScope,
} from '../../config/extensions/extensionSettings.js';
export interface ConfigLogger {
log(message: string): void;
error(message: string): void;
}
export type RequestSettingCallback = (
setting: ExtensionSetting,
) => Promise<string>;
export type RequestConfirmationCallback = (message: string) => Promise<boolean>;
const defaultLogger: ConfigLogger = {
log: (message: string) => debugLogger.log(message),
error: (message: string) => debugLogger.error(message),
};
const defaultRequestSetting: RequestSettingCallback = async (setting) =>
promptForSetting(setting);
const defaultRequestConfirmation: RequestConfirmationCallback = async (
message,
) => {
const response = await prompts({
type: 'confirm',
name: 'confirm',
message,
initial: false,
});
return response.confirm;
};
export async function getExtensionManager() { export async function getExtensionManager() {
const workspaceDir = process.cwd(); const workspaceDir = process.cwd();
@@ -25,18 +62,192 @@ export async function getExtensionManager() {
return extensionManager; return extensionManager;
} }
export async function getExtensionAndManager(name: string) { export async function getExtensionAndManager(
const extensionManager = await getExtensionManager(); extensionManager: ExtensionManager,
name: string,
logger: ConfigLogger = defaultLogger,
) {
const extension = extensionManager const extension = extensionManager
.getExtensions() .getExtensions()
.find((ext) => ext.name === name); .find((ext) => ext.name === name);
if (!extension) { if (!extension) {
debugLogger.error(`Extension "${name}" is not installed.`); logger.error(`Extension "${name}" is not installed.`);
return { extension: null, extensionManager: null }; return { extension: null };
} }
return { extension, extensionManager }; return { extension };
}
export async function configureSpecificSetting(
extensionManager: ExtensionManager,
extensionName: string,
settingKey: string,
scope: ExtensionSettingScope,
logger: ConfigLogger = defaultLogger,
requestSetting: RequestSettingCallback = defaultRequestSetting,
) {
const { extension } = await getExtensionAndManager(
extensionManager,
extensionName,
logger,
);
if (!extension) {
return;
}
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (!extensionConfig) {
logger.error(
`Could not find configuration for extension "${extensionName}".`,
);
return;
}
await updateSetting(
extensionConfig,
extension.id,
settingKey,
requestSetting,
scope,
process.cwd(),
);
logger.log(`Setting "${settingKey}" updated.`);
}
export async function configureExtension(
extensionManager: ExtensionManager,
extensionName: string,
scope: ExtensionSettingScope,
logger: ConfigLogger = defaultLogger,
requestSetting: RequestSettingCallback = defaultRequestSetting,
requestConfirmation: RequestConfirmationCallback = defaultRequestConfirmation,
) {
const { extension } = await getExtensionAndManager(
extensionManager,
extensionName,
logger,
);
if (!extension) {
return;
}
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
!extensionConfig ||
!extensionConfig.settings ||
extensionConfig.settings.length === 0
) {
logger.log(`Extension "${extensionName}" has no settings to configure.`);
return;
}
logger.log(`Configuring settings for "${extensionName}"...`);
await configureExtensionSettings(
extensionConfig,
extension.id,
scope,
logger,
requestSetting,
requestConfirmation,
);
}
export async function configureAllExtensions(
extensionManager: ExtensionManager,
scope: ExtensionSettingScope,
logger: ConfigLogger = defaultLogger,
requestSetting: RequestSettingCallback = defaultRequestSetting,
requestConfirmation: RequestConfirmationCallback = defaultRequestConfirmation,
) {
const extensions = extensionManager.getExtensions();
if (extensions.length === 0) {
logger.log('No extensions installed.');
return;
}
for (const extension of extensions) {
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
extensionConfig &&
extensionConfig.settings &&
extensionConfig.settings.length > 0
) {
logger.log(`\nConfiguring settings for "${extension.name}"...`);
await configureExtensionSettings(
extensionConfig,
extension.id,
scope,
logger,
requestSetting,
requestConfirmation,
);
}
}
}
export async function configureExtensionSettings(
extensionConfig: ExtensionConfig,
extensionId: string,
scope: ExtensionSettingScope,
logger: ConfigLogger = defaultLogger,
requestSetting: RequestSettingCallback = defaultRequestSetting,
requestConfirmation: RequestConfirmationCallback = defaultRequestConfirmation,
) {
const currentScopedSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
scope,
process.cwd(),
);
let workspaceSettings: Record<string, string> = {};
if (scope === ExtensionSettingScope.USER) {
workspaceSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
ExtensionSettingScope.WORKSPACE,
process.cwd(),
);
}
if (!extensionConfig.settings) return;
for (const setting of extensionConfig.settings) {
const currentValue = currentScopedSettings[setting.envVar];
const workspaceValue = workspaceSettings[setting.envVar];
if (workspaceValue !== undefined) {
logger.log(
`Note: Setting "${setting.name}" is already configured in the workspace scope.`,
);
}
if (currentValue !== undefined) {
const confirmed = await requestConfirmation(
`Setting "${setting.name}" (${setting.envVar}) is already set. Overwrite?`,
);
if (!confirmed) {
continue;
}
}
await updateSetting(
extensionConfig,
extensionId,
setting.envVar,
requestSetting,
scope,
process.cwd(),
);
}
} }
export function getFormattedSettingValue( export function getFormattedSettingValue(
+206
View File
@@ -18,6 +18,7 @@ import {
type ExtensionLoader, type ExtensionLoader,
debugLogger, debugLogger,
ApprovalMode, ApprovalMode,
type MCPServerConfig,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js'; import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import { type Settings, createTestMergedSettings } from './settings.js'; import { type Settings, createTestMergedSettings } from './settings.js';
@@ -1441,6 +1442,211 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
}); });
}); });
describe('loadCliConfig with admin.mcp.config', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
const localMcpServers: Record<string, MCPServerConfig> = {
serverA: {
command: 'npx',
args: ['-y', '@mcp/server-a'],
env: { KEY: 'VALUE' },
cwd: '/local/cwd',
trust: false,
},
serverB: {
command: 'npx',
args: ['-y', '@mcp/server-b'],
trust: false,
},
};
const baseSettings = createTestMergedSettings({
mcp: { serverCommand: 'npx -y @mcp/default-server' },
mcpServers: localMcpServers,
});
it('should use local configuration if admin allowlist is empty', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
mcp: baseSettings.mcp,
mcpServers: localMcpServers,
admin: {
...baseSettings.admin,
mcp: { enabled: true, config: {} },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getMcpServers()).toEqual(localMcpServers);
expect(config.getMcpServerCommand()).toBe('npx -y @mcp/default-server');
});
it('should ignore locally configured servers not present in the allowlist', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const adminAllowlist: Record<string, MCPServerConfig> = {
serverA: {
type: 'sse',
url: 'https://admin-server-a.com/sse',
trust: true,
},
};
const settings = createTestMergedSettings({
mcp: baseSettings.mcp,
mcpServers: localMcpServers,
admin: {
...baseSettings.admin,
mcp: { enabled: true, config: adminAllowlist },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
const mergedServers = config.getMcpServers();
expect(mergedServers).toHaveProperty('serverA');
expect(mergedServers).not.toHaveProperty('serverB');
});
it('should clear command, args, env, and cwd for present servers', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const adminAllowlist: Record<string, MCPServerConfig> = {
serverA: {
type: 'sse',
url: 'https://admin-server-a.com/sse',
trust: true,
},
};
const settings = createTestMergedSettings({
mcpServers: localMcpServers,
admin: {
...baseSettings.admin,
mcp: { enabled: true, config: adminAllowlist },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
const serverA = config.getMcpServers()?.['serverA'];
expect(serverA).toEqual({
...localMcpServers['serverA'],
type: 'sse',
url: 'https://admin-server-a.com/sse',
trust: true,
command: undefined,
args: undefined,
env: undefined,
cwd: undefined,
httpUrl: undefined,
tcp: undefined,
});
});
it('should not initialize a server if it is in allowlist but missing locally', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const adminAllowlist: Record<string, MCPServerConfig> = {
serverC: {
type: 'sse',
url: 'https://admin-server-c.com/sse',
trust: true,
},
};
const settings = createTestMergedSettings({
mcpServers: localMcpServers,
admin: {
...baseSettings.admin,
mcp: { enabled: true, config: adminAllowlist },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
const mergedServers = config.getMcpServers();
expect(mergedServers).not.toHaveProperty('serverC');
expect(Object.keys(mergedServers || {})).toHaveLength(0);
});
it('should merge local fields and prefer admin tool filters', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const adminAllowlist: Record<string, MCPServerConfig> = {
serverA: {
type: 'sse',
url: 'https://admin-server-a.com/sse',
trust: true,
includeTools: ['admin_tool'],
},
};
const localMcpServersWithTools: Record<string, MCPServerConfig> = {
serverA: {
...localMcpServers['serverA'],
includeTools: ['local_tool'],
timeout: 1234,
},
};
const settings = createTestMergedSettings({
mcpServers: localMcpServersWithTools,
admin: {
...baseSettings.admin,
mcp: { enabled: true, config: adminAllowlist },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
const serverA = config.getMcpServers()?.['serverA'];
expect(serverA).toMatchObject({
timeout: 1234,
includeTools: ['admin_tool'],
type: 'sse',
url: 'https://admin-server-a.com/sse',
trust: true,
});
expect(serverA).not.toHaveProperty('command');
expect(serverA).not.toHaveProperty('args');
expect(serverA).not.toHaveProperty('env');
expect(serverA).not.toHaveProperty('cwd');
expect(serverA).not.toHaveProperty('httpUrl');
expect(serverA).not.toHaveProperty('tcp');
});
it('should use local tool filters when admin does not define them', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const adminAllowlist: Record<string, MCPServerConfig> = {
serverA: {
type: 'sse',
url: 'https://admin-server-a.com/sse',
trust: true,
},
};
const localMcpServersWithTools: Record<string, MCPServerConfig> = {
serverA: {
...localMcpServers['serverA'],
includeTools: ['local_tool'],
},
};
const settings = createTestMergedSettings({
mcpServers: localMcpServersWithTools,
admin: {
...baseSettings.admin,
mcp: { enabled: true, config: adminAllowlist },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
const serverA = config.getMcpServers()?.['serverA'];
expect(serverA?.includeTools).toEqual(['local_tool']);
});
});
describe('loadCliConfig model selection', () => { describe('loadCliConfig model selection', () => {
beforeEach(() => { beforeEach(() => {
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]); vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
+48 -6
View File
@@ -12,7 +12,6 @@ import { extensionsCommand } from '../commands/extensions.js';
import { skillsCommand } from '../commands/skills.js'; import { skillsCommand } from '../commands/skills.js';
import { hooksCommand } from '../commands/hooks.js'; import { hooksCommand } from '../commands/hooks.js';
import { import {
Config,
setGeminiMdFilename as setServerGeminiMdFilename, setGeminiMdFilename as setServerGeminiMdFilename,
getCurrentGeminiMdFilename, getCurrentGeminiMdFilename,
ApprovalMode, ApprovalMode,
@@ -34,12 +33,16 @@ import {
ASK_USER_TOOL_NAME, ASK_USER_TOOL_NAME,
getVersion, getVersion,
PREVIEW_GEMINI_MODEL_AUTO, PREVIEW_GEMINI_MODEL_AUTO,
type HookDefinition,
type HookEventName,
type OutputFormat,
coreEvents, coreEvents,
GEMINI_MODEL_ALIAS_AUTO, GEMINI_MODEL_ALIAS_AUTO,
getAdminErrorMessage, getAdminErrorMessage,
Config,
} from '@google/gemini-cli-core';
import type {
MCPServerConfig,
HookDefinition,
HookEventName,
OutputFormat,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { import {
type Settings, type Settings,
@@ -687,6 +690,45 @@ export async function loadCliConfig(
? mcpEnablementManager.getEnablementCallbacks() ? mcpEnablementManager.getEnablementCallbacks()
: undefined; : undefined;
const adminAllowlist = settings.admin?.mcp?.config;
let mcpServerCommand = mcpEnabled ? settings.mcp?.serverCommand : undefined;
let mcpServers = mcpEnabled ? settings.mcpServers : {};
if (mcpEnabled && adminAllowlist && Object.keys(adminAllowlist).length > 0) {
const filteredMcpServers: Record<string, MCPServerConfig> = {};
for (const [serverId, localConfig] of Object.entries(mcpServers)) {
const adminConfig = adminAllowlist[serverId];
if (adminConfig) {
const mergedConfig = {
...localConfig,
url: adminConfig.url,
type: adminConfig.type,
trust: adminConfig.trust,
};
// Remove local connection details
delete mergedConfig.command;
delete mergedConfig.args;
delete mergedConfig.env;
delete mergedConfig.cwd;
delete mergedConfig.httpUrl;
delete mergedConfig.tcp;
if (
(adminConfig.includeTools && adminConfig.includeTools.length > 0) ||
(adminConfig.excludeTools && adminConfig.excludeTools.length > 0)
) {
mergedConfig.includeTools = adminConfig.includeTools;
mergedConfig.excludeTools = adminConfig.excludeTools;
}
filteredMcpServers[serverId] = mergedConfig;
}
}
mcpServers = filteredMcpServers;
mcpServerCommand = undefined;
}
return new Config({ return new Config({
sessionId, sessionId,
clientVersion: await getVersion(), clientVersion: await getVersion(),
@@ -706,8 +748,8 @@ export async function loadCliConfig(
excludeTools, excludeTools,
toolDiscoveryCommand: settings.tools?.discoveryCommand, toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand, toolCallCommand: settings.tools?.callCommand,
mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined, mcpServerCommand,
mcpServers: mcpEnabled ? settings.mcpServers : {}, mcpServers,
mcpEnablementCallbacks, mcpEnablementCallbacks,
mcpEnabled, mcpEnabled,
extensionsEnabled, extensionsEnabled,
@@ -821,5 +821,74 @@ describe('extensionSettings', () => {
); );
// Should complete without error // Should complete without error
}); });
it('should throw error if env var name contains invalid characters', async () => {
const securityConfig: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [{ name: 's2', description: 'd2', envVar: 'VAR-BAD' }],
};
mockRequestSetting.mockResolvedValue('value');
await expect(
updateSetting(
securityConfig,
'12345',
'VAR-BAD',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
),
).rejects.toThrow(/Invalid environment variable name/);
});
it('should throw error if env var value contains newlines', async () => {
mockRequestSetting.mockResolvedValue('value\nwith\nnewlines');
await expect(
updateSetting(
config,
'12345',
'VAR1',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
),
).rejects.toThrow(/Invalid environment variable value/);
});
it('should quote values with spaces', async () => {
mockRequestSetting.mockResolvedValue('value with spaces');
await updateSetting(
config,
'12345',
'VAR1',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
);
const expectedEnvPath = path.join(extensionDir, '.env');
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
expect(actualContent).toContain('VAR1="value with spaces"');
});
it('should escape quotes in values', async () => {
mockRequestSetting.mockResolvedValue('value with "quotes"');
await updateSetting(
config,
'12345',
'VAR1',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
);
const expectedEnvPath = path.join(extensionDir, '.env');
const actualContent = await fsPromises.readFile(expectedEnvPath, 'utf-8');
expect(actualContent).toContain('VAR1="value with \\"quotes\\""');
});
}); });
}); });
@@ -130,7 +130,19 @@ export async function maybePromptForSettings(
function formatEnvContent(settings: Record<string, string>): string { function formatEnvContent(settings: Record<string, string>): string {
let envContent = ''; let envContent = '';
for (const [key, value] of Object.entries(settings)) { for (const [key, value] of Object.entries(settings)) {
const formattedValue = value.includes(' ') ? `"${value}"` : value; if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
throw new Error(
`Invalid environment variable name: "${key}". Must contain only alphanumeric characters and underscores.`,
);
}
if (value.includes('\n') || value.includes('\r')) {
throw new Error(
`Invalid environment variable value for "${key}". Values cannot contain newlines.`,
);
}
const formattedValue = value.includes(' ')
? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
: value;
envContent += `${key}=${formattedValue}\n`; envContent += `${key}=${formattedValue}\n`;
} }
return envContent; return envContent;
+27 -1
View File
@@ -76,7 +76,11 @@ import {
LoadedSettings, LoadedSettings,
sanitizeEnvVar, sanitizeEnvVar,
} from './settings.js'; } from './settings.js';
import { FatalConfigError, GEMINI_DIR } from '@google/gemini-cli-core'; import {
FatalConfigError,
GEMINI_DIR,
type MCPServerConfig,
} from '@google/gemini-cli-core';
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
import { import {
getSettingsSchema, getSettingsSchema,
@@ -2350,6 +2354,28 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true); expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
}); });
it('should un-nest MCP configuration from remote settings', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
const mcpServers: Record<string, MCPServerConfig> = {
'admin-server': {
url: 'http://admin-mcp.com',
type: 'sse',
trust: true,
},
};
loadedSettings.setRemoteAdminSettings({
mcpSetting: {
mcpEnabled: true,
mcpConfig: {
mcpServers,
},
},
});
expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers);
});
it('should set skills based on unmanagedCapabilitiesEnabled', () => { it('should set skills based on unmanagedCapabilitiesEnabled', () => {
const loadedSettings = loadSettings(); const loadedSettings = loadSettings();
loadedSettings.setRemoteAdminSettings({ loadedSettings.setRemoteAdminSettings({
+4 -1
View File
@@ -412,7 +412,10 @@ export class LoadedSettings {
} }
admin.secureModeEnabled = !strictModeDisabled; admin.secureModeEnabled = !strictModeDisabled;
admin.mcp = { enabled: mcpSetting?.mcpEnabled }; admin.mcp = {
enabled: mcpSetting?.mcpEnabled,
config: mcpSetting?.mcpConfig?.mcpServers,
};
admin.extensions = { admin.extensions = {
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled, enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled,
}; };
+14
View File
@@ -1867,6 +1867,20 @@ const SETTINGS_SCHEMA = {
showInDialog: false, showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE, mergeStrategy: MergeStrategy.REPLACE,
}, },
config: {
type: 'object',
label: 'MCP Config',
category: 'Admin',
requiresRestart: false,
default: {} as Record<string, MCPServerConfig>,
description: 'Admin-configured MCP servers.',
showInDialog: false,
mergeStrategy: MergeStrategy.REPLACE,
additionalProperties: {
type: 'object',
ref: 'MCPServerConfig',
},
},
}, },
}, },
skills: { skills: {
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { type ReactElement } from 'react';
import type { import type {
ExtensionLoader, ExtensionLoader,
GeminiCLIExtension, GeminiCLIExtension,
@@ -15,7 +17,12 @@ import {
completeExtensionsAndScopes, completeExtensionsAndScopes,
extensionsCommand, extensionsCommand,
} from './extensionsCommand.js'; } from './extensionsCommand.js';
import {
ConfigExtensionDialog,
type ConfigExtensionDialogProps,
} from '../components/ConfigExtensionDialog.js';
import { type CommandContext, type SlashCommand } from './types.js'; import { type CommandContext, type SlashCommand } from './types.js';
import { import {
describe, describe,
it, it,
@@ -53,6 +60,20 @@ vi.mock('node:fs/promises', () => ({
stat: vi.fn(), stat: vi.fn(),
})); }));
vi.mock('../../config/extensions/extensionSettings.js', () => ({
ExtensionSettingScope: {
USER: 'user',
WORKSPACE: 'workspace',
},
getScopedEnvContents: vi.fn().mockResolvedValue({}),
promptForSetting: vi.fn(),
updateSetting: vi.fn(),
}));
vi.mock('prompts', () => ({
default: vi.fn(),
}));
vi.mock('../../config/extensions/update.js', () => ({ vi.mock('../../config/extensions/update.js', () => ({
updateExtension: vi.fn(), updateExtension: vi.fn(),
checkForAllExtensionUpdates: vi.fn(), checkForAllExtensionUpdates: vi.fn(),
@@ -107,27 +128,31 @@ const allExt: GeminiCLIExtension = {
describe('extensionsCommand', () => { describe('extensionsCommand', () => {
let mockContext: CommandContext; let mockContext: CommandContext;
const mockDispatchExtensionState = vi.fn(); const mockDispatchExtensionState = vi.fn();
let mockExtensionLoader: unknown;
beforeEach(() => { beforeEach(() => {
vi.resetAllMocks(); vi.resetAllMocks();
mockExtensionLoader = Object.create(ExtensionManager.prototype);
Object.assign(mockExtensionLoader as object, {
enableExtension: mockEnableExtension,
disableExtension: mockDisableExtension,
installOrUpdateExtension: mockInstallExtension,
uninstallExtension: mockUninstallExtension,
getExtensions: mockGetExtensions,
loadExtensionConfig: vi.fn().mockResolvedValue({
name: 'test-ext',
settings: [{ name: 'setting1', envVar: 'SETTING1' }],
}),
});
mockGetExtensions.mockReturnValue([inactiveExt, activeExt, allExt]); mockGetExtensions.mockReturnValue([inactiveExt, activeExt, allExt]);
vi.mocked(open).mockClear(); vi.mocked(open).mockClear();
mockContext = createMockCommandContext({ mockContext = createMockCommandContext({
services: { services: {
config: { config: {
getExtensions: mockGetExtensions, getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockImplementation(() => { getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
const actual = Object.create(ExtensionManager.prototype);
Object.assign(actual, {
enableExtension: mockEnableExtension,
disableExtension: mockDisableExtension,
installOrUpdateExtension: mockInstallExtension,
uninstallExtension: mockUninstallExtension,
getExtensions: mockGetExtensions,
});
return actual;
}),
getWorkingDir: () => '/test/dir', getWorkingDir: () => '/test/dir',
}, },
}, },
@@ -978,4 +1003,102 @@ describe('extensionsCommand', () => {
expect(suggestions).toEqual(['ext1']); expect(suggestions).toEqual(['ext1']);
}); });
}); });
describe('config', () => {
let configAction: SlashCommand['action'];
beforeEach(async () => {
configAction = extensionsCommand(true).subCommands?.find(
(cmd) => cmd.name === 'config',
)?.action;
expect(configAction).not.toBeNull();
mockContext.invocation!.name = 'config';
const prompts = (await import('prompts')).default;
vi.mocked(prompts).mockResolvedValue({ overwrite: true });
const { getScopedEnvContents } = await import(
'../../config/extensions/extensionSettings.js'
);
vi.mocked(getScopedEnvContents).mockResolvedValue({});
});
it('should return dialog to configure all extensions if no args provided', async () => {
const result = await configAction!(mockContext, '');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const dialogResult = result;
const component =
dialogResult.component as ReactElement<ConfigExtensionDialogProps>;
expect(component.type).toBe(ConfigExtensionDialog);
expect(component.props.configureAll).toBe(true);
expect(component.props.extensionManager).toBeDefined();
});
it('should return dialog to configure specific extension', async () => {
const result = await configAction!(mockContext, 'ext-one');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const dialogResult = result;
const component =
dialogResult.component as ReactElement<ConfigExtensionDialogProps>;
expect(component.type).toBe(ConfigExtensionDialog);
expect(component.props.extensionName).toBe('ext-one');
expect(component.props.settingKey).toBeUndefined();
expect(component.props.configureAll).toBe(false);
});
it('should return dialog to configure specific setting for an extension', async () => {
const result = await configAction!(mockContext, 'ext-one SETTING1');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const dialogResult = result;
const component =
dialogResult.component as ReactElement<ConfigExtensionDialogProps>;
expect(component.type).toBe(ConfigExtensionDialog);
expect(component.props.extensionName).toBe('ext-one');
expect(component.props.settingKey).toBe('SETTING1');
expect(component.props.scope).toBe('user'); // Default scope
});
it('should respect scope argument passed to dialog', async () => {
const result = await configAction!(
mockContext,
'ext-one SETTING1 --scope=workspace',
);
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const dialogResult = result;
const component =
dialogResult.component as ReactElement<ConfigExtensionDialogProps>;
expect(component.props.scope).toBe('workspace');
});
it('should show error for invalid extension name', async () => {
await configAction!(mockContext, '../invalid');
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Invalid extension name. Names cannot contain path separators or "..".',
});
});
// "should inform if extension has no settings" - This check is now inside ConfigExtensionDialog logic.
// We can test that we still return a dialog, and the dialog will handle logical checks via utils.ts
// For unit testing extensionsCommand, we just ensure delegation.
it('should return dialog even if extension has no settings (dialog handles logic)', async () => {
const result = await configAction!(mockContext, 'ext-one');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const dialogResult = result;
const component =
dialogResult.component as ReactElement<ConfigExtensionDialogProps>;
expect(component.type).toBe(ConfigExtensionDialog);
});
});
}); });
@@ -32,6 +32,10 @@ import { SettingScope } from '../../config/settings.js';
import { McpServerEnablementManager } from '../../config/mcp/mcpServerEnablement.js'; import { McpServerEnablementManager } from '../../config/mcp/mcpServerEnablement.js';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { stat } from 'node:fs/promises'; import { stat } from 'node:fs/promises';
import { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
import { type ConfigLogger } from '../../commands/extensions/utils.js';
import { ConfigExtensionDialog } from '../components/ConfigExtensionDialog.js';
import React from 'react';
function showMessageIfNoExtensions( function showMessageIfNoExtensions(
context: CommandContext, context: CommandContext,
@@ -583,6 +587,77 @@ async function uninstallAction(context: CommandContext, args: string) {
} }
} }
async function configAction(context: CommandContext, args: string) {
const parts = args.trim().split(/\s+/).filter(Boolean);
let scope = ExtensionSettingScope.USER;
const scopeEqIndex = parts.findIndex((p) => p.startsWith('--scope='));
if (scopeEqIndex > -1) {
const scopeVal = parts[scopeEqIndex].split('=')[1];
if (scopeVal === 'workspace') {
scope = ExtensionSettingScope.WORKSPACE;
} else if (scopeVal === 'user') {
scope = ExtensionSettingScope.USER;
}
parts.splice(scopeEqIndex, 1);
} else {
const scopeIndex = parts.indexOf('--scope');
if (scopeIndex > -1) {
const scopeVal = parts[scopeIndex + 1];
if (scopeVal === 'workspace' || scopeVal === 'user') {
scope =
scopeVal === 'workspace'
? ExtensionSettingScope.WORKSPACE
: ExtensionSettingScope.USER;
parts.splice(scopeIndex, 2);
}
}
}
const otherArgs = parts;
const name = otherArgs[0];
const setting = otherArgs[1];
if (name) {
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
context.ui.addItem({
type: MessageType.ERROR,
text: 'Invalid extension name. Names cannot contain path separators or "..".',
});
return;
}
}
const extensionManager = context.services.config?.getExtensionLoader();
if (!(extensionManager instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
);
return;
}
const logger: ConfigLogger = {
log: (message: string) => {
context.ui.addItem({ type: MessageType.INFO, text: message.trim() });
},
error: (message: string) =>
context.ui.addItem({ type: MessageType.ERROR, text: message }),
};
return {
type: 'custom_dialog' as const,
component: React.createElement(ConfigExtensionDialog, {
extensionManager,
onClose: () => context.ui.removeComponent(),
extensionName: name,
settingKey: setting,
scope,
configureAll: !name && !setting,
loggerAdapter: logger,
}),
};
}
/** /**
* Exported for testing. * Exported for testing.
*/ */
@@ -701,6 +776,14 @@ const restartCommand: SlashCommand = {
completion: completeExtensions, completion: completeExtensions,
}; };
const configCommand: SlashCommand = {
name: 'config',
description: 'Configure extension settings',
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: configAction,
};
export function extensionsCommand( export function extensionsCommand(
enableExtensionReloading?: boolean, enableExtensionReloading?: boolean,
): SlashCommand { ): SlashCommand {
@@ -711,6 +794,7 @@ export function extensionsCommand(
installCommand, installCommand,
uninstallCommand, uninstallCommand,
linkCommand, linkCommand,
configCommand,
] ]
: []; : [];
return { return {
@@ -0,0 +1,343 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useEffect, useState, useRef, useCallback } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { ExtensionManager } from '../../config/extension-manager.js';
import {
configureExtension,
configureSpecificSetting,
configureAllExtensions,
type ConfigLogger,
type RequestSettingCallback,
type RequestConfirmationCallback,
} from '../../commands/extensions/utils.js';
import {
ExtensionSettingScope,
type ExtensionSetting,
} from '../../config/extensions/extensionSettings.js';
import { TextInput } from './shared/TextInput.js';
import { useTextBuffer } from './shared/text-buffer.js';
import { DialogFooter } from './shared/DialogFooter.js';
import { type Key, useKeypress } from '../hooks/useKeypress.js';
export interface ConfigExtensionDialogProps {
extensionManager: ExtensionManager;
onClose: () => void;
extensionName?: string;
settingKey?: string;
scope?: ExtensionSettingScope;
configureAll?: boolean;
loggerAdapter: ConfigLogger;
}
type DialogState =
| { type: 'IDLE' }
| { type: 'BUSY'; message?: string }
| {
type: 'ASK_SETTING';
setting: ExtensionSetting;
resolve: (val: string) => void;
initialValue?: string;
}
| {
type: 'ASK_CONFIRMATION';
message: string;
resolve: (val: boolean) => void;
}
| { type: 'DONE' }
| { type: 'ERROR'; error: Error };
export const ConfigExtensionDialog: React.FC<ConfigExtensionDialogProps> = ({
extensionManager,
onClose,
extensionName,
settingKey,
scope = ExtensionSettingScope.USER,
configureAll,
loggerAdapter,
}) => {
const [state, setState] = useState<DialogState>({ type: 'IDLE' });
const [logMessages, setLogMessages] = useState<string[]>([]);
// Buffers for input
const settingBuffer = useTextBuffer({
initialText: '',
viewport: { width: 80, height: 1 },
singleLine: true,
isValidPath: () => true,
});
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const addLog = useCallback(
(msg: string) => {
setLogMessages((prev) => [...prev, msg].slice(-5)); // Keep last 5
loggerAdapter.log(msg);
},
[loggerAdapter],
);
const requestSetting: RequestSettingCallback = useCallback(
async (setting) =>
new Promise<string>((resolve) => {
if (!mounted.current) return;
settingBuffer.setText(''); // Clear buffer
setState({
type: 'ASK_SETTING',
setting,
resolve: (val) => {
resolve(val);
setState({ type: 'BUSY', message: 'Updating...' });
},
});
}),
[settingBuffer],
);
const requestConfirmation: RequestConfirmationCallback = useCallback(
async (message) =>
new Promise<boolean>((resolve) => {
if (!mounted.current) return;
setState({
type: 'ASK_CONFIRMATION',
message,
resolve: (val) => {
resolve(val);
setState({ type: 'BUSY', message: 'Processing...' });
},
});
}),
[],
);
useEffect(() => {
async function run() {
try {
setState({ type: 'BUSY', message: 'Initializing...' });
// Wrap logger to capture logs locally too
const localLogger: ConfigLogger = {
log: (msg) => {
addLog(msg);
},
error: (msg) => {
addLog('Error: ' + msg);
loggerAdapter.error(msg);
},
};
if (configureAll) {
await configureAllExtensions(
extensionManager,
scope,
localLogger,
requestSetting,
requestConfirmation,
);
} else if (extensionName && settingKey) {
await configureSpecificSetting(
extensionManager,
extensionName,
settingKey,
scope,
localLogger,
requestSetting,
);
} else if (extensionName) {
await configureExtension(
extensionManager,
extensionName,
scope,
localLogger,
requestSetting,
requestConfirmation,
);
}
if (mounted.current) {
setState({ type: 'DONE' });
// Delay close slightly to show done
setTimeout(onClose, 1000);
}
} catch (err: unknown) {
if (mounted.current) {
const error = err instanceof Error ? err : new Error(String(err));
setState({ type: 'ERROR', error });
loggerAdapter.error(error.message);
}
}
}
// Only run once
if (state.type === 'IDLE') {
void run();
}
}, [
extensionManager,
extensionName,
settingKey,
scope,
configureAll,
loggerAdapter,
requestSetting,
requestConfirmation,
addLog,
onClose,
state.type,
]);
// Handle Input Submission
const handleSettingSubmit = (val: string) => {
if (state.type === 'ASK_SETTING') {
state.resolve(val);
}
};
// Handle Keys for Confirmation
useKeypress(
(key: Key) => {
if (state.type === 'ASK_CONFIRMATION') {
if (key.name === 'y' || key.name === 'return') {
state.resolve(true);
return true;
}
if (key.name === 'n' || key.name === 'escape') {
state.resolve(false);
return true;
}
}
if (state.type === 'DONE' || state.type === 'ERROR') {
if (key.name === 'return' || key.name === 'escape') {
onClose();
return true;
}
}
return false;
},
{
isActive:
state.type === 'ASK_CONFIRMATION' ||
state.type === 'DONE' ||
state.type === 'ERROR',
},
);
if (state.type === 'BUSY' || state.type === 'IDLE') {
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
>
<Text color={theme.text.secondary}>
{state.type === 'BUSY' ? state.message : 'Starting...'}
</Text>
{logMessages.map((msg, i) => (
<Text key={i}>{msg}</Text>
))}
</Box>
);
}
if (state.type === 'ASK_SETTING') {
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
>
<Text bold color={theme.text.primary}>
Configure {state.setting.name}
</Text>
<Text color={theme.text.secondary}>
{state.setting.description || state.setting.envVar}
</Text>
<Box flexDirection="row" marginTop={1}>
<Text color={theme.text.accent}>{'> '}</Text>
<TextInput
buffer={settingBuffer}
onSubmit={handleSettingSubmit}
focus={true}
placeholder={`Enter value for ${state.setting.name}`}
/>
</Box>
<DialogFooter primaryAction="Enter to submit" />
</Box>
);
}
if (state.type === 'ASK_CONFIRMATION') {
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
>
<Text color={theme.status.warning} bold>
Confirmation Required
</Text>
<Text>{state.message}</Text>
<Box marginTop={1}>
<Text color={theme.text.secondary}>
Press{' '}
<Text color={theme.text.accent} bold>
Y
</Text>{' '}
to confirm or{' '}
<Text color={theme.text.accent} bold>
N
</Text>{' '}
to cancel
</Text>
</Box>
</Box>
);
}
if (state.type === 'ERROR') {
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.error}
paddingX={1}
>
<Text color={theme.status.error} bold>
Error
</Text>
<Text>{state.error.message}</Text>
<DialogFooter primaryAction="Enter to close" />
</Box>
);
}
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.success}
paddingX={1}
>
<Text color={theme.status.success} bold>
Configuration Complete
</Text>
<DialogFooter primaryAction="Enter to close" />
</Box>
);
};
+12 -5
View File
@@ -394,16 +394,23 @@ export class GeminiChat {
return; // Stop the generator return; // Stop the generator
} }
if (isConnectionPhase) { // Check if the error is retryable (e.g., transient SSL errors
throw error; // like ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC)
}
lastError = error;
const isContentError = error instanceof InvalidStreamError;
const isRetryable = isRetryableError( const isRetryable = isRetryableError(
error, error,
this.config.getRetryFetchErrors(), this.config.getRetryFetchErrors(),
); );
// For connection phase errors, only retryable errors should continue
if (isConnectionPhase) {
if (!isRetryable || signal.aborted) {
throw error;
}
// Fall through to retry logic for retryable connection errors
}
lastError = error;
const isContentError = error instanceof InvalidStreamError;
if ( if (
(isContentError && isGemini2Model(model)) || (isContentError && isGemini2Model(model)) ||
(isRetryable && !signal.aborted) (isRetryable && !signal.aborted)
@@ -274,4 +274,204 @@ describe('GeminiChat Network Retries', () => {
expect(mockLogContentRetry).not.toHaveBeenCalled(); expect(mockLogContentRetry).not.toHaveBeenCalled();
}); });
it('should retry on SSL error during connection phase (ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC)', async () => {
// Create an SSL error that occurs during connection (before any yield)
const sslError = new Error(
'SSL routines:ssl3_read_bytes:sslv3 alert bad record mac',
);
(sslError as NodeJS.ErrnoException).code =
'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
vi.mocked(mockContentGenerator.generateContentStream)
// First call: throw SSL error immediately (connection phase)
.mockRejectedValueOnce(sslError)
// Second call: succeed
.mockImplementationOnce(async () =>
(async function* () {
yield {
candidates: [
{
content: { parts: [{ text: 'Success after SSL retry' }] },
finishReason: 'STOP',
},
],
} as unknown as GenerateContentResponse;
})(),
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-ssl-retry',
new AbortController().signal,
);
const events: StreamEvent[] = [];
for await (const event of stream) {
events.push(event);
}
// Should have retried and succeeded
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
expect(retryEvent).toBeDefined();
const successChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Success after SSL retry',
);
expect(successChunk).toBeDefined();
// Verify the API was called twice (initial + retry)
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(2);
});
it('should retry on ECONNRESET error during connection phase', async () => {
const connectionError = new Error('read ECONNRESET');
(connectionError as NodeJS.ErrnoException).code = 'ECONNRESET';
vi.mocked(mockContentGenerator.generateContentStream)
.mockRejectedValueOnce(connectionError)
.mockImplementationOnce(async () =>
(async function* () {
yield {
candidates: [
{
content: {
parts: [{ text: 'Success after connection retry' }],
},
finishReason: 'STOP',
},
],
} as unknown as GenerateContentResponse;
})(),
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-connection-retry',
new AbortController().signal,
);
const events: StreamEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
expect(retryEvent).toBeDefined();
const successChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Success after connection retry',
);
expect(successChunk).toBeDefined();
});
it('should NOT retry on non-retryable error during connection phase', async () => {
const nonRetryableError = new Error('Some non-retryable error');
vi.mocked(mockContentGenerator.generateContentStream).mockRejectedValueOnce(
nonRetryableError,
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-no-connection-retry',
new AbortController().signal,
);
await expect(async () => {
for await (const _ of stream) {
// consume
}
}).rejects.toThrow(nonRetryableError);
// Should only be called once (no retry)
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(1);
});
it('should retry on SSL error during stream iteration (mid-stream failure)', async () => {
// This simulates the exact scenario from issue #17318 where the error
// occurs during a long session while streaming content
const sslError = new Error(
'request to https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent failed',
) as NodeJS.ErrnoException & { type?: string };
sslError.type = 'system';
sslError.errno = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC' as unknown as number;
sslError.code = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
vi.mocked(mockContentGenerator.generateContentStream)
// First call: yield some content, then throw SSL error mid-stream
.mockImplementationOnce(async () =>
(async function* () {
yield {
candidates: [
{ content: { parts: [{ text: 'Partial response...' }] } },
],
} as unknown as GenerateContentResponse;
// SSL error occurs while waiting for more data
throw sslError;
})(),
)
// Second call: succeed
.mockImplementationOnce(async () =>
(async function* () {
yield {
candidates: [
{
content: { parts: [{ text: 'Complete response after retry' }] },
finishReason: 'STOP',
},
],
} as unknown as GenerateContentResponse;
})(),
);
const stream = await chat.sendMessageStream(
{ model: 'test-model' },
'test message',
'prompt-id-ssl-mid-stream',
new AbortController().signal,
);
const events: StreamEvent[] = [];
for await (const event of stream) {
events.push(event);
}
// Should have received partial content, then retry, then success
const partialChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Partial response...',
);
expect(partialChunk).toBeDefined();
const retryEvent = events.find((e) => e.type === StreamEventType.RETRY);
expect(retryEvent).toBeDefined();
const successChunk = events.find(
(e) =>
e.type === StreamEventType.CHUNK &&
e.value.candidates?.[0]?.content?.parts?.[0]?.text ===
'Complete response after retry',
);
expect(successChunk).toBeDefined();
// Verify retry logging was called with NETWORK_ERROR type
expect(mockLogContentRetry).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
error_type: 'NETWORK_ERROR',
}),
);
});
}); });
+81
View File
@@ -409,6 +409,87 @@ describe('retryWithBackoff', () => {
await vi.runAllTimersAsync(); await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success'); await expect(promise).resolves.toBe('success');
}); });
it('should retry on SSL error code (ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC)', async () => {
const error = new Error('SSL error');
(error as any).code = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
const mockFn = vi
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn, {
initialDelayMs: 1,
maxDelayMs: 1,
});
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on SSL error code in deeply nested cause chain', async () => {
const deepCause = new Error('OpenSSL error');
(deepCause as any).code = 'ERR_SSL_BAD_RECORD_MAC';
const middleCause = new Error('TLS handshake failed');
(middleCause as any).cause = deepCause;
const outerError = new Error('fetch failed');
(outerError as any).cause = middleCause;
const mockFn = vi
.fn()
.mockRejectedValueOnce(outerError)
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn, {
initialDelayMs: 1,
maxDelayMs: 1,
});
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on EPROTO error (generic protocol/SSL error)', async () => {
const error = new Error('Protocol error');
(error as any).code = 'EPROTO';
const mockFn = vi
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn, {
initialDelayMs: 1,
maxDelayMs: 1,
});
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on gaxios-style SSL error with code property', async () => {
// This matches the exact structure from issue #17318
const error = new Error(
'request to https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent failed',
);
(error as any).type = 'system';
(error as any).errno = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
(error as any).code = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
const mockFn = vi
.fn()
.mockRejectedValueOnce(error)
.mockResolvedValue('success');
const promise = retryWithBackoff(mockFn, {
initialDelayMs: 1,
maxDelayMs: 1,
});
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
}); });
describe('Flash model fallback for OAuth users', () => { describe('Flash model fallback for OAuth users', () => {
+22 -2
View File
@@ -54,6 +54,12 @@ const RETRYABLE_NETWORK_CODES = [
'ENOTFOUND', 'ENOTFOUND',
'EAI_AGAIN', 'EAI_AGAIN',
'ECONNREFUSED', 'ECONNREFUSED',
// SSL/TLS transient errors
'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC',
'ERR_SSL_WRONG_VERSION_NUMBER',
'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
'ERR_SSL_BAD_RECORD_MAC',
'EPROTO', // Generic protocol error (often SSL-related)
]; ];
function getNetworkErrorCode(error: unknown): string | undefined { function getNetworkErrorCode(error: unknown): string | undefined {
@@ -72,8 +78,22 @@ function getNetworkErrorCode(error: unknown): string | undefined {
return directCode; return directCode;
} }
if (typeof error === 'object' && error !== null && 'cause' in error) { // Traverse the cause chain to find error codes (SSL errors are often nested)
return getCode((error as { cause: unknown }).cause); let current: unknown = error;
const maxDepth = 5; // Prevent infinite loops in case of circular references
for (let depth = 0; depth < maxDepth; depth++) {
if (
typeof current !== 'object' ||
current === null ||
!('cause' in current)
) {
break;
}
current = (current as { cause: unknown }).cause;
const code = getCode(current);
if (code) {
return code;
}
} }
return undefined; return undefined;
+10
View File
@@ -1712,6 +1712,16 @@
"markdownDescription": "If false, disallows MCP servers from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `true`", "markdownDescription": "If false, disallows MCP servers from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `true`",
"default": true, "default": true,
"type": "boolean" "type": "boolean"
},
"config": {
"title": "MCP Config",
"description": "Admin-configured MCP servers.",
"markdownDescription": "Admin-configured MCP servers.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`",
"default": {},
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/MCPServerConfig"
}
} }
}, },
"additionalProperties": false "additionalProperties": false