mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-22 15:51:18 -07:00
Merge branch 'main' into feat/card-component
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user