Compare commits

...

119 Commits

Author SHA1 Message Date
Shreya Keshive e41970ed21 init 2026-02-03 15:47:54 -05:00
Jerop Kipruto 675ca07c8b chore(core): explicitly state plan storage path in prompt (#18222) 2026-02-03 19:55:43 +00:00
Adam Weidman 40f505e257 docs: document GEMINI_CLI_HOME environment variable (#18219) 2026-02-03 19:49:37 +00:00
Serghei 5407878138 fix(core): Respect user's .gitignore preference (#15482)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2026-02-03 19:37:21 +00:00
christine betts 14e2e09d10 Match on extension ID when stopping extensions (#18218) 2026-02-03 19:29:15 +00:00
Adib234 0365f13caa feat(plan): use custom deny messages in plan mode policies (#18195) 2026-02-03 19:23:22 +00:00
Coco Sheng 3183e4137a fix(test): improve test isolation and enable subagent evaluations (#18138) 2026-02-03 19:05:26 +00:00
Jerop Kipruto 4aa295994d feat(plan): add exit_plan_mode ui and prompt (#18162) 2026-02-03 18:04:07 +00:00
Sehoon Shon e1bd1d239f Set default max attempts to 3 and use the common variable (#18209) 2026-02-03 17:47:13 +00:00
Adam Weidman b84585d0c8 feat(core): Add A2A auth config types (#18205) 2026-02-03 17:44:22 +00:00
Bryan Morgan 0e7944df4f refactor: localize ACP error parsing logic to cli package (#18193) 2026-02-03 16:32:20 +00:00
christine betts d8837ec95e Remove MCP servers on extension uninstall (#18121)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-03 16:18:09 +00:00
Adam Weidman dc37b190fa refactor(core): robust trimPreservingTrailingNewline and regression test (#18196) 2026-02-03 15:51:09 +00:00
Alexander Farber 19b1a74c99 feat(core): add draft-2020-12 JSON Schema support with lenient fallback (#15060)
Co-authored-by: A.K.M. Adib <adibakm@google.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-02-03 15:49:08 +00:00
Adib234 1d045792ce Add link to rewind doc in commands.md (#17961) 2026-02-03 15:06:33 +00:00
matt korwel a8b4c38c89 chore(core): reassign telemetry keys to avoid server conflict (#18161)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-03 06:43:57 -06:00
N. Taylor Mullen ad8796b02d feat(core): add .agents/skills directory alias for skill discovery (#18151) 2026-02-03 06:07:36 +00:00
Bryan Morgan e7bfd2bf83 fix(cli): resolve environment loading and auth validation issues in ACP mode (#18025)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-03 05:54:10 +00:00
Gaurav 1b274b081d fix(core): prioritize detailed error messages for code assist setup (#17852) 2026-02-03 04:27:55 +00:00
Sandy Tao 5b254c379c feat(core): rename search_file_content tool to grep_search and add legacy alias (#18003) 2026-02-03 04:18:24 +00:00
Jerop Kipruto ed26ea49e9 feat(plan): add core logic and exit_plan_mode tool definition (#18110) 2026-02-03 03:30:03 +00:00
Adib234 01e33465bd feat(plan): handle inconsistency in schedulers (#17813) 2026-02-03 03:10:04 +00:00
Jack Wotherspoon 18cce6a9ab fix: improve Ctrl+R reverse search (#18075) 2026-02-03 01:03:28 +00:00
Gal Zahavi 18d7d1a92c feat: update review-frontend-and-fix slash command to review-and-fix (#18146) 2026-02-03 00:44:20 +00:00
Shreya Keshive 0dd0b83612 fix(ide): no-op refactoring that moves the connection logic to helper functions (#18118) 2026-02-03 00:42:29 +00:00
Abhijit Balaji 4e4a55be35 feat(cli): implement automatic theme switching based on terminal background (#17976)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-02-03 00:39:17 +00:00
Pavan Shinde f57fd642df docs: fix help-wanted label spelling (#18114)
Co-authored-by: Christian Gunderman <gundermanc@gmail.com>
2026-02-03 00:38:52 +00:00
Sandy Tao 5e96373e6b feat(core): implement tool name aliasing for backward compatibility (#17974) 2026-02-03 00:34:14 +00:00
Jerop Kipruto 09beb648b8 refactor: migrate checks.ts utility to core and deduplicate (#18139) 2026-02-03 00:32:13 +00:00
Jerop Kipruto c159c85c89 fix(ui): prevent content leak in MaxSizedBox bottom overflow (#17991) 2026-02-02 23:37:33 +00:00
Jacob Richman 8cae90f404 Fix up/down arrow regression and add test. (#18108)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-02 13:55:45 -08:00
Jenna Inouye c5d0fc2c3e Docs: Revise docs/index.md (#17879) 2026-02-02 18:28:32 +00:00
Sandy Tao e860f517c0 feat(core): add setting to disable loop detection (#18008) 2026-02-02 18:13:20 +00:00
Sehoon Shon b0be1f1689 Sehoon/oncall filter (#18105)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-02 18:10:28 +00:00
Jenna Inouye 85dd6ef773 docs-writer skill: Update docs writer skill (#17928) 2026-02-02 18:07:25 +00:00
Jack Wotherspoon ae672881d1 docs: update clearContext to hookSpecificOutput (#18024) 2026-02-02 17:32:37 +00:00
Jerop Kipruto 9e7c10ad88 feat(plan): use placeholder for choice question "Other" option (#18101) 2026-02-02 17:00:13 +00:00
Victor Vianna 381669dce0 chore: delete autoAccept setting unused in production (#17862)
Co-authored-by: Victor Hugo Vianna Silva <victorvianna@google.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-02-02 13:49:17 +00:00
Bryan Morgan 707b3e85d5 fix(ci): prevent stale PR closer from incorrectly closing new PRs (#18069) 2026-02-01 15:13:29 -05:00
Bryan Morgan 7d36cc004f feat(acp): add session resume support (#18043)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-02-01 19:16:57 +00:00
Dmitry Lyalin cb4f0c6fa4 Fix Esc cancel during streaming (#18039) 2026-02-01 16:16:14 +00:00
Bryan Morgan b0f38104d7 fix(workflow): update maintainer check logic to be inclusive and case-insensitive (#18009) 2026-01-30 22:32:58 -05:00
Jacob Richman 7469ea0fca Fix truncation for AskQuestion (#18001) 2026-01-31 01:07:41 +00:00
Jacob Richman 00fdb30211 Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over AppContainer for input handling. (#17993) 2026-01-31 00:11:14 +00:00
g-samroberts 0fe8492569 Fix broken link. (#17972) 2026-01-30 21:31:07 +00:00
Abhi 76387d22ae fix(core): ensure YOLO mode auto-approves complex shell commands when parsing fails (#17920) 2026-01-30 20:44:17 +00:00
Sehoon Shon a362b7b382 lower the default max retries to reduce contention (#17975) 2026-01-30 20:00:31 +00:00
Jerop Kipruto 71308caf05 feat(plan): refactor ToolConfirmationPayload to union type (#17980) 2026-01-30 19:51:45 +00:00
kevinjwang1 6396ab1ccb Add allowedExtensions setting (#17695) 2026-01-30 19:43:51 +00:00
Sehoon Shon 12531a06f8 run npx pointing to the specific commit SHA (#17970)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-01-30 19:42:29 +00:00
lottielin d1cd69a20d feat(core): enable overriding CODE_ASSIST_API_VERSION with env var (#17942)
Co-authored-by: Charlotte Lin <charlotteclin@google.com>
2026-01-30 19:12:04 +00:00
Jerop Kipruto 62346875e4 feat(plan): reuse standard tool confirmation for AskUser tool (#17864)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-01-30 18:32:21 +00:00
christine betts 13e013230b Fix broken link in docs (#17959) 2026-01-30 18:27:25 +00:00
Jacob Richman bb6a336ca9 Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. (#17940) 2026-01-30 18:09:27 +00:00
Shreya Keshive f14d0c6a17 feat(admin): provide actionable error messages for disabled features (#17815) 2026-01-30 18:05:22 +00:00
Gal Zahavi b611f9a519 feat: Implement background shell commands (#14849) 2026-01-30 17:53:09 +00:00
Abhi d3bca5d97a feat(core): implement dynamic policy registration for subagents (#17838) 2026-01-30 16:57:54 +00:00
Bryan Morgan 94f4e027f8 feat(ci): add npx smoke test to verify installability (#17927) 2026-01-30 16:09:59 +00:00
Abhi 95b7d69d5b refactor(hooks): remove legacy tools.enableHooks setting (#17867) 2026-01-30 15:15:48 +00:00
Abhi f605628624 perf(core): optimize token calculation and add support for multimodal tool responses (#17835) 2026-01-30 15:15:00 +00:00
Emily Hedlund 2238802e97 feat(core): implement interactive and non-interactive consent for OAuth (#17699) 2026-01-30 14:57:34 +00:00
Jacob Richman 32cfce16bb bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X in vim mode. (#17938) 2026-01-30 07:31:47 +00:00
Jacob Richman 137080da45 Fix HalfLinePaddedBox in screenreader mode. (#17914) 2026-01-30 03:21:18 +00:00
Thomas Shephard 695785c69d feat: preserve EOL in files (#16087)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-01-30 00:57:06 +00:00
g-samroberts d43d772e65 Create skills page, update commands, refine docs (#17842) 2026-01-29 23:52:06 +00:00
Sandy Tao 59e3624ada feat(core): Isolate and cleanup truncated tool outputs (#17594) 2026-01-29 23:20:11 +00:00
Danielle Yim fdda3a2399 feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support (#16177)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-01-29 22:45:37 +00:00
Alisa 2de39311cf Refactoring of disabling of mouse tracking in e2e tests (#17902) 2026-01-29 22:05:19 +00:00
Sehoon Shon fc926cd0b0 Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" (#17898) 2026-01-29 21:13:00 +00:00
Jack Wotherspoon 567761fbc0 chore: revert IDE specific ASCII logo (#17887) 2026-01-29 20:44:02 +00:00
Sehoon Shon f2591aada6 fix(cli): use correct setting key for Cloud Shell auth (#17884) 2026-01-29 20:21:17 +00:00
Sehoon Shon 74468928e8 Revert "chore: refresh package-lock.json to resolve inconsistencies"
This reverts commit d561b995c0.
2026-01-29 15:29:26 -05:00
Sehoon Shon d561b995c0 chore: refresh package-lock.json to resolve inconsistencies 2026-01-29 15:28:40 -05:00
Alisa b44e432f55 Disable mouse tracking e2e (#17880)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-29 19:25:11 +00:00
Coco Sheng bcc6ee596a fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration - Fixes #17877 (#17803) 2026-01-29 19:08:34 +00:00
Christian Gunderman bc258eba4c Cleanup post delegate_to_agent removal (#17875) 2026-01-29 18:24:35 +00:00
Adib234 0e30055ae4 feat(plan): create generic Checklist component and refactor Todo (#17741) 2026-01-29 18:13:18 +00:00
Atharva Ralegankar ac7687c1dd fix(core): resolve DEP0040 punycode deprecation via patch-package (#17692)
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
2026-01-29 16:04:26 +00:00
Dev Randalpura f9da18e0cc feat(ux): update truncate dots to be shorter in tables (#17825) 2026-01-29 15:56:56 +00:00
Shreya Keshive f619cd8750 fix(admin): Rename secureModeEnabled to strictModeDisabled (#17789) 2026-01-29 15:31:00 +00:00
christine betts 42eedc9184 Resolve error thrown for sensitive values (#17826) 2026-01-29 14:59:26 +00:00
Adam Weidman eb17d94e91 docs: mark A2A subagents as experimental in subagents.md (#17863) 2026-01-29 14:48:27 +00:00
Jack Wotherspoon 9f1623688d fix: prevent ghost border for AskUserDialog (#17788) 2026-01-29 14:00:46 +00:00
Yuna Seol 7f066fd3a7 feat(cli): Display user identity (auth, email, tier) on startup (#17591)
Co-authored-by: Keith Guerin <keithguerin@gmail.com>
Co-authored-by: Yuna Seol <yunaseol@google.com>
2026-01-29 05:07:52 +00:00
Abhi 6d36219e55 feat(cli): show hooksConfig.enabled in settings dialog (#17810) 2026-01-29 04:58:20 +00:00
Benjamin G. 5cedd28733 Fix detection of bun as package manager (#17462)
Co-authored-by: Sehoon Shon <sshon@google.com>
2026-01-29 03:31:07 +00:00
Mark Cockram ba3e1ed1c7 fix(core): use returnDisplay for error result display (#14994)
Co-authored-by: Sehoon Shon <sshon@google.com>
2026-01-29 03:02:39 +00:00
Alexander Farber 7c2cdb225a fix(cli): handle PAT tokens and credentials in git remote URL parsing (#14650) 2026-01-29 02:41:30 +00:00
Tu Shaokun a91f4e692a fix(cli): list installed extensions when update target missing (#17082) 2026-01-29 02:13:40 +00:00
Vedant Mahajan 9bb175a506 test: add integration test to verify stdout/stderr routing (#17280)
Co-authored-by: Allen Hutchison <adh@google.com>
2026-01-29 00:01:57 +00:00
Sandy Tao 0b169e9867 fix(evals): use absolute path for activity log directory (#17830) 2026-01-28 15:20:21 -08:00
Tommaso Sciortino 553ae3ad69 feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and Shift+Cmd+Z/Shift+Alt+Z (#17800) 2026-01-28 22:57:27 +00:00
Dev Randalpura b2265cfde4 fix(ux): fixed off-by-some wrapping caused by fixed-width characters (#17816) 2026-01-28 21:52:37 +00:00
Sehoon Shon bee1267e2a feat(cli): add user identity info to stats command (#17612) 2026-01-28 21:26:33 +00:00
Jacob Richman 30065c51fb Change height for the ToolConfirmationQueue. (#17799) 2026-01-28 21:23:51 +00:00
Dev Randalpura 065e69a12b feat(ux): update cell border color and created test file for table rendering (#17798) 2026-01-28 19:55:01 +00:00
g-samroberts 519a0e617a Update release notes pages for 0.26.0 and 0.27.0-preview. (#17744) 2026-01-28 19:35:37 +00:00
Krushna Korade 0465de303a fix: persist and restore workspace directories on session resume (#17454)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-01-28 19:07:58 +00:00
Spencer beaa134f0e feat(extensions): add support for custom themes in extensions (#17327) 2026-01-28 18:58:35 +00:00
Adib234 47f4a3e50e Fix unintended credential exposure to MCP Servers (#17311)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-01-28 18:56:15 +00:00
Allen Hutchison 3787c71d15 feat(core): add support for custom deny messages in policy rules (#17427) 2026-01-28 18:52:48 +00:00
Sandy Tao 9e09db1ddb feat(cli): enable activity logging for non-interactive mode and evals (#17703) 2026-01-28 17:02:41 +00:00
Jack Wotherspoon 25ae1a1b54 test: add more tests for AskUser (#17720) 2026-01-28 16:57:16 +00:00
Abhi 65accca296 fix(core): suppress duplicate hook failure warnings during streaming (#17727) 2026-01-28 16:40:44 +00:00
g-samroberts 72f0733d98 Fix docs-writer skill issues (#17734) 2026-01-28 16:15:53 +00:00
christine betts 4d4e16cc8c [extension config] Make breaking optional value non-optional (#17785) 2026-01-28 16:11:21 +00:00
Shreya Keshive 370b165769 fix(admin): rename advanced features admin setting (#17786) 2026-01-28 15:53:05 +00:00
Sehoon Shon 899e8a9edc /oncall dedup - wrap texts to nextlines (#17782) 2026-01-28 15:13:25 +00:00
kapsner 4dca5c27c6 feat(core): add support for positron ide (#15045) (#15047)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
2026-01-28 15:08:03 +00:00
maruto 06c7dc561e fix: verify audio/video MIME types with content check (#16907) 2026-01-28 14:58:39 +00:00
Adib234 f54eaa1713 Rewind documentation (#17446) 2026-01-28 14:40:32 +00:00
steven semyung oh 6807a2fa28 Add a email privacy note to bug_report template (#17474)
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-01-28 13:37:54 +00:00
erikus 9aeeb69ab0 feat(cli): include auth method in /bug (#17569)
Co-authored-by: Erik Staab <estaab@google.com>
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-01-28 13:32:37 +00:00
jw bot 0a77999d73 test(core): mock fetch in OAuth transport fallback tests (#17059)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2026-01-28 13:30:08 +00:00
Abhi 9035ca32bc feat(skills): final stable promotion cleanup (#17726) 2026-01-28 04:56:04 +00:00
gemini-cli-robot ef3146eed6 chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb (#17725) 2026-01-28 03:25:06 +00:00
Leo Chiu ffbecd4eb4 docs: add CLI reference documentation (#17504)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-28 02:20:43 +00:00
Sehoon Shon 2c09785748 feat(cli): align hooks enable/disable with skills and improve completion (#16822) 2026-01-28 01:56:06 +00:00
N. Taylor Mullen 130b596c80 feat(commands): add /prompt-suggest slash command (#17264) 2026-01-28 01:32:20 +00:00
377 changed files with 21582 additions and 5809 deletions
+22
View File
@@ -0,0 +1,22 @@
description = "Analyze agent behavior and suggest high-level improvements to system prompts."
prompt = """
# Prompt Engineering Analysis
You are a world-class prompt engineer and an expert AI engineer at the top of your class. Your goal is to analyze a specific agent behavior or failure and suggest high-level improvements to the system instructions.
**Observed Behavior / Issue:**
{{args}}
**Reference Context:**
- System Prompt Logic: @packages/core/src/core/prompts.ts
### Task
1. **Analyze the Failure:** Review the provided behavior and identify the underlying instructional causes. Use the `/introspect` command output if provided by the user.
2. **Strategic Insights:** Share your technical view of the issue. Focus on the "why" and identify any instructional inertia or ambiguity.
3. **Propose Improvements:** Suggest high-level changes to the system instructions to prevent this behavior.
### Principles
- **Avoid Hyper-scoping:** Do not create narrow solutions for specific scenarios; aim for generalized improvements that handle classes of behavior.
- **Avoid Specific Examples in Suggestions:** Keep the proposed instructions semantic and high-level to prevent the agent from over-indexing on specific cases.
- **Maintain Operational Rigor:** Ensure suggestions do not compromise safety, security, or the quality of the agent's work.
"""
@@ -1,6 +1,6 @@
description = "Reviews a frontend PR or staged changes and automatically initiates a Pickle Fix loop for findings."
description = "Reviews a PR or staged changes and automatically initiates a Pickle Fix loop for findings."
prompt = """
You are an expert Frontend Reviewer and Pickle Rick Worker.
You are an expert Reviewer and Pickle Rick Worker.
Target: {{args}}
@@ -56,8 +56,6 @@ Follow these steps to conduct a thorough review:
pollution.
* Use `vi.useFakeTimers()` for tests involving time-based logic to avoid
flakiness.
* Avoid using `any` in tests; prefer proper types or `unknown` with
narrowing.
* When creating parameterized tests, give the parameters types to ensure
that the tests are type-safe.
8. Evaluate all react logic carefully keeping in mind that the author of the
@@ -105,7 +103,20 @@ Follow these steps to conduct a thorough review:
to include the new property or propose a new provider to add if the
property drilling is excessive. Only use providers for properties that are
consistent for the entire application.
9. General Gemini CLI design principles:
9. Evaluate `packages/core` (Services, Tools, Utilities):
* Ensure services are implemented as classes with clear lifecycle management (e.g., `initialize()` methods).
* Verify that `debugLogger` from `packages/core/src/utils/debugLogger.ts` is used for internal logging instead of `console`.
* Ensure all shell operations use `spawnAsync` from `packages/core/src/utils/shell-utils.ts` for consistent error handling and promise management.
* Check that filesystem errors are handled gracefully using `isNodeError` from `packages/core/src/utils/errors.ts`.
* Verify that new tools are added to `packages/core/src/tools/` and registered in `packages/core/src/tools/tool-registry.ts`.
* Ensure all new public services, utilities, and types are exported from `packages/core/src/index.ts`.
* Check that services are stateless where possible, or use the centralized `Storage` service for persistence.
* **Cross-Service Communication**: Prefer using the `coreEvents` bus (from `packages/core/src/utils/events.ts`) for asynchronous communication between services or to notify the UI of state changes. Avoid tight coupling between services.
10. Architectural Audit (Package Boundaries):
* **Logic Placement**: Non-UI logic (e.g., model orchestration, tool implementation, git/filesystem operations) MUST reside in `packages/core`. `packages/cli` should only contain UI/Ink components, command-line argument parsing, and user interaction logic.
* **Environment Isolation**: Core logic should not assume a TUI environment. Use the `ConfirmationBus` or `Output` abstractions for communicating with the user from Core.
* **Decoupling**: Actively look for opportunities to decouple services by using `coreEvents`. If a service is importing another service just to notify it of a change, it should probably be using an event instead.
11. General Gemini CLI design principles:
* Make sure that settings are only used for options that a user might
consider changing.
* Do not add new command line arguments and suggest settings instead.
@@ -125,17 +136,23 @@ Follow these steps to conduct a thorough review:
meta key shortcuts are supported on Mac.
* Be skeptical of function keys and keyboard shortcuts that are commonly
bound in VSCode as they may conflict.
10. TypeScript Best Practices:
12. TypeScript Best Practices:
* Use 'checkExhaustive' in the 'default' clause of 'switch' statements to
ensure all cases are handled.
* Avoid using the non-null assertion operator ('!') unless absolutely
necessary and you are confident the value is not null.
11. Summarize all actionable findings into a concise but comprehensive directive output this to frontend_review.md and advance to phase 2.
* **STRICT TYPING**: Strictly forbid 'any' and 'unknown' in both CLI and Core
packages. 'unknown' is only allowed if it is immediately narrowed using
type guards or Zod validation. Reject any code that uses 'any' or
'unknown' without narrowing.
13. **Ruthless Cleanup**:
* If you identify significant code duplication, technical debt, or "AI Slop" (boilerplate, redundant comments), explicitly suggest initiating a `ruthless-refactorer` loop to clean it up.
14. Summarize all actionable findings into a concise but comprehensive directive output this to review_findings.md and advance to phase 2.
Remember to use the GitHub CLI (`gh`) for all GitHub-related tasks, and local `git` commands if the target is 'staged'.
Phase 2:
You are initiating Pickle Rick - the ultimate coding agent.
You are initiating Pickle Rick - the ultimate engineering agent.
**Step 0: Persona Injection**
First, you **MUST** activate your persona.
@@ -160,7 +177,7 @@ bash "${extensionPath}/scripts/setup.sh" $ARGUMENTS
pwsh -File "${extensionPath}/scripts/setup.ps1" $ARGUMENTS
```
**CRITICAL**: Your request is to fix all findings in frontend_review.md
**CRITICAL**: Your request is to fix all findings in review_findings.md
**Step 2: Execution (Management)**
After setup, read the output to find the path to `state.json`.
@@ -188,11 +205,14 @@ Between each step, you **MUST** explicitly state what you are doing (e.g., "Movi
* **Validation**: IGNORE worker logs. DIRECTLY verify:
1. `git status` (Check for file changes)
2. `git diff` (Check code quality)
3. Run tests/build (Check functionality)
3. `npm run build` (Ensure the project still builds)
4. `npm run test` (Ensure no regressions)
5. `npm run lint` (Ensure code style is maintained)
6. `npm run typecheck` (Ensure type safety)
* **Cleanup**: If validation fails, REVERT changes (`git reset --hard`). If it passes, COMMIT changes.
* **Next Ticket**: Pick the next ticket and repeat.
4. **Cleanup**:
* **Action**: After all tickets are completed delete `frontend_review.md`.
* **Action**: After all tickets are completed delete `review_findings.md`.
**Loop Constraints:**
- **Iteration Count**: Monitor `"iteration"` in `state.json`. If `"max_iterations"` (if > 0) is reached, you must stop.
-5
View File
@@ -1,5 +0,0 @@
{
"experimental": {
"skills": true
}
}
+120 -56
View File
@@ -1,72 +1,136 @@
---
name: docs-writer
description:
Use this skill for writing, reviewing, and editing documentation (`/docs`
directory or any .md file) for Gemini CLI.
Always use this skill when the task involves writing, reviewing, or editing
documentation, specifically for any files in the `/docs` directory or any
`.md` files in the repository.
---
# `docs-writer` skill instructions
As an expert technical writer and editor for the Gemini CLI project, your goal
is to produce and refine documentation that is accurate, clear, consistent, and
easy for users to understand. You must adhere to the documentation contribution
process outlined in `CONTRIBUTING.md`.
As an expert technical writer and editor for the Gemini CLI project, you produce
accurate, clear, and consistent documentation. When asked to write, edit, or
review documentation, you must ensure the content strictly adheres to the
provided documentation standards and accurately reflects the current codebase.
Adhere to the contribution process in `CONTRIBUTING.md` and the following
project standards.
## Step 1: Understand the goal and create a plan
## Phase 1: Documentation standards
1. **Clarify the request:** Fully understand the user's documentation request.
Identify the core feature, command, or concept that needs work.
2. **Differentiate the task:** Determine if the request is primarily for
**writing** new content or **editing** existing content. If the request is
ambiguous (e.g., "fix the docs"), ask the user for clarification.
3. **Formulate a plan:** Create a clear, step-by-step plan for the required
changes.
Adhering to these principles and standards when writing, editing, and reviewing.
## Step 2: Investigate and gather information
### Voice and tone
Adopt a tone that balances professionalism with a helpful, conversational
approach.
1. **Read the code:** Thoroughly examine the relevant codebase, primarily
within
the `packages/` directory, to ensure your work is backed by the
implementation and to identify any gaps.
2. **Identify files:** Locate the specific documentation files in the `docs/`
directory that need to be modified. Always read the latest version of a file
before you begin work.
3. **Check for connections:** Consider related documentation. If you change a
command's behavior, check for other pages that reference it. If you add a new
page, check if `docs/sidebar.json` needs to be updated. Make sure all
links are up to date.
- **Perspective and tense:** Address the reader as "you." Use active voice and
present tense (e.g., "The API returns...").
- **Tone:** Professional, friendly, and direct.
- **Clarity:** Use simple vocabulary. Avoid jargon, slang, and marketing hype.
- **Global Audience:** Write in standard US English. Avoid idioms and cultural
references.
- **Requirements:** Be clear about requirements ("must") vs. recommendations
("we recommend"). Avoid "should."
- **Word Choice:** Avoid "please" and anthropomorphism (e.g., "the server
thinks"). Use contractions (don't, it's).
## Step 3: Write or edit the documentation
### Language and grammar
Write precisely to ensure your instructions are unambiguous.
1. **Follow the style guide:** Adhere to the rules in
`references/style-guide.md`. Read this file to understand the project's
documentation standards.
2. Ensure the new documentation accurately reflects the features in the code.
3. **Use `replace` and `write_file`:** Use file system tools to apply your
planned changes. For small edits, `replace` is preferred. For new files or
large rewrites, `write_file` is more appropriate.
- **Abbreviations:** Avoid Latin abbreviations; use "for example" (not "e.g.")
and "that is" (not "i.e.").
- **Punctuation:** Use the serial comma. Place periods and commas inside
quotation marks.
- **Dates:** Use unambiguous formats (e.g., "January 22, 2026").
- **Conciseness:** Use "lets you" instead of "allows you to." Use precise,
specific verbs.
- **Examples:** Use meaningful names in examples; avoid placeholders like
"foo" or "bar."
### Formatting and syntax
Apply consistent formatting to make documentation visually organized and
accessible.
- **Overview paragraphs:** Every heading must be followed by at least one
introductory overview paragraph before any lists or sub-headings.
- **Text wrap:** Wrap text at 80 characters (except long links or tables).
- **Casing:** Use sentence case for headings, titles, and bolded text.
- **Naming:** Always refer to the project as `Gemini CLI` (never
`the Gemini CLI`).
- **Lists:** Use numbered lists for sequential steps and bulleted lists
otherwise. Keep list items parallel in structure.
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
snippets, commands, and API elements. Focus on the task when discussing
interaction.
- **Links:** Use descriptive anchor text; avoid "click here." Ensure the link
makes sense out of context.
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
tables).
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
for all images.
### Structure
- **BLUF:** Start with an introduction explaining what to expect.
- **Experimental features:** If a feature is clearly noted as experimental,
add the following note immediately after the introductory paragraph:
`> **Note:** This is a preview feature currently under active development.`
- **Headings:** Use hierarchical headings to support the user journey.
- **Procedures:**
- Introduce lists of steps with a complete sentence.
- Start each step with an imperative verb.
- Number sequential steps; use bullets for non-sequential lists.
- Put conditions before instructions (e.g., "On the Settings page, click...").
- Provide clear context for where the action takes place.
- Indicate optional steps clearly (e.g., "Optional: ...").
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
(`> **Warning:**`).
- **Avoid using a table of contents:** If a table of contents is present, remove
it.
- **Next steps:** Conclude with a "Next steps" section if applicable.
## Phase 2: Preparation
Before modifying any documentation, thoroughly investigate the request and the
surrounding context.
1. **Clarify:** Understand the core request. Differentiate between writing new
content and editing existing content. If the request is ambiguous (e.g.,
"fix the docs"), ask for clarification.
2. **Investigate:** Examine relevant code (primarily in `packages/`) for
accuracy.
3. **Audit:** Read the latest versions of relevant files in `docs/`.
4. **Connect:** Identify all referencing pages if changing behavior. Check if
`docs/sidebar.json` needs updates.
5. **Plan:** Create a step-by-step plan before making changes.
## Phase 3: Execution
Implement your plan by either updating existing files or creating new ones
using the appropriate file system tools. Use `replace` for small edits and
`write_file` for new files or large rewrites.
### Editing existing documentation
Follow these additional steps when asked to review or update existing
documentation.
- **Gaps:** Identify areas where the documentation is incomplete or no longer
reflects existing code.
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
adding new sections to existing pages.
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
sentences to make them easier for users to understand.
- **Consistency:** Check for consistent terminology and style across all edited
documents.
## Phase 4: Verification and finalization
Perform a final quality check to ensure that all changes are correctly formatted
and that all links are functional.
### Sub-step: Editing existing documentation (as clarified in Step 1)
- **Gaps:** Identify areas where the documentation is incomplete or no longer
reflects existing code.
- **Tone:** Ensure the tone is active and engaging, not passive.
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
sentences to make them easier for users to understand.
- **Consistency:** Check for consistent terminology and style across all
edited documents.
## Step 4: Verify and finalize
1. **Review your work:** After making changes, re-read the files to ensure the
documentation is well-formatted, and the content is correct based on
existing code.
2. **Link verification:** Verify the validity of all links in the new content.
Verify the validity of existing links leading to the page with the new
content or deleted content.
2. **Offer to run npm format:** Once all changes are complete, offer to run the
project's formatting script to ensure consistency by proposing the command:
`npm run format`
1. **Accuracy:** Ensure content accurately reflects the implementation and
technical behavior.
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
3. **Link check:** Verify all new and existing links leading to or from modified
pages.
4. **Format:** Once all changes are complete, ask to execute `npm run format`
to ensure consistent formatting across the project. If the user confirms,
execute the command.
@@ -1,95 +0,0 @@
# Documentation style guide
## I. Core principles
1. **Clarity:** Write for easy understanding. Prioritize clear, direct, and
simple language.
2. **Consistency:** Use consistent terminology, formatting, and style
throughout the documentation.
3. **Accuracy:** Ensure all information is technically correct and up-to-date.
4. **Accessibility:** Design documentation to be usable by everyone. Focus on
semantic structure, clear link text, and image alternatives.
5. **Global audience:** Write in standard US English. Avoid slang, idioms, and
cultural references.
6. **Prescriptive:** Guide the reader by recommending specific actions and
paths, especially for complex tasks.
## II. Voice and tone
- **Professional yet friendly:** Maintain a helpful, knowledgeable, and
conversational tone without being frivolous.
- **Direct:** Get straight to the point. Keep paragraphs short and focused.
- **Second person:** Address the reader as "you."
- **Present tense:** Use the present tense to describe functionality (e.g., "The
API returns a JSON object.").
- **Avoid:** Jargon, slang, marketing hype, and overly casual language.
## III. Language and grammar
- **Active voice:** Prefer active voice over passive voice.
- _Example:_ "The system sends a notification." (Not: "A notification is sent
by the system.")
- **Contractions:** Use common contractions (e.g., "don't," "it's") to maintain
a natural tone.
- **Simple vocabulary:** Use common words. Define technical terms when
necessary.
- **Conciseness:** Keep sentences short and focused.
- **"Please":** Avoid using the word "please."
## IV. Procedures and steps
- Start each step with an imperative verb (e.g., "Connect to the database").
- Number steps sequentially.
- Introduce lists of steps with a complete sentence.
- Put conditions before instructions, not after.
- Provide clear context for where the action takes place (e.g., "In the
administration console...").
- Indicate optional steps clearly (e.g., "Optional: ...").
## V. Formatting and punctuation
- **Text wrap:** Wrap all text at 80 characters, with exceptions for long links
or tables.
- **Headings, titles, and bold text:** Use sentence case. Structure headings
hierarchically.
- **Lists:** Use numbered lists for sequential steps and bulleted lists for all
other lists. Keep list items parallel in structure.
- **Serial comma:** Use the serial comma (e.g., "one, two, and three").
- **Punctuation:** Use standard American punctuation. Place periods inside
quotation marks.
- **Dates:** Use unambiguous date formatting (e.g., "January 22, 2026").
## VI. UI, code, and links
- **UI elements:** Put UI elements in **bold**. Focus on the task when
discussing interaction.
- **Code:** Use `code font` for filenames, code snippets, commands, and API
elements. Use code blocks for multi-line samples.
- **Links:** Use descriptive link text that indicates what the link leads to.
Avoid "click here."
## VII. Word choice and terminology
- **Consistent naming:** Use product and feature names consistently. Always
refer to Gemini CLI as `Gemini CLI`, never `the Gemini CLI`.
- **Specific verbs:** Use precise verbs.
- **Avoid:**
- Latin abbreviations (e.g., use "for example" instead of "e.g.").
- Placeholder names like "foo" and "bar" in examples; use meaningful names
instead.
- Anthropomorphism (e.g., "The server thinks...").
- "Should": Be clear about requirements ("must") vs. recommendations ("we
recommend").
## VIII. Files and media
- **Filenames:** Use lowercase letters, separate words with hyphens (-), and use
standard ASCII characters.
- **Images:** Provide descriptive alt text for all images. Provide
high-resolution or vector images when practical.
## IX. Accessibility quick check
- Provide descriptive alt text for images.
- Ensure link text makes sense out of context.
- Use semantic HTML elements correctly (headings, lists, tables).
+1 -1
View File
@@ -28,7 +28,7 @@ body:
id: 'info'
attributes:
label: 'Client information'
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux).'
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux). Note that this output contains your email address. Consider removing it before submitting.'
value: |-
<details>
<summary>Client Information</summary>
+41
View File
@@ -177,6 +177,19 @@ jobs:
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
TARBALL=$(npm pack | tail -n 1)
# 2. Move to a fresh directory for isolation
mkdir -p ../smoke-test-dir
mv "$TARBALL" ../smoke-test-dir/
cd ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
- name: 'Wait for file system sync'
run: 'sleep 2'
@@ -252,6 +265,19 @@ jobs:
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
TARBALL=$(npm pack | tail -n 1)
# 2. Move to a fresh directory for isolation
mkdir -p ../smoke-test-dir
mv "$TARBALL" ../smoke-test-dir/
cd ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
- name: 'Wait for file system sync'
run: 'sleep 2'
@@ -396,6 +422,21 @@ jobs:
run: 'node ./bundle/gemini.js --version'
shell: 'pwsh'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
$PACK_OUTPUT = npm pack
$TARBALL = $PACK_OUTPUT[-1]
# 2. Move to a fresh directory for isolation
New-Item -ItemType Directory -Force -Path ../smoke-test-dir
Move-Item $TARBALL ../smoke-test-dir/
Set-Location ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
shell: 'pwsh'
ci:
name: 'CI'
if: 'always()'
@@ -43,19 +43,23 @@ jobs:
// 1. Fetch maintainers for verification
let maintainerLogins = new Set();
let teamFetchSucceeded = false;
try {
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
org: context.repo.owner,
team_slug: 'gemini-cli-maintainers'
});
maintainerLogins = new Set(members.map(m => m.login));
maintainerLogins = new Set(members.map(m => m.login.toLowerCase()));
teamFetchSucceeded = true;
core.info(`Successfully fetched ${maintainerLogins.size} team members from gemini-cli-maintainers`);
} catch (e) {
core.warning('Failed to fetch team members');
core.warning(`Failed to fetch team members from gemini-cli-maintainers: ${e.message}. Falling back to author_association only.`);
}
const isMaintainer = (login, assoc) => {
if (maintainerLogins.size > 0) return maintainerLogins.has(login);
return ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
const isTeamMember = maintainerLogins.has(login.toLowerCase());
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
return isTeamMember || isRepoMaintainer;
};
// 2. Determine which PRs to check
@@ -153,7 +157,17 @@ jobs:
const labels = pr.labels.map(l => l.name.toLowerCase());
if (labels.includes('help wanted') || labels.includes('🔒 maintainer only')) continue;
let lastActivity = new Date(0);
// Skip PRs that were created less than 30 days ago - they cannot be stale yet
const prCreatedAt = new Date(pr.created_at);
if (prCreatedAt > thirtyDaysAgo) {
const daysOld = Math.floor((Date.now() - prCreatedAt.getTime()) / (1000 * 60 * 60 * 24));
core.info(`PR #${pr.number} was created ${daysOld} days ago. Skipping staleness check.`);
continue;
}
// Initialize lastActivity to PR creation date (not epoch) as a safety baseline.
// This ensures we never incorrectly mark a PR as stale due to failed activity lookups.
let lastActivity = new Date(pr.created_at);
try {
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
@@ -177,8 +191,12 @@ jobs:
if (d > lastActivity) lastActivity = d;
}
}
} catch (e) {}
} catch (e) {
core.warning(`Failed to fetch reviews/comments for PR #${pr.number}: ${e.message}`);
}
// For maintainer PRs, the PR creation itself counts as maintainer activity.
// (Now redundant since we initialize to pr.created_at, but kept for clarity)
if (maintainerPr) {
const d = new Date(pr.created_at);
if (d > lastActivity) lastActivity = d;
@@ -28,22 +28,59 @@ jobs:
permission: 'write'
issue-type: 'pull-request'
- name: 'Get PR Status'
id: 'pr_status'
- name: 'Get Commits'
id: 'get_commits'
if: "startsWith(github.event.comment.body, '/patch')"
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
COMMENT_BODY: '${{ github.event.comment.body }}'
CURRENT_PR: '${{ github.event.issue.number }}'
run: |
gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json
echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT"
echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT"
# Find all PR numbers in the comment body (e.g., #123, owner/repo#456)
# The regex handles both formats and extracts just the number
comment_prs=$(echo "$COMMENT_BODY" | grep -oP '(?:\S+/)?\S+#\K\d+')
# Combine with the current PR number, ensuring no duplicates
all_prs=$(echo -e "${CURRENT_PR}\n${comment_prs}" | sort -u)
echo "Found PRs to patch: ${all_prs}"
commit_shas=()
unmerged_prs=()
merged_prs=()
for pr in $all_prs; do
echo "Checking status of PR #${pr}..."
# Get PR status (state and merge commit)
pr_status=$(gh pr view "$pr" --json mergeCommit,state)
state=$(echo "$pr_status" | jq -r .state)
if [[ "$state" != "MERGED" ]]; then
unmerged_prs+=("$pr")
else
commit_sha=$(echo "$pr_status" | jq -r .mergeCommit.oid)
commit_shas+=("$commit_sha")
merged_prs+=("$pr")
fi
done
if [[ ${#unmerged_prs[@]} -gt 0 ]]; then
echo "ALL_MERGED=false" >> "$GITHUB_OUTPUT"
echo "UNMERGED_PRS=$(IFS=,; echo "${unmerged_prs[*]}")" >> "$GITHUB_OUTPUT"
else
echo "ALL_MERGED=true" >> "$GITHUB_OUTPUT"
echo "COMMITS=$(IFS=,; echo "${commit_shas[*]}")" >> "$GITHUB_OUTPUT"
echo "PR_NUMBERS=$(IFS=,; echo "${merged_prs[*]}")" >> "$GITHUB_OUTPUT"
fi
- name: 'Dispatch if Merged'
if: "steps.pr_status.outputs.STATE == 'MERGED'"
if: "steps.get_commits.outputs.ALL_MERGED == 'true'"
id: 'dispatch_patch'
uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325'
env:
COMMENT_BODY: '${{ github.event.comment.body }}'
COMMITS: '${{ steps.get_commits.outputs.COMMITS }}'
PR_NUMBERS: '${{ steps.get_commits.outputs.PR_NUMBERS }}'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
@@ -60,17 +97,10 @@ jobs:
// /patch preview
if (commentBody.trim() === '/patch' || commentBody.trim() === '/patch both') {
channels = ['stable', 'preview'];
} else if (commentBody.trim() === '/patch stable') {
} else if (commentBody.includes('stable')) {
channels = ['stable'];
} else if (commentBody.trim() === '/patch preview') {
} else if (commentBody.includes('preview')) {
channels = ['preview'];
} else {
// Fallback parsing for legacy formats
if (commentBody.includes('channel=preview')) {
channels = ['preview'];
} else if (commentBody.includes('--channel preview')) {
channels = ['preview'];
}
}
console.log('Detected channels:', channels);
@@ -87,9 +117,9 @@ jobs:
workflow_id: 'release-patch-1-create-pr.yml',
ref: 'main',
inputs: {
commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
commits: process.env.COMMITS,
channel: channel,
original_pr: '${{ github.event.issue.number }}',
original_prs: process.env.PR_NUMBERS,
environment: 'prod'
}
});
@@ -123,13 +153,13 @@ jobs:
}
- name: 'Comment on Failure'
if: "startsWith(github.event.comment.body, '/patch') && steps.pr_status.outputs.STATE != 'MERGED'"
if: "startsWith(github.event.comment.body, '/patch') && steps.get_commits.outputs.ALL_MERGED == 'false'"
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
issue-number: '${{ github.event.issue.number }}'
body: |
:x: The `/patch` command failed. This pull request must be merged before a patch can be created.
:x: The `/patch` command failed. The following pull request(s) must be merged before a patch can be created: #${{ steps.get_commits.outputs.UNMERGED_PRS }}
- name: 'Final Status Comment - Success'
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && steps.dispatch_patch.outputs.dispatched_run_urls"
@@ -142,7 +172,8 @@ jobs:
**📋 Details:**
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
- **PRs**: `${{ steps.get_commits.outputs.PR_NUMBERS }}`
- **Commits**: `${{ steps.get_commits.outputs.COMMITS }}`
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
**🔗 Track Progress:**
@@ -160,7 +191,8 @@ jobs:
**📋 Details:**
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
- **PRs**: `${{ steps.get_commits.outputs.PR_NUMBERS }}`
- **Commits**: `${{ steps.get_commits.outputs.COMMITS }}`
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
**🔗 Track Progress:**
+12 -12
View File
@@ -1,13 +1,13 @@
name: 'Release: Patch (1) Create PR'
run-name: >-
Release Patch (1) Create PR | S:${{ inputs.channel }} | C:${{ inputs.commit }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}
Release Patch (1) Create PR | S:${{ inputs.channel }} | C:${{ inputs.commits }} ${{ inputs.original_prs && format('| PRs:#{0}', inputs.original_prs) || '' }}
on:
workflow_dispatch:
inputs:
commit:
description: 'The commit SHA to cherry-pick for the patch.'
commits:
description: 'The commit SHAs to cherry-pick for the patch (comma-separated).'
required: true
type: 'string'
channel:
@@ -27,8 +27,8 @@ on:
required: false
type: 'string'
default: 'main'
original_pr:
description: 'The original PR number to comment back on.'
original_prs:
description: 'The original PR numbers to comment back on (comma-separated).'
required: false
type: 'string'
environment:
@@ -85,9 +85,9 @@ jobs:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
PATCH_COMMIT: '${{ github.event.inputs.commit }}'
PATCH_COMMITS: '${{ github.event.inputs.commits }}'
PATCH_CHANNEL: '${{ github.event.inputs.channel }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
ORIGINAL_PRS: '${{ github.event.inputs.original_prs }}'
DRY_RUN: '${{ github.event.inputs.dry_run }}'
continue-on-error: true
run: |
@@ -95,9 +95,9 @@ jobs:
{
node scripts/releasing/create-patch-pr.js \
--cli-package-name="${CLI_PACKAGE_NAME}" \
--commit="${PATCH_COMMIT}" \
--commits="${PATCH_COMMITS}" \
--channel="${PATCH_CHANNEL}" \
--pullRequestNumber="${ORIGINAL_PR}" \
--pullRequestNumbers="${ORIGINAL_PRS}" \
--dry-run="${DRY_RUN}"
} 2>&1 | tee >(
echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
@@ -107,12 +107,12 @@ jobs:
echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
- name: 'Comment on Original PR'
if: 'always() && inputs.original_pr'
if: 'always() && inputs.original_prs'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
ORIGINAL_PRS: '${{ github.event.inputs.original_prs }}'
EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
COMMIT: '${{ github.event.inputs.commit }}'
COMMITS: '${{ github.event.inputs.commits }}'
CHANNEL: '${{ github.event.inputs.channel }}'
REPOSITORY: '${{ github.repository }}'
GITHUB_RUN_ID: '${{ github.run_id }}'
@@ -1,7 +1,7 @@
name: 'Release: Patch (3) Release'
run-name: >-
Release Patch (3) Release | T:${{ inputs.type }} | R:${{ inputs.release_ref }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}
Release Patch (3) Release | T:${{ inputs.type }} | R:${{ inputs.release_ref }} ${{ inputs.original_prs && format('| PRs:#{0}', inputs.original_prs) || '' }}
on:
workflow_dispatch:
@@ -27,8 +27,8 @@ on:
description: 'The branch, tag, or SHA to release from.'
required: true
type: 'string'
original_pr:
description: 'The original PR number to comment back on.'
original_prs:
description: 'The original PR numbers to comment back on.'
required: false
type: 'string'
environment:
@@ -208,10 +208,10 @@ jobs:
--label 'release-failure,priority/p0'
- name: 'Comment Success on Original PR'
if: '${{ success() && github.event.inputs.original_pr }}'
if: '${{ success() && github.event.inputs.original_prs }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
ORIGINAL_PRS: '${{ github.event.inputs.original_prs }}'
SUCCESS: 'true'
RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
@@ -225,10 +225,10 @@ jobs:
node scripts/releasing/patch-comment.js
- name: 'Comment Failure on Original PR'
if: '${{ failure() && github.event.inputs.original_pr }}'
if: '${{ failure() && github.event.inputs.original_prs }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
ORIGINAL_PRS: '${{ github.event.inputs.original_prs }}'
SUCCESS: 'false'
RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
+1 -1
View File
@@ -45,7 +45,7 @@ The process for contributing code is as follows:
`🔒Maintainers only`, this means it is reserved for project maintainers. We
will not accept pull requests related to these issues. In the near future,
we will explicitly mark issues looking for contributions using the
`help wanted` label. If you believe an issue is a good candidate for
`help-wanted` label. If you believe an issue is a good candidate for
community contribution, please leave a comment on the issue. A maintainer
will review it and apply the `help-wanted` label if appropriate. Only
maintainers should attempt to add the `help-wanted` label to an issue.
+3 -2
View File
@@ -72,7 +72,8 @@ powerful tool for developers.
## Documentation
- Always use the `docs-writer` skill when you are asked to write, edit, or
review any documentation.
- Documentation is located in the `docs/` directory.
- Suggest documentation updates when code changes render existing documentation
obsolete or incomplete.
- Located in the `docs/` directory.
- Use the `docs-writer` skill.
+17
View File
@@ -18,6 +18,23 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.26.0 - 2026-01-27
- **Agents and Skills:** We've introduced a new `skill-creator` skill
([#16394](https://github.com/google-gemini/gemini-cli/pull/16394) by
@NTaylorMullen), enabled agent skills by default, and added a generalist agent
to improve task routing
([#16638](https://github.com/google-gemini/gemini-cli/pull/16638) by
@joshualitt).
- **UI/UX Improvements:** You can now "Rewind" through your conversation history
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by @Adib234)
and use a new `/introspect` command for debugging.
- **Core and Scheduler Refactoring:** The core scheduler has been significantly
refactored to improve performance and reliability
([#16895](https://github.com/google-gemini/gemini-cli/pull/16895) by
@abhipatel12), and numerous performance and stability fixes have been
included.
## Announcements: v0.25.0 - 2026-01-20
- **Skills and Agents Improvements:** We've enhanced the `activate_skill` tool,
+315 -347
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.25.0
# Latest stable release: v0.26.0
Released: January 20, 2026
Released: January 27, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,360 +11,328 @@ npm install -g @google/gemini-cli
## Highlights
- **Skills and Agents Improvements:** Enhanced `activate_skill` tool, new
`pr-creator` skill, default enablement of skills, improved `cli_help` agent,
and a new `/agents refresh` command.
- **UI/UX Refinements:** Transparent feedback for skills, ability to switch
focus between shell and input with Tab, and dynamic terminal tab titles.
- **Core Functionality & Performance:** Support for built-in agent skills,
refined Gemini 3 system instructions, caching ignore instances for
performance, and improved retry mechanisms.
- **Bug Fixes and Stability:** Numerous bug fixes across the CLI, core, and
workflows, including issues with subagent delegation, unicode character
crashes, and sticky header regressions.
- **Enhanced Agent and Skill Capabilities:** This release introduces the new
`skill-creator` built-in skill, enables Agent Skills by default, and adds a
generalist agent to improve task routing. Security for skill installation has
also been enhanced with new consent prompts.
- **Improved UI and UX:** A new "Rewind" feature lets you walk back through
conversation history. We've also added an `/introspect` command for debugging
and unified various shell confirmation dialogs for a more consistent user
experience.
- **Core Stability and Performance:** This release includes significant
performance improvements, including a fix for PDF token estimation,
optimizations for large inputs, and prevention of OOM crashes. Key memory
management components like `LRUCache` have also been updated.
- **Scheduler and Policy Refactoring:** The core tool scheduler has been
decoupled into distinct orchestration, policy, and confirmation components,
and we've added an experimental event-driven scheduler to improve performance
and reliability.
## What's Changed
- feat(core): improve activate_skill tool and use lowercase XML tags by
@NTaylorMullen in
[#16009](https://github.com/google-gemini/gemini-cli/pull/16009)
- Add initiation method telemetry property by @gundermanc in
[#15818](https://github.com/google-gemini/gemini-cli/pull/15818)
- chore(release): bump version to 0.25.0-nightly.20260107.59a18e710 by
- fix: PDF token estimation (#16494) by @korade-krushna in
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
@gemini-cli-robot in
[#16048](https://github.com/google-gemini/gemini-cli/pull/16048)
- Hx support by @kevinfjiang in
[#16032](https://github.com/google-gemini/gemini-cli/pull/16032)
- [Skills] Foundation: Centralize management logic and feedback rendering by
@NTaylorMullen in
[#15952](https://github.com/google-gemini/gemini-cli/pull/15952)
- Introduce GEMINI_CLI_HOME for strict test isolation by @NTaylorMullen in
[#15907](https://github.com/google-gemini/gemini-cli/pull/15907)
- [Skills] Multi-scope skill enablement and shadowing fix by @NTaylorMullen in
[#15953](https://github.com/google-gemini/gemini-cli/pull/15953)
- policy: extract legacy policy from core tool scheduler to policy engine by
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
- docs: clarify F12 to open debug console by @jackwotherspoon in
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
- docs: Remove .md extension from internal links in architecture.md by
@medic-code in
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
- Add an experimental setting for extension config by @chrstnb in
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
- prefactor: add rootCommands as array so it can be used for policy parsing by
@abhipatel12 in
[#15902](https://github.com/google-gemini/gemini-cli/pull/15902)
- Enhance TestRig with process management and timeouts by @NTaylorMullen in
[#15908](https://github.com/google-gemini/gemini-cli/pull/15908)
- Update troubleshooting doc for UNABLE_TO_GET_ISSUER_CERT_LOCALLY by @sehoon38
in [#16069](https://github.com/google-gemini/gemini-cli/pull/16069)
- Add keytar to dependencies by @chrstnb in
[#15928](https://github.com/google-gemini/gemini-cli/pull/15928)
- Simplify extension settings command by @chrstnb in
[#16001](https://github.com/google-gemini/gemini-cli/pull/16001)
- feat(admin): implement extensions disabled by @skeshive in
[#16024](https://github.com/google-gemini/gemini-cli/pull/16024)
- Core data structure updates for Rewind functionality by @Adib234 in
[#15714](https://github.com/google-gemini/gemini-cli/pull/15714)
- feat(hooks): simplify hook firing with HookSystem wrapper methods by @ved015
in [#15982](https://github.com/google-gemini/gemini-cli/pull/15982)
- Add exp.gws_experiment field to LogEventEntry by @gsquared94 in
[#16062](https://github.com/google-gemini/gemini-cli/pull/16062)
- Revert "feat(admin): implement extensions disabled" by @chrstnb in
[#16082](https://github.com/google-gemini/gemini-cli/pull/16082)
- feat(core): Decouple enabling hooks UI from subsystem. by @joshualitt in
[#16074](https://github.com/google-gemini/gemini-cli/pull/16074)
- docs: add docs for hooks + extensions by @abhipatel12 in
[#16073](https://github.com/google-gemini/gemini-cli/pull/16073)
- feat(core): Preliminary changes for subagent model routing. by @joshualitt in
[#16035](https://github.com/google-gemini/gemini-cli/pull/16035)
- Optimize CI workflow: Parallelize jobs and cache linters by @NTaylorMullen in
[#16054](https://github.com/google-gemini/gemini-cli/pull/16054)
- Add option to fallback for capacity errors in ProQuotaDi… by @sehoon38 in
[#16050](https://github.com/google-gemini/gemini-cli/pull/16050)
- feat: add confirmation details support + jsonrpc vs http rest support by
@adamfweidman in
[#16079](https://github.com/google-gemini/gemini-cli/pull/16079)
- fix(workflows): fix and limit labels for pr-triage.sh script by @jacob314 in
[#16096](https://github.com/google-gemini/gemini-cli/pull/16096)
- Fix and rename introspection agent -> cli help agent by @scidomino in
[#16097](https://github.com/google-gemini/gemini-cli/pull/16097)
- Docs: Changelogs update 20260105 by @jkcinouye in
[#15937](https://github.com/google-gemini/gemini-cli/pull/15937)
- enable cli_help agent by default by @scidomino in
[#16100](https://github.com/google-gemini/gemini-cli/pull/16100)
- Optimize json-output tests with mock responses by @NTaylorMullen in
[#16102](https://github.com/google-gemini/gemini-cli/pull/16102)
- Fix CI for forks by @scidomino in
[#16113](https://github.com/google-gemini/gemini-cli/pull/16113)
- Reduce nags about PRs that reference issues but don't fix them. by @jacob314
in [#16112](https://github.com/google-gemini/gemini-cli/pull/16112)
- feat(cli): add filepath autosuggestion after slash commands by @jasmeetsb in
[#14738](https://github.com/google-gemini/gemini-cli/pull/14738)
- Add upgrade option for paid users by @cayden-google in
[#15978](https://github.com/google-gemini/gemini-cli/pull/15978)
- [Skills] UX Polishing: Transparent feedback and CLI refinements by
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
- remove unnecessary \x7f key bindings by @scidomino in
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
- docs(skills): use body-file in pr-creator skill for better reliability by
@abhipatel12 in
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
@NTaylorMullen in
[#15954](https://github.com/google-gemini/gemini-cli/pull/15954)
- Polish: Move 'Failed to load skills' warning to debug logs by @NTaylorMullen
in [#16142](https://github.com/google-gemini/gemini-cli/pull/16142)
- feat(cli): export chat history in /bug and prefill GitHub issue by
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
- 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
[#16115](https://github.com/google-gemini/gemini-cli/pull/16115)
- bug(core): fix issue with overrides to bases. by @joshualitt in
[#15255](https://github.com/google-gemini/gemini-cli/pull/15255)
- enableInteractiveShell for external tooling relying on a2a server by
@DavidAPierce in
[#16080](https://github.com/google-gemini/gemini-cli/pull/16080)
- Reapply "feat(admin): implement extensions disabled" (#16082) by @skeshive in
[#16109](https://github.com/google-gemini/gemini-cli/pull/16109)
- bug(core): Fix spewie getter in hookTranslator.ts by @joshualitt in
[#16108](https://github.com/google-gemini/gemini-cli/pull/16108)
- feat(hooks): add mcp_context to BeforeTool and AfterTool hook inputs by @vrv
in [#15656](https://github.com/google-gemini/gemini-cli/pull/15656)
- Add extension linking capabilities in cli by @kevinjwang1 in
[#16040](https://github.com/google-gemini/gemini-cli/pull/16040)
- Update the page's title to be consistent and show in site. by @kschaab in
[#16174](https://github.com/google-gemini/gemini-cli/pull/16174)
- docs: correct typo in bufferFastReturn JSDoc ("accomodate" → "accommodate") by
@minglu7 in [#16056](https://github.com/google-gemini/gemini-cli/pull/16056)
- fix: typo in MCP servers settings description by @alphanota in
[#15929](https://github.com/google-gemini/gemini-cli/pull/15929)
- fix: yolo should auto allow redirection by @abhipatel12 in
[#16183](https://github.com/google-gemini/gemini-cli/pull/16183)
- fix(cli): disableYoloMode shouldn't enforce default approval mode against args
by @psinha40898 in
[#16155](https://github.com/google-gemini/gemini-cli/pull/16155)
- feat: add native Sublime Text support to IDE detection by @phreakocious in
[#16083](https://github.com/google-gemini/gemini-cli/pull/16083)
- refactor(core): extract ToolModificationHandler from scheduler by @abhipatel12
in [#16118](https://github.com/google-gemini/gemini-cli/pull/16118)
- Add support for Antigravity terminal in terminal setup utility by @raky291 in
[#16051](https://github.com/google-gemini/gemini-cli/pull/16051)
- feat(core): Wire up model routing to subagents. by @joshualitt in
[#16043](https://github.com/google-gemini/gemini-cli/pull/16043)
- feat(cli): add /agents slash command to list available agents by @adamfweidman
in [#16182](https://github.com/google-gemini/gemini-cli/pull/16182)
- docs(cli): fix includeDirectories nesting in configuration.md by @maru0804 in
[#15067](https://github.com/google-gemini/gemini-cli/pull/15067)
- feat: implement file system reversion utilities for rewind by @Adib234 in
[#15715](https://github.com/google-gemini/gemini-cli/pull/15715)
- Always enable redaction in GitHub actions. by @gundermanc in
[#16200](https://github.com/google-gemini/gemini-cli/pull/16200)
- fix: remove unsupported 'enabled' key from workflow config by @Han5991 in
[#15611](https://github.com/google-gemini/gemini-cli/pull/15611)
- docs: Remove redundant and duplicate documentation files by @liqzheng in
[#14699](https://github.com/google-gemini/gemini-cli/pull/14699)
- docs: shorten run command and use published version by @dsherret in
[#16172](https://github.com/google-gemini/gemini-cli/pull/16172)
- test(command-registry): increase initialization test timeout by @wszqkzqk in
[#15979](https://github.com/google-gemini/gemini-cli/pull/15979)
- Ensure TERM is set to xterm-256color by @falouu in
[#15828](https://github.com/google-gemini/gemini-cli/pull/15828)
- The telemetry.js script should handle paths that contain spaces by @JohnJAS in
[#12078](https://github.com/google-gemini/gemini-cli/pull/12078)
- ci: guard links workflow from running on forks by @wtanaka in
[#15461](https://github.com/google-gemini/gemini-cli/pull/15461)
- ci: guard nightly release workflow from running on forks by @wtanaka in
[#15463](https://github.com/google-gemini/gemini-cli/pull/15463)
- Support @ suggestions for subagenets by @sehoon38 in
[#16201](https://github.com/google-gemini/gemini-cli/pull/16201)
- feat(hooks): Support explicit stop and block execution control in model hooks
by @SandyTao520 in
[#15947](https://github.com/google-gemini/gemini-cli/pull/15947)
- Refine Gemini 3 system instructions to reduce model verbosity by
[#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
[#16139](https://github.com/google-gemini/gemini-cli/pull/16139)
- chore: clean up unused models and use consts by @sehoon38 in
[#16246](https://github.com/google-gemini/gemini-cli/pull/16246)
- Always enable bracketed paste by @scidomino in
[#16179](https://github.com/google-gemini/gemini-cli/pull/16179)
- refactor: migrate clearCommand hook calls to HookSystem by @ved015 in
[#16157](https://github.com/google-gemini/gemini-cli/pull/16157)
- refactor: migrate app containter hook calls to hook system by @ishaanxgupta in
[#16161](https://github.com/google-gemini/gemini-cli/pull/16161)
- Show settings source in extensions lists by @chrstnb in
[#16207](https://github.com/google-gemini/gemini-cli/pull/16207)
- feat(skills): add pr-creator skill and enable skills by @NTaylorMullen in
[#16232](https://github.com/google-gemini/gemini-cli/pull/16232)
- fix: handle Shift+Space in Kitty keyboard protocol terminals by @tt-a1i in
[#15767](https://github.com/google-gemini/gemini-cli/pull/15767)
- feat(core, ui): Add /agents refresh command. by @joshualitt in
[#16204](https://github.com/google-gemini/gemini-cli/pull/16204)
- feat(core): add local experiments override via GEMINI_EXP by @kevin-ramdass in
[#16181](https://github.com/google-gemini/gemini-cli/pull/16181)
- feat(ui): reduce home directory warning noise and add opt-out setting by
[#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
[#16229](https://github.com/google-gemini/gemini-cli/pull/16229)
- refactor: migrate chatCompressionService to use HookSystem by @ved015 in
[#16259](https://github.com/google-gemini/gemini-cli/pull/16259)
- fix: properly use systemMessage for hooks in UI by @jackwotherspoon in
[#16250](https://github.com/google-gemini/gemini-cli/pull/16250)
- Infer modifyOtherKeys support by @scidomino in
[#16270](https://github.com/google-gemini/gemini-cli/pull/16270)
- feat(core): Cache ignore instances for performance by @EricRahm in
[#16185](https://github.com/google-gemini/gemini-cli/pull/16185)
- feat: apply remote admin settings (no-op) by @skeshive in
[#16106](https://github.com/google-gemini/gemini-cli/pull/16106)
- Autogenerate docs/cli/settings.md docs/getting-started/configuration.md was
already autogenerated but settings.md was not. by @jacob314 in
[#14408](https://github.com/google-gemini/gemini-cli/pull/14408)
- refactor(config): remove legacy V1 settings migration logic by @galz10 in
[#16252](https://github.com/google-gemini/gemini-cli/pull/16252)
- Fix an issue where the agent stops prematurely by @gundermanc in
[#16269](https://github.com/google-gemini/gemini-cli/pull/16269)
- Update system prompt to prefer non-interactive commands by @NTaylorMullen in
[#16117](https://github.com/google-gemini/gemini-cli/pull/16117)
- Update ink version to 6.4.7 by @jacob314 in
[#16284](https://github.com/google-gemini/gemini-cli/pull/16284)
- Support for Built-in Agent Skills by @NTaylorMullen in
[#16045](https://github.com/google-gemini/gemini-cli/pull/16045)
- fix(skills): remove "Restart required" message from non-interactive commands
by @NTaylorMullen in
[#16307](https://github.com/google-gemini/gemini-cli/pull/16307)
- remove unused sessionHookTriggers and exports by @ved015 in
[#16324](https://github.com/google-gemini/gemini-cli/pull/16324)
- Triage action cleanup by @bdmorgan in
[#16319](https://github.com/google-gemini/gemini-cli/pull/16319)
- fix: Add event-driven trigger to issue triage workflow by @bdmorgan in
[#16334](https://github.com/google-gemini/gemini-cli/pull/16334)
- fix(workflows): resolve triage workflow failures and actionlint errors by
@bdmorgan in [#16338](https://github.com/google-gemini/gemini-cli/pull/16338)
- docs: add note about experimental hooks by @abhipatel12 in
[#16337](https://github.com/google-gemini/gemini-cli/pull/16337)
- feat(cli): implement passive activity logger for session analysis by
[#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
[#15829](https://github.com/google-gemini/gemini-cli/pull/15829)
- feat(cli): add /chat debug command for nightly builds by @abhipatel12 in
[#16339](https://github.com/google-gemini/gemini-cli/pull/16339)
- style: format pr-creator skill by @NTaylorMullen in
[#16381](https://github.com/google-gemini/gemini-cli/pull/16381)
- feat(cli): Hooks enable-all/disable-all feature with dynamic status by
@AbdulTawabJuly in
[#15552](https://github.com/google-gemini/gemini-cli/pull/15552)
- fix(core): ensure silent local subagent delegation while allowing remote
confirmation by @adamfweidman in
[#16395](https://github.com/google-gemini/gemini-cli/pull/16395)
- Markdown w/ Frontmatter Agent Parser by @sehoon38 in
[#16094](https://github.com/google-gemini/gemini-cli/pull/16094)
- Fix crash on unicode character by @chrstnb in
[#16420](https://github.com/google-gemini/gemini-cli/pull/16420)
- Attempt to resolve OOM w/ useMemo on history items by @chrstnb in
[#16424](https://github.com/google-gemini/gemini-cli/pull/16424)
- fix(core): ensure sub-agent schema and prompt refresh during runtime by
@adamfweidman in
[#16409](https://github.com/google-gemini/gemini-cli/pull/16409)
- Update extension examples by @chrstnb in
[#16274](https://github.com/google-gemini/gemini-cli/pull/16274)
- revert the change that was recently added from a fix by @sehoon38 in
[#16390](https://github.com/google-gemini/gemini-cli/pull/16390)
- Add other hook wrapper methods to hooksystem by @ved015 in
[#16361](https://github.com/google-gemini/gemini-cli/pull/16361)
- feat: introduce useRewindLogic hook for conversation history navigation by
@Adib234 in [#15716](https://github.com/google-gemini/gemini-cli/pull/15716)
- docs: Fix formatting issue in memport documentation by @wanglc02 in
[#14774](https://github.com/google-gemini/gemini-cli/pull/14774)
- fix(policy): enhance shell command safety and parsing by @allenhutchison in
[#15034](https://github.com/google-gemini/gemini-cli/pull/15034)
- fix(core): avoid 'activate_skill' re-registration warning by @NTaylorMullen in
[#16398](https://github.com/google-gemini/gemini-cli/pull/16398)
- perf(workflows): optimize PR triage script for faster execution by @bdmorgan
in [#16355](https://github.com/google-gemini/gemini-cli/pull/16355)
- feat(admin): prompt user to restart the CLI if they change auth to oauth
mid-session or don't have auth type selected at start of session by @skeshive
in [#16426](https://github.com/google-gemini/gemini-cli/pull/16426)
- Update cli-help agent's system prompt in sub-agents section by @sehoon38 in
[#16441](https://github.com/google-gemini/gemini-cli/pull/16441)
- Revert "Update extension examples" by @chrstnb in
[#16442](https://github.com/google-gemini/gemini-cli/pull/16442)
- Fix: add back fastreturn support by @scidomino in
[#16440](https://github.com/google-gemini/gemini-cli/pull/16440)
- feat(a2a): Introduce /memory command for a2a server by @cocosheng-g in
[#14456](https://github.com/google-gemini/gemini-cli/pull/14456)
- docs: fix broken internal link by using relative path by @Gong-Mi in
[#15371](https://github.com/google-gemini/gemini-cli/pull/15371)
- migrate yolo/auto-edit keybindings by @scidomino in
[#16457](https://github.com/google-gemini/gemini-cli/pull/16457)
- feat(cli): add install and uninstall commands for skills by @NTaylorMullen in
[#16377](https://github.com/google-gemini/gemini-cli/pull/16377)
- feat(ui): use Tab to switch focus between shell and input by @jacob314 in
[#14332](https://github.com/google-gemini/gemini-cli/pull/14332)
- feat(core): support shipping built-in skills with the CLI by @NTaylorMullen in
[#16300](https://github.com/google-gemini/gemini-cli/pull/16300)
- Collect hardware details telemetry. by @gundermanc in
[#16119](https://github.com/google-gemini/gemini-cli/pull/16119)
- feat(agents): improve UI feedback and parser reliability by @NTaylorMullen in
[#16459](https://github.com/google-gemini/gemini-cli/pull/16459)
- Migrate keybindings by @scidomino in
[#16460](https://github.com/google-gemini/gemini-cli/pull/16460)
- feat(cli): cleanup activity logs alongside session files by @SandyTao520 in
[#16399](https://github.com/google-gemini/gemini-cli/pull/16399)
- feat(cli): implement dynamic terminal tab titles for CLI status by
@NTaylorMullen in
[#16378](https://github.com/google-gemini/gemini-cli/pull/16378)
- feat(core): add disableLLMCorrection setting to skip auto-correction in edit
tools by @SandyTao520 in
[#16000](https://github.com/google-gemini/gemini-cli/pull/16000)
- fix: Set both tab and window title instead of just window title by
@NTaylorMullen in
[#16464](https://github.com/google-gemini/gemini-cli/pull/16464)
- fix(policy): ensure MCP policies match unqualified names in non-interactive
mode by @NTaylorMullen in
[#16490](https://github.com/google-gemini/gemini-cli/pull/16490)
- fix(cli): refine 'Action Required' indicator and focus hints by @NTaylorMullen
in [#16497](https://github.com/google-gemini/gemini-cli/pull/16497)
- Refactor beforeAgent and afterAgent hookEvents to follow desired output by
@ved015 in [#16495](https://github.com/google-gemini/gemini-cli/pull/16495)
- feat(agents): clarify mandatory YAML frontmatter for sub-agents by
@NTaylorMullen in
[#16515](https://github.com/google-gemini/gemini-cli/pull/16515)
- docs(telemetry): add Google Cloud Monitoring dashboard documentation by @jerop
in [#16520](https://github.com/google-gemini/gemini-cli/pull/16520)
- Implement support for subagents as extensions. by @gundermanc in
[#16473](https://github.com/google-gemini/gemini-cli/pull/16473)
- refactor: make baseTimestamp optional in addItem and remove redundant calls by
@sehoon38 in [#16471](https://github.com/google-gemini/gemini-cli/pull/16471)
- Improve key binding names and descriptions by @scidomino in
[#16529](https://github.com/google-gemini/gemini-cli/pull/16529)
- feat(core, cli): Add support for agents in settings.json. by @joshualitt in
[#16433](https://github.com/google-gemini/gemini-cli/pull/16433)
- fix(cli): fix 'gemini skills install' unknown argument error by @NTaylorMullen
in [#16537](https://github.com/google-gemini/gemini-cli/pull/16537)
- chore(ui): optimize AgentsStatus layout with dense list style and group
separation by @adamfweidman in
[#16545](https://github.com/google-gemini/gemini-cli/pull/16545)
- fix(cli): allow @ file selector on slash command lines by @galz10 in
[#16370](https://github.com/google-gemini/gemini-cli/pull/16370)
- fix(ui): resolve sticky header regression in tool messages by @jacob314 in
[#16514](https://github.com/google-gemini/gemini-cli/pull/16514)
- feat(core): Align internal agent settings with configs exposed through
settings.json by @joshualitt in
[#16458](https://github.com/google-gemini/gemini-cli/pull/16458)
- fix(cli): copy uses OSC52 only in SSH/WSL by @assagman in
[#16554](https://github.com/google-gemini/gemini-cli/pull/16554)
- docs(skills): clarify skill directory structure and file location by
@NTaylorMullen in
[#16532](https://github.com/google-gemini/gemini-cli/pull/16532)
- Fix: make ctrl+x use preferred editor by @scidomino in
[#16556](https://github.com/google-gemini/gemini-cli/pull/16556)
- fix(core): Resolve race condition in tool response reporting by @abhipatel12
in [#16557](https://github.com/google-gemini/gemini-cli/pull/16557)
- feat(ui): highlight persist mode status in ModelDialog by @sehoon38 in
[#16483](https://github.com/google-gemini/gemini-cli/pull/16483)
- refactor: clean up A2A task output for users and LLMs by @adamfweidman in
[#16561](https://github.com/google-gemini/gemini-cli/pull/16561)
- feat(core/ui): enhance retry mechanism and UX by @sehoon38 in
[#16489](https://github.com/google-gemini/gemini-cli/pull/16489)
- Modernize MaxSizedBox to use and ResizeObservers by @jacob314 in
[#16565](https://github.com/google-gemini/gemini-cli/pull/16565)
- Behavioral evals framework. by @gundermanc in
[#16047](https://github.com/google-gemini/gemini-cli/pull/16047)
- Aggregate test results. by @gundermanc in
[#16581](https://github.com/google-gemini/gemini-cli/pull/16581)
- feat(admin): support admin-enforced settings for Agent Skills by
@NTaylorMullen in
[#16406](https://github.com/google-gemini/gemini-cli/pull/16406)
- fix(patch): cherry-pick cfdc4cf to release/v0.25.0-preview.0-pr-16759 to patch
version v0.25.0-preview.0 and create version 0.25.0-preview.1 by
@gemini-cli-robot in
[#16866](https://github.com/google-gemini/gemini-cli/pull/16866)
- Patch #16730 into v0.25.0 preview by @chrstnb in
[#16882](https://github.com/google-gemini/gemini-cli/pull/16882)
- fix(patch): cherry-pick 3b55581 to release/v0.25.0-preview.2-pr-16506 to patch
version v0.25.0-preview.2 and create version 0.25.0-preview.3 by
@gemini-cli-robot in
[#17098](https://github.com/google-gemini/gemini-cli/pull/17098)
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
- fix(core): update telemetry token count after session resume by @psinha40898
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
- Demote the subagent test to nightly by @gundermanc in
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
- feat: Add flash lite utility fallback chain by @adamfweidman in
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
- feat(core): Add initial eval for generalist agent. by @joshualitt in
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
- fix(core): resolve auto model in default strategy by @sehoon38 in
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
- docs: update project context and pr-creator workflow by @NTaylorMullen in
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
- fix(cli): send gemini-cli version as mcp client version by @dsp in
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
- Remove missing sidebar item by @chrstnb in
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
- feat(core): Ensure all properties in hooks object are event names. by
@joshualitt in
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
- fix(cli): fix newline support broken in previous PR by @scidomino in
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
by @gsquared94 in
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
- Avoid spurious warnings about unexpected renders triggered by appEvents and
coreEvents. by @jacob314 in
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
- fix(cli): resolve home/end keybinding conflict by @scidomino in
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
- fix(cli): display 'http' type on mcp list by @pamanta in
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
- fix bad fallback logic external editor logic by @scidomino in
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
- Fix bug where System scopes weren't migrated. by @jacob314 in
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
- Fix mcp tool lookup in tool registry by @werdnum in
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.24.5...v0.25.0
https://github.com/google-gemini/gemini-cli/compare/v0.25.2...v0.26.0
+415 -310
View File
@@ -1,6 +1,6 @@
# Preview release: Release v0.26.0-preview.0
# Preview release: Release v0.27.0-preview.0
Released: January 21, 2026
Released: January 27, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -13,320 +13,425 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Skills and Agents:** Improvements to the `activate_skill` tool and skill
management. Experimental Agent Skills support.
- **UI/UX:** Addition of a Rewind Confirmation dialog and Viewer component.
- **Extensions:** Experimental setting for extension configuration.
- **Bug Fixes and Stability:** PDF token estimation fix and improvements to
scheduled issue triage.
- **Event-Driven Architecture:** The tool execution scheduler is now
event-driven, improving performance and reliability.
- **System Prompt Override:** Now supports dynamic variable substitution.
- **Rewind Command:** The `/rewind` command has been implemented.
- **Linux Clipboard:** Image pasting capabilities for Wayland and X11 on Linux.
## What's Changed
- fix: PDF token estimation
([#16494](https://github.com/google-gemini/gemini-cli/pull/16494)) by
@korade-krushna in
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
- 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
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
- docs: clarify F12 to open debug console by @jackwotherspoon in
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
- docs: Remove .md extension from internal links in architecture.md by
@medic-code in
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
- Add an experimental setting for extension config by @chrstnb in
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
- prefactor: add rootCommands as array so it can be used for policy parsing by
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
- Remove other rewind reference in docs by @chrstnb in
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
- feat(skills): add code-reviewer skill by @sehoon38 in
[#17187](httpshttps://github.com/google-gemini/gemini-cli/pull/17187)
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
- feat(plan): refactor TestRig and eval helper to support configurable approval
modes by @jerop in
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
- feat(workflows): support recursive workstream labeling and new IDs by
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
- 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
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
- remove unnecessary \x7f key bindings by @scidomino in
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
- docs(skills): use body-file in pr-creator skill for better reliability by
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
- fix(core): update token count and telemetry on /chat resume history load by
@psinha40898 in
[#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
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
@NTaylorMullen in
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
- 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](https://github.com/google-gemini/gemini-cli/pull/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
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
- feat(hooks): enable hooks system by default by @abhipatel12 in
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
- feat(core): Enable AgentRegistry to track all discovered subagents by
@SandyTao520 in
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
- fix(core): update telemetry token count after session resume by @psinha40898
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
- Demote the subagent test to nightly by @gundermanc in
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
- feat: Add flash lite utility fallback chain by @adamfweidman in
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
- feat(core): Add initial eval for generalist agent. by @joshualitt in
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
- fix(core): resolve auto model in default strategy by @sehoon38 in
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
- docs: update project context and pr-creator workflow by @NTaylorMullen in
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
- fix(cli): send gemini-cli version as mcp client version by @dsp in
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
- Remove missing sidebar item by @chrstnb in
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
- feat(core): Ensure all properties in hooks object are event names. by
@joshualitt in
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
- fix(cli): fix newline support broken in previous PR by @scidomino in
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
by @gsquared94 in
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
- Avoid spurious warnings about unexpected renders triggered by appEvents and
coreEvents. by @jacob314 in
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
- fix(cli): resolve home/end keybinding conflict by @scidomino in
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
- fix(cli): display 'http' type on mcp list by @pamanta in
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
- fix bad fallback logic external editor logic by @scidomino in
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
- Fix bug where System scopes weren't migrated. by @jacob314 in
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
- Fix mcp tool lookup in tool registry by @werdnum in
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by
@jackwotherspoon in
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
- Enable the ability to queue specific nightly eval tests by @gundermanc in
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
- docs(hooks): comprehensive update of hook documentation and specs by
@abhipatel12 in
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
- refactor: improve large text paste placeholder by @jacob314 in
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
- feat: implement /rewind command by @Adib234 in
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
- Feature/jetbrains ide detection by @SoLoHiC in
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
- docs: update typo in mcp-server.md file by @schifferl in
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
- Sanitize command names and descriptions by @ehedlund in
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
- fix(auth): don't crash when initial auth fails by @skeshive in
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
- feat: add AskUser tool schema by @jackwotherspoon in
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
now handles the caching internally by @jacob314 in
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
- ci: allow failure in evals-nightly run step by @gundermanc in
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
- feat(cli): Add state management and plumbing for agent configuration dialog by
@SandyTao520 in
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
- bug: fix ide-client connection to ide-companion when inside docker via
ssh/devcontainer by @kapsner in
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
- Emit correct newline type return by @scidomino in
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
- New skill: docs-writer by @g-samroberts in
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
@spencer426 in
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
- 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**:
https://github.com/google-gemini/gemini-cli/compare/v0.25.0-preview.4...v0.26.0-preview.0
https://github.com/google-gemini/gemini-cli/compare/v0.26.0-preview.5...v0.27.0-preview.0
+101
View File
@@ -0,0 +1,101 @@
# CLI cheatsheet
This page provides a reference for commonly used Gemini CLI commands, options,
and parameters.
## CLI commands
| Command | Description | Example |
| ---------------------------------- | ---------------------------------- | --------------------------------------------------- |
| `gemini` | Start interactive REPL | `gemini` |
| `gemini "query"` | Query non-interactively, then exit | `gemini "explain this project"` |
| `gemini -p "query"` | Query via SDK, then exit | `gemini -p "explain this function"` |
| `cat file \| gemini -p "query"` | Process piped content | `cat logs.txt \| gemini -p "explain"` |
| `gemini -i "query"` | Execute and continue interactively | `gemini -i "What is the purpose of this project?"` |
| `gemini -r "latest"` | Continue most recent session | `gemini -r "latest"` |
| `gemini -r "latest" "query"` | Continue session with a new prompt | `gemini -r "latest" "Check for type errors"` |
| `gemini -r "<session-id>" "query"` | Resume session by ID | `gemini -r "abc123" "Finish this PR"` |
| `gemini update` | Update to latest version | `gemini update` |
| `gemini extensions` | Manage extensions | See [Extensions Management](#extensions-management) |
| `gemini mcp` | Configure MCP servers | See [MCP Server Management](#mcp-server-management) |
### Positional arguments
| Argument | Type | Description |
| -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
| `query` | string (variadic) | Positional prompt. Defaults to one-shot mode. Use `-i/--prompt-interactive` to execute and continue interactively. |
## CLI Options
| Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
| `--version` | `-v` | - | - | Show CLI version number and exit |
| `--help` | `-h` | - | - | Show help information |
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
| `--allowed-tools` | - | array | - | Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
## Model selection
The `--model` (or `-m`) flag allows you to specify which Gemini model to use.
You can use either model aliases (user-friendly names) or concrete model names.
### Model aliases
These are convenient shortcuts that map to specific models:
| Alias | Resolves To | Description |
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `auto` | `gemini-2.5-pro` or `gemini-3-pro-preview` | **Default.** Resolves to the preview model if preview features are enabled, otherwise resolves to the standard pro model. |
| `pro` | `gemini-2.5-pro` or `gemini-3-pro-preview` | For complex reasoning tasks. Uses preview model if enabled. |
| `flash` | `gemini-2.5-flash` | Fast, balanced model for most tasks. |
| `flash-lite` | `gemini-2.5-flash-lite` | Fastest model for simple tasks. |
## Extensions management
| Command | Description | Example |
| -------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ |
| `gemini extensions install <source>` | Install extension from Git URL or local path | `gemini extensions install https://github.com/user/my-extension` |
| `gemini extensions install <source> --ref <ref>` | Install from specific branch/tag/commit | `gemini extensions install https://github.com/user/my-extension --ref develop` |
| `gemini extensions install <source> --auto-update` | Install with auto-update enabled | `gemini extensions install https://github.com/user/my-extension --auto-update` |
| `gemini extensions uninstall <name>` | Uninstall one or more extensions | `gemini extensions uninstall my-extension` |
| `gemini extensions list` | List all installed extensions | `gemini extensions list` |
| `gemini extensions update <name>` | Update a specific extension | `gemini extensions update my-extension` |
| `gemini extensions update --all` | Update all extensions | `gemini extensions update --all` |
| `gemini extensions enable <name>` | Enable an extension | `gemini extensions enable my-extension` |
| `gemini extensions disable <name>` | Disable an extension | `gemini extensions disable my-extension` |
| `gemini extensions link <path>` | Link local extension for development | `gemini extensions link /path/to/extension` |
| `gemini extensions new <path>` | Create new extension from template | `gemini extensions new ./my-extension` |
| `gemini extensions validate <path>` | Validate extension structure | `gemini extensions validate ./my-extension` |
See [Extensions Documentation](../extensions/index.md) for more details.
## MCP server management
| Command | Description | Example |
| ------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `gemini mcp add <name> <command>` | Add stdio-based MCP server | `gemini mcp add github npx -y @modelcontextprotocol/server-github` |
| `gemini mcp add <name> <url> --transport http` | Add HTTP-based MCP server | `gemini mcp add api-server http://localhost:3000 --transport http` |
| `gemini mcp add <name> <command> --env KEY=value` | Add with environment variables | `gemini mcp add slack node server.js --env SLACK_TOKEN=xoxb-xxx` |
| `gemini mcp add <name> <command> --scope user` | Add with user scope | `gemini mcp add db node db-server.js --scope user` |
| `gemini mcp add <name> <command> --include-tools tool1,tool2` | Add with specific tools | `gemini mcp add github npx -y @modelcontextprotocol/server-github --include-tools list_repos,get_pr` |
| `gemini mcp remove <name>` | Remove an MCP server | `gemini mcp remove github` |
| `gemini mcp list` | List all configured MCP servers | `gemini mcp list` |
See [MCP Server Integration](../tools/mcp-server.md) for more details.
+139 -84
View File
@@ -10,6 +10,14 @@ Slash commands provide meta-level control over the CLI itself.
### Built-in Commands
- **`/about`**
- **Description:** Show version info. Please share this information when
filing issues.
- **`/auth`**
- **Description:** Open a dialog that lets you change the authentication
method.
- **`/bug`**
- **Description:** File an issue about Gemini CLI. By default, the issue is
filed within the GitHub repository for Gemini CLI. The string you enter
@@ -22,10 +30,21 @@ Slash commands provide meta-level control over the CLI itself.
conversation state interactively, or resuming a previous state from a later
session.
- **Sub-commands:**
- **`save`**
- **`delete <tag>`**
- **Description:** Deletes a saved conversation checkpoint.
- **`list`**
- **Description:** Lists available tags for chat state resumption.
- **Note:** This command only lists chats saved within the current
project. Because chat history is project-scoped, chats saved in other
project directories will not be displayed.
- **`resume <tag>`**
- **Description:** Resumes a conversation from a previous save.
- **Note:** You can only resume chats that were saved within the current
project. To resume a chat from a different project, you must run the
Gemini CLI from that project's directory.
- **`save <tag>`**
- **Description:** Saves the current conversation history. You must add a
`<tag>` for identifying the conversation state.
- **Usage:** `/chat save <tag>`
- **Details on checkpoint location:** The default locations for saved chat
checkpoints are:
- Linux/macOS: `~/.gemini/tmp/<project_hash>/`
@@ -37,25 +56,11 @@ Slash commands provide meta-level control over the CLI itself.
conversation states. For automatic checkpoints created before file
modifications, see the
[Checkpointing documentation](../cli/checkpointing.md).
- **`resume`**
- **Description:** Resumes a conversation from a previous save.
- **Usage:** `/chat resume <tag>`
- **Note:** You can only resume chats that were saved within the current
project. To resume a chat from a different project, you must run the
Gemini CLI from that project's directory.
- **`list`**
- **Description:** Lists available tags for chat state resumption.
- **Note:** This command only lists chats saved within the current
project. Because chat history is project-scoped, chats saved in other
project directories will not be displayed.
- **`delete`**
- **Description:** Deletes a saved conversation checkpoint.
- **Usage:** `/chat delete <tag>`
- **`share`**
- **`share [filename]`**
- **Description** Writes the current conversation to a provided Markdown
or JSON file.
- **Usage** `/chat share file.md` or `/chat share file.json`. If no
filename is provided, then the CLI will generate one.
or JSON file. If no filename is provided, then the CLI will generate
one.
- **Usage** `/chat share file.md` or `/chat share file.json`.
- **`/clear`**
- **Description:** Clear the terminal screen, including the visible session
@@ -98,6 +103,9 @@ Slash commands provide meta-level control over the CLI itself.
`--include-directories`.
- **Usage:** `/directory show`
- **`/docs`**
- **Description:** Open the Gemini CLI documentation in your browser.
- **`/editor`**
- **Description:** Open a dialog for selecting supported editors.
@@ -109,30 +117,65 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Display help information about Gemini CLI, including
available commands and their usage.
- **`/hooks`**
- **Description:** Manage hooks, which allow you to intercept and customize
Gemini CLI behavior at specific lifecycle events.
- **Sub-commands:**
- **`disable-all`**:
- **Description:** Disable all enabled hooks.
- **`disable <hook-name>`**:
- **Description:** Disable a hook by name.
- **`enable-all`**:
- **Description:** Enable all disabled hooks.
- **`enable <hook-name>`**:
- **Description:** Enable a hook by name.
- **`list`** (or `show`, `panel`):
- **Description:** Display all registered hooks with their status.
- **`/ide`**
- **Description:** Manage IDE integration.
- **Sub-commands:**
- **`disable`**:
- **Description:** Disable IDE integration.
- **`enable`**:
- **Description:** Enable IDE integration.
- **`install`**:
- **Description:** Install required IDE companion.
- **`status`**:
- **Description:** Check status of IDE integration.
- **`/init`**
- **Description:** To help users easily create a `GEMINI.md` file, this
command analyzes the current directory and generates a tailored context
file, making it simpler for them to provide project-specific instructions to
the Gemini agent.
- **`/introspect`**
- **Description:** Provide debugging information about the current Gemini CLI
session, including the state of loaded sub-agents and active hooks. This
command is primarily for advanced users and developers.
- **`/mcp`**
- **Description:** Manage configured Model Context Protocol (MCP) servers.
- **Sub-commands:**
- **`list`** or **`ls`**:
- **Description:** List configured MCP servers and tools. This is the
default action if no subcommand is specified.
- **`desc`**
- **Description:** List configured MCP servers and tools with
descriptions.
- **`schema`**:
- **Description:** List configured MCP servers and tools with descriptions
and schemas.
- **`auth`**:
- **Description:** Authenticate with an OAuth-enabled MCP server.
- **Usage:** `/mcp auth <server-name>`
- **Details:** If `<server-name>` is provided, it initiates the OAuth flow
for that server. If no server name is provided, it lists all configured
servers that support OAuth authentication.
- **`desc`**
- **Description:** List configured MCP servers and tools with
descriptions.
- **`list`** or **`ls`**:
- **Description:** List configured MCP servers and tools. This is the
default action if no subcommand is specified.
- **`refresh`**:
- **Description:** Restarts all MCP servers and re-discovers their
available tools.
- [**`/model`**](./model.md)
- **Description:** Opens a dialog to choose your Gemini model.
- **`schema`**:
- **Description:** List configured MCP servers and tools with descriptions
and schemas.
- **`/memory`**
- **Description:** Manage the AI's instructional context (hierarchical memory
@@ -141,23 +184,40 @@ Slash commands provide meta-level control over the CLI itself.
- **`add`**:
- **Description:** Adds the following text to the AI's memory. Usage:
`/memory add <text to remember>`
- **`show`**:
- **Description:** Display the full, concatenated content of the current
hierarchical memory that has been loaded from all `GEMINI.md` files.
This lets you inspect the instructional context being provided to the
Gemini model.
- **`list`**:
- **Description:** Lists the paths of the GEMINI.md files in use for
hierarchical memory.
- **`refresh`**:
- **Description:** Reload the hierarchical instructional memory from all
`GEMINI.md` files found in the configured locations (global,
project/ancestors, and sub-directories). This command updates the model
with the latest `GEMINI.md` content.
- **`list`**:
- **Description:** Lists the paths of the GEMINI.md files in use for
hierarchical memory.
- **`show`**:
- **Description:** Display the full, concatenated content of the current
hierarchical memory that has been loaded from all `GEMINI.md` files.
This lets you inspect the instructional context being provided to the
Gemini model.
- **Note:** For more details on how `GEMINI.md` files contribute to
hierarchical memory, see the
[CLI Configuration documentation](../get-started/configuration.md).
- [**`/model`**](./model.md)
- **Description:** Opens a dialog to choose your Gemini model.
- **`/policies`**
- **Description:** Manage policies.
- **Sub-commands:**
- **`list`**:
- **Description:** List all active policies grouped by mode.
- **`/privacy`**
- **Description:** Display the Privacy Notice and allow users to select
whether they consent to the collection of their data for service improvement
purposes.
- **`/quit`** (or **`/exit`**)
- **Description:** Exit Gemini CLI.
- **`/restore`**
- **Description:** Restores the project files to the state they were in just
before a tool was executed. This is particularly useful for undoing file
@@ -168,18 +228,24 @@ Slash commands provide meta-level control over the CLI itself.
[settings](../get-started/configuration.md). See
[Checkpointing documentation](../cli/checkpointing.md) for more details.
- [**`/rewind`**](./rewind.md)
- **Description:** Navigates backward through the conversation history,
allowing you to review past interactions and potentially revert to a
previous state. This feature helps in managing complex or branched
conversations.
- **`/resume`**
- **Description:** Browse and resume previous conversation sessions. Opens an
interactive session browser where you can search, filter, and select from
automatically saved conversations.
- **Features:**
- **Session Browser:** Interactive interface showing all saved sessions with
timestamps, message counts, and first user message for context
- **Search:** Use `/` to search through conversation content across all
sessions
- **Sorting:** Sort sessions by date or message count
- **Management:** Delete unwanted sessions directly from the browser
- **Resume:** Select any session to resume and continue the conversation
- **Search:** Use `/` to search through conversation content across all
sessions
- **Session Browser:** Interactive interface showing all saved sessions with
timestamps, message counts, and first user message for context
- **Sorting:** Sort sessions by date or message count
- **Note:** All conversations are automatically saved as you chat - no manual
saving required. See [Session Management](../cli/session-management.md) for
complete details.
@@ -198,19 +264,26 @@ Slash commands provide meta-level control over the CLI itself.
modify them as desired. Changes to some settings are applied immediately,
while others require a restart.
- **`/shells`** (or **`/bashes`**)
- **Description:** Toggle the background shells view. This allows you to view
and manage long-running processes that you've sent to the background.
- **`/setup-github`**
- **Description:** Set up GitHub Actions to triage issues and review PRs with
Gemini.
- [**`/skills`**](./skills.md)
- **Description:** Manage Agent Skills, which provide on-demand expertise and
specialized workflows.
- **Sub-commands:**
- **`disable <name>`**:
- **Description:** Disable a specific skill by name.
- **Usage:** `/skills disable <name>`
- **`enable <name>`**:
- **Description:** Enable a specific skill by name.
- **Usage:** `/skills enable <name>`
- **`list`**:
- **Description:** List all discovered skills and their current status
(enabled/disabled).
- **`enable`**:
- **Description:** Enable a specific skill by name.
- **Usage:** `/skills enable <name>`
- **`disable`**:
- **Description:** Disable a specific skill by name.
- **Usage:** `/skills disable <name>`
- **`reload`**:
- **Description:** Refresh the list of discovered skills from all tiers
(workspace, user, and extensions).
@@ -222,18 +295,14 @@ Slash commands provide meta-level control over the CLI itself.
cached tokens are being used, which occurs with API key authentication but
not with OAuth authentication at this time.
- **`/terminal-setup`**
- **Description:** Configure terminal keybindings for multiline input (VS
Code, Cursor, Windsurf).
- [**`/theme`**](./themes.md)
- **Description:** Open a dialog that lets you change the visual theme of
Gemini CLI.
- **`/auth`**
- **Description:** Open a dialog that lets you change the authentication
method.
- **`/about`**
- **Description:** Show version info. Please share this information when
filing issues.
- [**`/tools`**](../tools/index.md)
- **Description:** Display a list of tools that are currently available within
Gemini CLI.
@@ -245,37 +314,23 @@ Slash commands provide meta-level control over the CLI itself.
- **`nodesc`** or **`nodescriptions`**:
- **Description:** Hide tool descriptions, showing only the tool names.
- **`/privacy`**
- **Description:** Display the Privacy Notice and allow users to select
whether they consent to the collection of their data for service improvement
purposes.
- **`/quit`** (or **`/exit`**)
- **Description:** Exit Gemini CLI.
- **`/vim`**
- **Description:** Toggle vim mode on or off. When vim mode is enabled, the
input area supports vim-style navigation and editing commands in both NORMAL
and INSERT modes.
- **Features:**
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
- **NORMAL mode:** Navigate with `h`, `j`, `k`, `l`; jump by words with `w`,
`b`, `e`; go to line start/end with `0`, `$`, `^`; go to specific lines
with `G` (or `gg` for first line)
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
- **Repeat last command:** Use `.` to repeat the last editing operation
- **Persistent setting:** Vim mode preference is saved to
`~/.gemini/settings.json` and restored between sessions
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
footer
- **`/init`**
- **Description:** To help users easily create a `GEMINI.md` file, this
command analyzes the current directory and generates a tailored context
file, making it simpler for them to provide project-specific instructions to
the Gemini agent.
- **Repeat last command:** Use `.` to repeat the last editing operation
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
footer
### Custom commands
@@ -288,12 +343,12 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
These shortcuts apply directly to the input prompt for text manipulation.
- **Undo:**
- **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input
prompt.
- **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
in the input prompt.
- **Redo:**
- **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action
in the input prompt.
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
last undone action in the input prompt.
## At commands (`@`)
+80
View File
@@ -0,0 +1,80 @@
# Creating Agent Skills
This guide provides an overview of how to create your own Agent Skills to extend
the capabilities of Gemini CLI.
## Getting started: The `skill-creator` skill
The recommended way to create a new skill is to use the built-in `skill-creator`
skill. To use it, ask Gemini CLI to create a new skill for you.
**Example prompt:**
> "create a new skill called 'code-reviewer'"
Gemini CLI will then use the `skill-creator` to generate the skill:
1. Generate a new directory for your skill (e.g., `my-new-skill/`).
2. Create a `SKILL.md` file with the necessary YAML frontmatter (`name` and
`description`).
3. Create the standard resource directories: `scripts/`, `references/`, and
`assets/`.
## Manual skill creation
If you prefer to create skills manually:
1. **Create a directory** for your skill (e.g., `my-new-skill/`).
2. **Create a `SKILL.md` file** inside the new directory.
To add additional resources that support the skill, refer to the skill
structure.
## Skill structure
A skill is a directory containing a `SKILL.md` file at its root.
### Folder structure
While a `SKILL.md` file is the only required component, we recommend the
following structure for organizing your skill's resources:
```text
my-skill/
├── SKILL.md (Required) Instructions and metadata
├── scripts/ (Optional) Executable scripts
├── references/ (Optional) Static documentation
└── assets/ (Optional) Templates and other resources
```
### `SKILL.md` file
The `SKILL.md` file is the core of your skill. This file uses YAML frontmatter
for metadata and Markdown for instructions. For example:
```markdown
---
name: code-reviewer
description:
Use this skill to review code. It supports both local changes and remote Pull
Requests.
---
# Code Reviewer
This skill guides the agent in conducting thorough code reviews.
## Workflow
### 1. Determine Review Target
- **Remote PR**: If the user gives a PR number or URL, target that remote PR.
- **Local Changes**: If changes are local... ...
```
- **`name`**: A unique identifier for the skill. This should match the directory
name.
- **`description`**: A description of what the skill does and when Gemini should
use it.
- **Body**: The Markdown body of the file contains the instructions that guide
the agent's behavior when the skill is active.
+17
View File
@@ -203,6 +203,23 @@ with the actual Gemini CLI process, which inherits the environment variable.
This makes it significantly more difficult for a user to bypass the enforced
settings.
## User isolation in shared environments
In shared compute environments (like ML experiment runners or shared build
servers), you can isolate Gemini CLI state by overriding the user's home
directory.
By default, Gemini CLI stores configuration and history in `~/.gemini`. You can
use the `GEMINI_CLI_HOME` environment variable to point to a unique directory
for a specific user or job. The CLI will create a `.gemini` folder inside the
specified path.
```bash
# Isolate state for a specific job
export GEMINI_CLI_HOME="/tmp/gemini-job-123"
gemini
```
## Restricting tool access
You can significantly enhance security by controlling which tools the Gemini
+21 -12
View File
@@ -23,24 +23,24 @@ available combinations.
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Shift, Ctrl)` |
| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + B` |
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + F` |
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
#### Editing
| Action | Keys |
| ------------------------------------------------ | --------------------------------------------------------- |
| Delete from the cursor to the end of the line. | `Ctrl + K` |
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
| Action | Keys |
| ------------------------------------------------ | ---------------------------------------------------------------- |
| Delete from the cursor to the end of the line. | `Ctrl + K` |
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
| Undo the most recent text edit. | `Cmd + Z (no Shift)`<br />`Alt + Z (no Shift)` |
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`Shift + Alt + Z` |
#### Scrolling
@@ -106,10 +106,19 @@ available combinations.
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`<br />`Ctrl + S` |
| Ctrl+B | `Ctrl + B` |
| Ctrl+L | `Ctrl + L` |
| Ctrl+K | `Ctrl + K` |
| Enter | `Enter` |
| Esc | `Esc` |
| Shift+Tab | `Shift + Tab` |
| Tab | `Tab (no Shift)` |
| Tab | `Tab (no Shift)` |
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Suspend the application (not yet implemented). | `Ctrl + Z` |
<!-- KEYBINDINGS-AUTOGEN:END -->
+51
View File
@@ -0,0 +1,51 @@
# Rewind
The `/rewind` command allows you to go back to a previous state in your
conversation and, optionally, revert any file changes made by the AI during
those interactions. This is a powerful tool for undoing mistakes, exploring
different approaches, or simply cleaning up your session history.
## Usage
To use the rewind feature, simply type `/rewind` into the input prompt and press
**Enter**.
Alternatively, you can use the keyboard shortcut: **Press `Esc` twice**.
## Interface
When you trigger a rewind, an interactive list of your previous interactions
appears.
1. **Select Interaction:** Use the **Up/Down arrow keys** to navigate through
the list. The most recent interactions are at the bottom.
2. **Preview:** As you select an interaction, you'll see a preview of the user
prompt and, if applicable, the number of files changed during that step.
3. **Confirm Selection:** Press **Enter** on the interaction you want to rewind
back to.
4. **Action Selection:** After selecting an interaction, you'll be presented
with a confirmation dialog with up to three options:
- **Rewind conversation and revert code changes:** Reverts both the chat
history and the file modifications to the state before the selected
interaction.
- **Rewind conversation:** Only reverts the chat history. File changes are
kept.
- **Revert code changes:** Only reverts the file modifications. The chat
history is kept.
- **Do nothing (esc):** Cancels the rewind operation.
If no code changes were made since the selected point, the options related to
reverting code changes will be hidden.
## Key Considerations
- **Destructive Action:** Rewinding is a destructive action for your current
session history and potentially your files. Use it with care.
- **Agent Awareness:** When you rewind the conversation, the AI model loses all
memory of the interactions that were removed. If you only revert code changes,
you may need to inform the model that the files have changed.
- **Manual Edits:** Rewinding only affects file changes made by the AI's edit
tools. It does **not** undo manual edits you've made or changes triggered by
the shell tool (`!`).
- **Compression:** Rewind works across chat compression points by reconstructing
the history from stored session data.
+40 -35
View File
@@ -39,30 +39,33 @@ they appear in the UI.
### UI
| UI Label | Setting | Description | Default |
| ------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
### IDE
@@ -76,6 +79,7 @@ they appear in the UI.
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
### Context
@@ -96,7 +100,6 @@ they appear in the UI.
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
| Approval Mode | `tools.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
@@ -106,13 +109,14 @@ they appear in the UI.
### Security
| UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
### Experimental
@@ -129,8 +133,9 @@ they appear in the UI.
### HooksConfig
| UI Label | Setting | Description | Default |
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
| UI Label | Setting | Description | Default |
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- | ------- |
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
<!-- SETTINGS-AUTOGEN:END -->
+5 -77
View File
@@ -89,83 +89,6 @@ gemini skills enable my-expertise
gemini skills disable my-expertise --scope workspace
```
## Creating a Skill
A skill is a directory containing a `SKILL.md` file at its root. This file uses
YAML frontmatter for metadata and Markdown for instructions.
### Folder Structure
Skills are self-contained directories. At a minimum, a skill requires a
`SKILL.md` file, but can include other resources:
```text
my-skill/
├── SKILL.md (Required) Instructions and metadata
├── scripts/ (Optional) Executable scripts/tools
├── references/ (Optional) Static documentation and examples
└── assets/ (Optional) Templates and binary resources
```
### Basic Structure (SKILL.md)
```markdown
---
name: <unique-name>
description: <what the skill does and when Gemini should use it>
---
<your instructions for how the agent should behave / use the skill>
```
- **`name`**: A unique identifier (lowercase, alphanumeric, and dashes).
- **`description`**: The most critical field. Gemini uses this to decide when
the skill is relevant. Be specific about the expertise provided.
- **Body**: Everything below the second `---` is injected as expert procedural
guidance for the model.
### Example: Team Code Reviewer
Create `~/.gemini/skills/code-reviewer/SKILL.md`:
```markdown
---
name: code-reviewer
description:
Expertise in reviewing code for style, security, and performance. Use when the
user asks for "feedback," a "review," or to "check" their changes.
---
# Code Reviewer
You are an expert code reviewer. When reviewing code, follow this workflow:
1. **Analyze**: Review the staged changes or specific files provided. Ensure
that the changes are scoped properly and represent minimal changes required
to address the issue.
2. **Style**: Ensure code follows the workspace's conventions and idiomatic
patterns as described in the `GEMINI.md` file.
3. **Security**: Flag any potential security vulnerabilities.
4. **Tests**: Verify that new logic has corresponding test coverage and that
the test coverage adequately validates the changes.
Provide your feedback as a concise bulleted list of "Strengths" and
"Opportunities."
```
### Resource Conventions
While you can structure your skill directory however you like, the Agent Skills
standard encourages these conventions:
- **`scripts/`**: Executable scripts (bash, python, node) the agent can run.
- **`references/`**: Static documentation, schemas, or example data for the
agent to consult.
- **`assets/`**: Code templates, boilerplate, or binary resources.
When a skill is activated, Gemini CLI provides the model with a tree view of the
entire skill directory, allowing it to discover and utilize these assets.
## How it Works (Security & Privacy)
1. **Discovery**: At the start of a session, Gemini CLI scans the discovery
@@ -182,3 +105,8 @@ entire skill directory, allowing it to discover and utilize these assets.
it permission to read any bundled assets.
5. **Execution**: The model proceeds with the specialized expertise active. It
is instructed to prioritize the skill's procedural guidance within reason.
## Creating your own skills
To create your own skills, see the [Create Agent Skills](./creating-skills.md)
guide.
+8 -3
View File
@@ -210,6 +210,10 @@ decision = "ask_user"
# The priority of the rule, from 0 to 999.
priority = 10
# (Optional) A custom message to display when a tool call is denied by this rule.
# This message is returned to the model and user, useful for explaining *why* it was denied.
deny_message = "Deletion is permanent"
# (Optional) An array of approval modes where this rule is active.
modes = ["autoEdit"]
```
@@ -282,6 +286,7 @@ only the `mcpName`.
mcpName = "untrusted-server"
decision = "deny"
priority = 500
deny_message = "This server is not trusted by the admin."
```
## Default policies
@@ -290,9 +295,9 @@ The Gemini CLI ships with a set of default policies to provide a safe
out-of-the-box experience.
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
- **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to
ensure remote agents can prompt for confirmation, but local sub-agent actions
are executed silently and checked individually.
- **Agent delegation** defaults to **`ask_user`** to ensure remote agents can
prompt for confirmation, but local sub-agent actions are executed silently and
checked individually.
- **Write tools** (like `write_file`, `run_shell_command`) default to
**`ask_user`**.
- In **`yolo`** mode, a high-priority rule allows all tools.
+35 -30
View File
@@ -24,30 +24,29 @@ the main agent's context or toolset.
## What are sub-agents?
Think of sub-agents as "specialists" that the main Gemini agent can hire for a
specific job.
Sub-agents are "specialists" that the main Gemini agent can hire for a specific
job.
- **Focused context:** Each sub-agent has its own system prompt and persona.
- **Specialized tools:** Sub-agents can have a restricted or specialized set of
tools.
- **Independent context window:** Interactions with a sub-agent happen in a
separate context loop. The main agent only sees the final result, saving
tokens in your main conversation history.
separate context loop, which saves tokens in your main conversation history.
Sub-agents are exposed to the main agent as a tool of the same name which
delegates to the sub-agent, when called. Once the sub-agent completes its task
(or fails), it reports back to the main agent with its findings (usually as a
text summary or structured report returned by the tool).
Sub-agents are exposed to the main agent as a tool of the same name. When the
main agent calls the tool, it delegates the task to the sub-agent. Once the
sub-agent completes its task, it reports back to the main agent with its
findings.
## Built-in sub-agents
Gemini CLI comes with powerful built-in sub-agents.
Gemini CLI comes with the following built-in sub-agents:
### Codebase Investigator
- **Name:** `codebase_investigator`
- **Purpose:** Deep analysis of the codebase, reverse engineering, and
understanding complex dependencies.
- **Purpose:** Analyze the codebase, reverse engineer, and understand complex
dependencies.
- **When to use:** "How does the authentication system work?", "Map out the
dependencies of the `AgentRegistry` class."
- **Configuration:** Enabled by default. You can configure it in
@@ -67,20 +66,25 @@ Gemini CLI comes with powerful built-in sub-agents.
### CLI Help Agent
- **Name:** `cli_help`
- **Purpose:** Expert knowledge about Gemini CLI itself, its commands,
- **Purpose:** Get expert knowledge about Gemini CLI itself, its commands,
configuration, and documentation.
- **When to use:** "How do I configure a proxy?", "What does the `/rewind`
command do?"
- **Configuration:** Enabled by default.
### Generalist Agent
- **Name:** `generalist_agent`
- **Purpose:** Route tasks to the appropriate specialized sub-agent.
- **When to use:** Implicitly used by the main agent for routing. Not directly
invoked by the user.
- **Configuration:** Enabled by default. No specific configuration options.
## Creating custom sub-agents
You can create your own sub-agents to automate specific workflows or enforce
specific personas.
### Prerequisites
To use custom sub-agents, you must enable them in your `settings.json`:
specific personas. To use custom sub-agents, you must enable them in your
`settings.json`:
```json
{
@@ -112,7 +116,7 @@ description: Specialized in finding security vulnerabilities in code.
kind: local
tools:
- read_file
- search_file_content
- grep_search
model: gemini-2.5-pro
temperature: 0.2
max_turns: 10
@@ -123,10 +127,10 @@ vulnerabilities.
Focus on:
1. SQL Injection
2. XSS (Cross-Site Scripting)
3. Hardcoded credentials
4. Unsafe file operations
1. SQL Injection
2. XSS (Cross-Site Scripting)
3. Hardcoded credentials
4. Unsafe file operations
When you find a vulnerability, explain it clearly and suggest a fix. Do not fix
it yourself; just report it.
@@ -147,17 +151,16 @@ it yourself; just report it.
### Optimizing your sub-agent
The main agent system prompt contains language that encourages use of an expert
sub-agent when one is available for the task at hand. It decides whether an
agent is a relevant expert based on the agent's description. You can improve the
reliability with which an agent is used by updating the description to more
clearly indicate:
The main agent's system prompt encourages it to use an expert sub-agent when one
is available. It decides whether an agent is a relevant expert based on the
agent's description. You can improve the reliability with which an agent is used
by updating the description to more clearly indicate:
- Its area of expertise.
- When it should be used.
- Some example scenarios.
For example: the following sub-agent description should be called fairly
For example, the following sub-agent description should be called fairly
consistently for Git operations.
> Git expert agent which should be used for all local and remote git operations.
@@ -171,11 +174,13 @@ If you need to further tune your sub-agent, you can do so by selecting the model
to optimize for with `/model` and then asking the model why it does not think
that your sub-agent was called with a specific prompt and the given description.
## Remote subagents (Agent2Agent)
## Remote subagents (Agent2Agent) (experimental)
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
Gemini CLI can also delegate tasks to remote sub-agents using the Agent-to-Agent
(A2A) protocol.
> **Note: Remote subagents are currently an experimental feature.**
See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed
configuration and usage instructions.
-8
View File
@@ -215,14 +215,6 @@ a few things you can try in order of recommendation:
MCP servers of their own. This should not be used as an airtight security
mechanism.
- **`autoAccept`** (boolean):
- **Description:** Controls whether the CLI automatically accepts and executes
tool calls that are considered safe (e.g., read-only operations) without
explicit user confirmation. If set to `true`, the CLI will bypass the
confirmation prompt for tools deemed safe.
- **Default:** `false`
- **Example:** `"autoAccept": true`
- **`theme`** (string):
- **Description:** Sets the visual [theme](../cli/themes.md) for Gemini CLI.
- **Default:** `"Default"`
+38 -16
View File
@@ -170,6 +170,15 @@ their corresponding top-level category object in your `settings.json` file.
available options.
- **Default:** `undefined`
- **`ui.autoThemeSwitching`** (boolean):
- **Description:** Automatically switch between default light and dark themes
based on terminal background color.
- **Default:** `true`
- **`ui.terminalBackgroundPollingInterval`** (number):
- **Description:** Interval in seconds to poll the terminal background color.
- **Default:** `60`
- **`ui.customThemes`** (object):
- **Description:** Custom theme definitions.
- **Default:** `{}`
@@ -244,6 +253,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Show the model name in the chat for each model turn.
- **Default:** `false`
- **`ui.showUserIdentity`** (boolean):
- **Description:** Show the logged-in user's identity (e.g. email) in the UI.
- **Default:** `true`
- **`ui.useAlternateBuffer`** (boolean):
- **Description:** Use an alternate screen buffer for the UI, preserving shell
history.
@@ -322,6 +335,12 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `0.5`
- **Requires restart:** Yes
- **`model.disableLoopDetection`** (boolean):
- **Description:** Disable automatic detection and prevention of infinite
loops.
- **Default:** `false`
- **Requires restart:** Yes
- **`model.skipNextSpeakerCheck`** (boolean):
- **Description:** Skip the next speaker check.
- **Default:** `true`
@@ -657,11 +676,6 @@ their corresponding top-level category object in your `settings.json` file.
performance.
- **Default:** `true`
- **`tools.autoAccept`** (boolean):
- **Description:** Automatically accept and execute tool calls that are
considered safe (e.g., read-only operations).
- **Default:** `false`
- **`tools.approvalMode`** (enum):
- **Description:** The default approval mode for tool execution. 'default'
prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is
@@ -729,12 +743,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.enableHooks`** (boolean):
- **Description:** Enables the hooks system experiment. When disabled, the
hooks system is completely deactivated regardless of other settings.
- **Default:** `true`
- **Requires restart:** Yes
#### `mcp`
- **`mcp.serverCommand`** (string):
@@ -775,6 +783,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.allowedExtensions`** (array):
- **Description:** List of Regex patterns for allowed extensions. If nonempty,
only extensions that match the patterns in this list are allowed. Overrides
the blockGitExtensions setting.
- **Default:** `[]`
- **Requires restart:** Yes
- **`security.folderTrust.enabled`** (boolean):
- **Description:** Setting to track whether Folder trust is enabled.
- **Default:** `false`
@@ -869,11 +884,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.skills`** (boolean):
- **Description:** [Deprecated] Enable Agent Skills (experimental).
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
(useful for remote sessions).
@@ -902,6 +912,7 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Canonical toggle for the hooks system. When disabled, no
hooks will be executed.
- **Default:** `true`
- **Requires restart:** Yes
- **`hooksConfig.disabled`** (array):
- **Description:** List of hook names (commands) that should be disabled.
@@ -1165,6 +1176,13 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Specifies the default Gemini model to use.
- Overrides the hardcoded default
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"`
- **`GEMINI_CLI_HOME`**:
- Specifies the root directory for Gemini CLI's user-level configuration and
storage.
- By default, this is the user's system home directory. The CLI will create a
`.gemini` folder inside this directory.
- Useful for shared compute environments or keeping CLI state isolated.
- Example: `export GEMINI_CLI_HOME="/path/to/user/config"`
- **`GOOGLE_API_KEY`**:
- Your Google Cloud API key.
- Required for using Vertex AI in express mode.
@@ -1185,6 +1203,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **Description:** The path to your Google Application Credentials JSON file.
- **Example:**
`export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
- **`GOOGLE_GENAI_API_VERSION`**:
- Specifies the API version to use for Gemini API requests.
- When set, overrides the default API version used by the SDK.
- Example: `export GOOGLE_GENAI_API_VERSION="v1"`
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
- Your Google Cloud Project ID for Telemetry in Google Cloud
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
+2 -2
View File
@@ -167,8 +167,8 @@ case is response validation and automatic retries.
- `reason`: Required if denied. This text is sent **to the agent as a new
prompt** to request a correction.
- `continue`: Set to `false` to **stop the session** without retrying.
- `clearContext`: If `true`, clears conversation history (LLM memory) while
preserving UI display.
- `hookSpecificOutput.clearContext`: If `true`, clears conversation history
(LLM memory) while preserving UI display.
- **Exit Code 2 (Retry)**: Rejects the response and triggers an automatic retry
turn using `stderr` as the feedback prompt.
+99 -125
View File
@@ -1,149 +1,123 @@
# Welcome to Gemini CLI documentation
# Gemini CLI documentation
This documentation provides a comprehensive guide to installing, using, and
developing Gemini CLI, a tool that lets you interact with Gemini models through
a command-line interface.
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
into your terminal. It is designed to be a terminal-first, extensible, and
powerful tool for developers, engineers, SREs, and beyond.
## Gemini CLI overview
Gemini CLI integrates with your local environment. It can read and edit files,
execute shell commands, and search the web, all while maintaining your project
context.
Gemini CLI brings the capabilities of Gemini models to your terminal in an
interactive Read-Eval-Print Loop (REPL) environment. Gemini CLI consists of a
client-side application (`packages/cli`) that communicates with a local server
(`packages/core`), which in turn manages requests to the Gemini API and its AI
models. Gemini CLI also contains a variety of tools for tasks such as performing
file system operations, running shells, and web fetching, which are managed by
`packages/core`.
## Get started
## Navigating the documentation
Begin your journey with Gemini CLI by setting up your environment and learning
the basics.
This documentation is organized into the following sections:
- **[Quickstart](./get-started/index.md):** A streamlined guide to get you
chatting in minutes.
- **[Installation](./get-started/installation.md):** Instructions for macOS,
Linux, and Windows.
- **[Authentication](./get-started/authentication.md):** Set up access using
Google OAuth, API keys, or Vertex AI.
- **[Examples](./get-started/examples.md):** View common usage scenarios to
inspire your own workflows.
### Overview
## Use Gemini CLI
- **[Architecture overview](./architecture.md):** Understand the high-level
design of Gemini CLI, including its components and how they interact.
- **[Contribution guide](../CONTRIBUTING.md):** Information for contributors and
developers, including setup, building, testing, and coding conventions.
Master the core capabilities that let Gemini CLI interact with your system
safely and effectively.
### Get started
- **[Gemini CLI quickstart](./get-started/index.md):** Let's get started with
Gemini CLI.
- **[Gemini 3 Pro on Gemini CLI](./get-started/gemini-3.md):** Learn how to
enable and use Gemini 3.
- **[Authentication](./get-started/authentication.md):** Authenticate to Gemini
CLI.
- **[Configuration](./get-started/configuration.md):** Learn how to configure
the CLI.
- **[Installation](./get-started/installation.md):** Install and run Gemini CLI.
- **[Examples](./get-started/examples.md):** Example usage of Gemini CLI.
### CLI
- **[Introduction: Gemini CLI](./cli/index.md):** Overview of the command-line
- **[Using the CLI](./cli/index.md):** Learn the basics of the command-line
interface.
- **[Commands](./cli/commands.md):** Description of available CLI commands.
- **[Checkpointing](./cli/checkpointing.md):** Documentation for the
checkpointing feature.
- **[Custom commands](./cli/custom-commands.md):** Create your own commands and
shortcuts for frequently used prompts.
- **[Enterprise](./cli/enterprise.md):** Gemini CLI for enterprise.
- **[Headless mode](./cli/headless.md):** Use Gemini CLI programmatically for
scripting and automation.
- **[Keyboard shortcuts](./cli/keyboard-shortcuts.md):** A reference for all
keyboard shortcuts to improve your workflow.
- **[Model selection](./cli/model.md):** Select the model used to process your
commands with `/model`.
- **[Sandbox](./cli/sandbox.md):** Isolate tool execution in a secure,
containerized environment.
- **[Agent Skills](./cli/skills.md):** Extend the CLI with specialized expertise
and procedural workflows.
- **[Settings](./cli/settings.md):** Configure various aspects of the CLI's
behavior and appearance with `/settings`.
- **[Telemetry](./cli/telemetry.md):** Overview of telemetry in the CLI.
- **[Themes](./cli/themes.md):** Themes for Gemini CLI.
- **[Token caching](./cli/token-caching.md):** Token caching and optimization.
- **[Trusted Folders](./cli/trusted-folders.md):** An overview of the Trusted
Folders security feature.
- **[Tutorials](./cli/tutorials.md):** Tutorials for Gemini CLI.
- **[Uninstall](./cli/uninstall.md):** Methods for uninstalling the Gemini CLI.
- **[File management](./tools/file-system.md):** Grant the model the ability to
read code and apply changes directly to your files.
- **[Shell commands](./tools/shell.md):** Allow the model to run builds, tests,
and git commands.
- **[Memory management](./tools/memory.md):** Teach Gemini CLI facts about your
project and preferences that persist across sessions.
- **[Project context](./cli/gemini-md.md):** Use `GEMINI.md` files to provide
persistent context for your projects.
- **[Web search and fetch](./tools/web-search.md):** Enable the model to fetch
real-time information from the internet.
- **[Session management](./cli/session-management.md):** Save, resume, and
organize your chat sessions.
### Core
## Configuration
- **[Introduction: Gemini CLI core](./core/index.md):** Information about Gemini
CLI core.
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
exposes tools.
- **[System Prompt Override](./cli/system-prompt.md):** Replace built-in system
instructions using `GEMINI_SYSTEM_MD`.
Customize Gemini CLI to match your workflow and preferences.
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
fine-grained control over tool execution.
- **[Settings](./cli/settings.md):** Control response creativity, output
verbosity, and more.
- **[Model selection](./cli/model.md):** Choose the best Gemini model for your
specific task.
- **[Ignore files](./cli/gemini-ignore.md):** Use `.geminiignore` to keep
sensitive files out of the model's context.
- **[Trusted folders](./cli/trusted-folders.md):** Define security boundaries
for file access and execution.
- **[Token caching](./cli/token-caching.md):** Optimize performance and cost by
caching context.
- **[Themes](./cli/themes.md):** Personalize the visual appearance of the CLI.
### Tools
## Advanced features
- **[Introduction: Gemini CLI tools](./tools/index.md):** Information about
Gemini CLI's tools.
- **[File system tools](./tools/file-system.md):** Documentation for the
`read_file` and `write_file` tools.
- **[Shell tool](./tools/shell.md):** Documentation for the `run_shell_command`
tool.
- **[Web fetch tool](./tools/web-fetch.md):** Documentation for the `web_fetch`
tool.
- **[Web search tool](./tools/web-search.md):** Documentation for the
`google_web_search` tool.
- **[Memory tool](./tools/memory.md):** Documentation for the `save_memory`
tool.
- **[Todo tool](./tools/todos.md):** Documentation for the `write_todos` tool.
- **[MCP servers](./tools/mcp-server.md):** Using MCP servers with Gemini CLI.
Explore powerful features for complex workflows and enterprise environments.
### Extensions
- **[Headless mode](./cli/headless.md):** Run Gemini CLI in scripts or CI/CD
pipelines for automated reasoning.
- **[Sandboxing](./cli/sandbox.md):** Execute untrusted code or tools in a
secure, isolated container.
- **[Checkpointing](./cli/checkpointing.md):** Save and restore workspace state
to recover from experimental changes.
- **[Custom commands](./cli/custom-commands.md):** Create shortcuts for
frequently used prompts.
- **[System prompt override](./cli/system-prompt.md):** Customize the core
instructions given to the model.
- **[Telemetry](./cli/telemetry.md):** Understand how usage data is collected
and managed.
- **[Enterprise](./cli/enterprise.md):** Manage configurations and policies for
large teams.
- **[Introduction: Extensions](./extensions/index.md):** How to extend the CLI
with new functionality.
## Extensions
Extend Gemini CLI's capabilities with new tools and behaviors using extensions.
- **[Introduction](./extensions/index.md):** Learn about the extension system
and how to manage extensions.
- **[Writing extensions](./extensions/writing-extensions.md):** Learn how to
build your own extension.
- **[Extension releasing](./extensions/releasing.md):** How to release Gemini
CLI extensions.
create your first extension.
- **[Extensions reference](./extensions/reference.md):** Deeply understand the
extension format, commands, and configuration.
- **[Best practices](./extensions/best-practices.md):** Learn strategies for
building great extensions.
- **[Extensions releasing](./extensions/releasing.md):** Learn how to share your
extensions with the world.
### Hooks
## Ecosystem and extensibility
- **[Hooks](./hooks/index.md):** Intercept and customize Gemini CLI behavior at
key lifecycle points.
- **[Writing Hooks](./hooks/writing-hooks.md):** Learn how to create your first
hook with a comprehensive example.
- **[Best Practices](./hooks/best-practices.md):** Security, performance, and
debugging guidelines for hooks.
Connect Gemini CLI to external services and other development tools.
### IDE integration
- **[MCP servers](./tools/mcp-server.md):** Connect to external services using
the Model Context Protocol.
- **[IDE integration](./ide-integration/index.md):** Use Gemini CLI alongside VS
Code.
- **[Hooks](./hooks/index.md):** (Preview) Write scripts that run on specific
CLI events.
- **[Agent skills](./cli/skills.md):** (Preview) Add specialized expertise and
workflows.
- **[Sub-agents](./core/subagents.md):** (Preview) Delegate tasks to specialized
agents.
- **[Introduction to IDE integration](./ide-integration/index.md):** Connect the
CLI to your editor.
- **[IDE companion extension spec](./ide-integration/ide-companion-spec.md):**
Spec for building IDE companion extensions.
## Development and reference
### Development
Deep dive into the architecture and contribute to the project.
- **[NPM](./npm.md):** Details on how the project's packages are structured.
- **[Releases](./releases.md):** Information on the project's releases and
deployment cadence.
- **[Changelog](./changelogs/index.md):** Highlights and notable changes to
- **[Architecture](./architecture.md):** Understand the technical design of
Gemini CLI.
- **[Integration tests](./integration-tests.md):** Information about the
integration testing framework used in this project.
- **[Issue and PR automation](./issue-and-pr-automation.md):** A detailed
overview of the automated processes we use to manage and triage issues and
pull requests.
### Support
- **[FAQ](./faq.md):** Frequently asked questions.
- **[Troubleshooting guide](./troubleshooting.md):** Find solutions to common
problems.
- **[Quota and pricing](./quota-and-pricing.md):** Learn about the free tier and
paid options.
- **[Terms of service and privacy notice](./tos-privacy.md):** Information on
the terms of service and privacy notices applicable to your use of Gemini CLI.
We hope this documentation helps you make the most of Gemini CLI!
- **[Command reference](./cli/commands.md):** A complete list of available
commands.
- **[Local development](./local-development.md):** Set up your environment to
contribute to Gemini CLI.
- **[Contributing](../CONTRIBUTING.md):** Learn how to submit pull requests and
report issues.
- **[FAQ](./faq.md):** Answers to common questions.
- **[Troubleshooting](./troubleshooting.md):** Solutions for common issues.
+14 -2
View File
@@ -7,7 +7,8 @@
{ "label": "Installation", "slug": "docs/get-started/installation" },
{ "label": "Authentication", "slug": "docs/get-started/authentication" },
{ "label": "Examples", "slug": "docs/get-started/examples" },
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" }
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" },
{ "label": "CLI Reference", "slug": "docs/cli/cli-reference" }
]
},
{
@@ -78,6 +79,10 @@
"label": "Ecosystem and extensibility",
"items": [
{ "label": "Agent skills", "slug": "docs/cli/skills" },
{
"label": "Creating Agent skills",
"slug": "docs/cli/creating-skills"
},
{
"label": "Sub-agents (experimental)",
"slug": "docs/core/subagents"
@@ -118,7 +123,14 @@
"items": [
{ "label": "FAQ", "slug": "docs/faq" },
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
{ "label": "Release notes", "slug": "docs/changelogs/" },
{
"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": "Troubleshooting", "slug": "docs/troubleshooting" },
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
+5 -6
View File
@@ -117,14 +117,13 @@ directories) will be created.
`Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...`
- **Confirmation:** No.
## 5. `search_file_content` (SearchText)
## 5. `grep_search` (SearchText)
`search_file_content` searches for a regular expression pattern within the
content of files in a specified directory. Can filter files by a glob pattern.
Returns the lines containing matches, along with their file paths and line
numbers.
`grep_search` searches for a regular expression pattern within the content of
files in a specified directory. Can filter files by a glob pattern. Returns the
lines containing matches, along with their file paths and line numbers.
- **Tool name:** `search_file_content`
- **Tool name:** `grep_search`
- **Display name:** SearchText
- **File:** `grep.ts`
- **Parameters:**
+13 -2
View File
@@ -739,10 +739,21 @@ The MCP integration tracks several states:
cautiously and only for servers you completely control
- **Access tokens:** Be security-aware when configuring environment variables
containing API keys or tokens
- **Environment variable redaction:** By default, the Gemini CLI redacts
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
spawning MCP servers using the `stdio` transport. This prevents unintended
exposure of your credentials to third-party servers.
- **Explicit environment variables:** If you need to pass a specific environment
variable to an MCP server, you should define it explicitly in the `env`
property of the server configuration in `settings.json`.
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
available within the sandbox environment
available within the sandbox environment.
- **Private data:** Using broadly scoped personal access tokens can lead to
information leakage between repositories
information leakage between repositories.
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
untrusted or third-party sources. Malicious servers could attempt to
exfiltrate data or perform unauthorized actions through the tools they expose.
### Performance and resource management
+2 -2
View File
@@ -24,11 +24,11 @@ describe('generalist_agent', () => {
prompt:
'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success',
assert: async (rig) => {
// 1) Verify the generalist agent was invoked via delegate_to_agent
// 1) Verify the generalist agent was invoked
const foundToolCall = await rig.waitForToolCall('generalist');
expect(
foundToolCall,
'Expected to find a delegate_to_agent tool call for generalist agent',
'Expected to find a tool call for generalist agent',
).toBeTruthy();
// 2) Verify the file was created as expected
+80 -11
View File
@@ -7,9 +7,13 @@
import { it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { execSync } from 'node:child_process';
import { TestRig } from '@google/gemini-cli-test-utils';
import { createUnauthorizedToolError } from '@google/gemini-cli-core';
import {
createUnauthorizedToolError,
parseAgentMarkdown,
} from '@google/gemini-cli-core';
export * from '@google/gemini-cli-test-utils';
@@ -34,14 +38,63 @@ export type EvalPolicy = 'ALWAYS_PASSES' | 'USUALLY_PASSES';
export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
const fn = async () => {
const rig = new TestRig();
const { logDir, sanitizedName } = await prepareLogDir(evalCase.name);
const activityLogFile = path.join(logDir, `${sanitizedName}.jsonl`);
const logFile = path.join(logDir, `${sanitizedName}.log`);
let isSuccess = false;
try {
rig.setup(evalCase.name, evalCase.params);
if (evalCase.files) {
const acknowledgedAgents: Record<string, Record<string, string>> = {};
const projectRoot = fs.realpathSync(rig.testDir!);
for (const [filePath, content] of Object.entries(evalCase.files)) {
const fullPath = path.join(rig.testDir!, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
// If it's an agent file, calculate hash for acknowledgement
if (
filePath.startsWith('.gemini/agents/') &&
filePath.endsWith('.md')
) {
const hash = crypto
.createHash('sha256')
.update(content)
.digest('hex');
try {
const agentDefs = await parseAgentMarkdown(fullPath, content);
if (agentDefs.length > 0) {
const agentName = agentDefs[0].name;
if (!acknowledgedAgents[projectRoot]) {
acknowledgedAgents[projectRoot] = {};
}
acknowledgedAgents[projectRoot][agentName] = hash;
}
} catch (error) {
console.warn(
`Failed to parse agent for test acknowledgement: ${filePath}`,
error,
);
}
}
}
// Write acknowledged_agents.json to the home directory
if (Object.keys(acknowledgedAgents).length > 0) {
const ackPath = path.join(
rig.homeDir!,
'.gemini',
'acknowledgments',
'agents.json',
);
fs.mkdirSync(path.dirname(ackPath), { recursive: true });
fs.writeFileSync(
ackPath,
JSON.stringify(acknowledgedAgents, null, 2),
);
}
const execOptions = { cwd: rig.testDir!, stdio: 'inherit' as const };
@@ -62,6 +115,10 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
const result = await rig.run({
args: evalCase.prompt,
approvalMode: evalCase.approvalMode ?? 'yolo',
timeout: evalCase.timeout,
env: {
GEMINI_CLI_ACTIVITY_LOG_FILE: activityLogFile,
},
});
const unauthorizedErrorPrefix =
@@ -73,9 +130,21 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
}
await evalCase.assert(rig, result);
isSuccess = true;
} finally {
await logToFile(
evalCase.name,
if (isSuccess) {
await fs.promises.unlink(activityLogFile).catch((err) => {
if (err.code !== 'ENOENT') throw err;
});
}
if (rig._lastRunStderr) {
const stderrFile = path.join(logDir, `${sanitizedName}.stderr.log`);
await fs.promises.writeFile(stderrFile, rig._lastRunStderr);
}
await fs.promises.writeFile(
logFile,
JSON.stringify(rig.readToolLogs(), null, 2),
);
await rig.cleanup();
@@ -89,19 +158,19 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
}
}
async function prepareLogDir(name: string) {
const logDir = path.resolve(process.cwd(), 'evals/logs');
await fs.promises.mkdir(logDir, { recursive: true });
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
return { logDir, sanitizedName };
}
export interface EvalCase {
name: string;
params?: Record<string, any>;
prompt: string;
timeout?: number;
files?: Record<string, string>;
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
assert: (rig: TestRig, result: string) => Promise<void>;
}
async function logToFile(name: string, content: string) {
const logDir = 'evals/logs';
await fs.promises.mkdir(logDir, { recursive: true });
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const logFile = `${logDir}/${sanitizedName}.log`;
await fs.promises.writeFile(logFile, content);
}
+163
View File
@@ -0,0 +1,163 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { spawn, ChildProcess } from 'node:child_process';
import { join, resolve } from 'node:path';
import { writeFileSync, mkdirSync } from 'node:fs';
import { Writable, Readable } from 'node:stream';
import { env } from 'node:process';
import * as acp from '@agentclientprotocol/sdk';
const sandboxEnv = env['GEMINI_SANDBOX'];
const itMaybe = sandboxEnv && sandboxEnv !== 'false' ? it.skip : it;
class MockClient implements acp.Client {
updates: acp.SessionNotification[] = [];
sessionUpdate = async (params: acp.SessionNotification) => {
this.updates.push(params);
};
requestPermission = async (): Promise<acp.RequestPermissionResponse> => {
throw new Error('unexpected');
};
}
describe('ACP Environment and Auth', () => {
let rig: TestRig;
let child: ChildProcess | undefined;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
child?.kill();
child = undefined;
await rig.cleanup();
});
itMaybe(
'should load .env from project directory and use the provided API key',
async () => {
rig.setup('acp-env-loading');
// Create a project directory with a .env file containing a recognizable invalid key
const projectDir = resolve(join(rig.testDir!, 'project'));
mkdirSync(projectDir, { recursive: true });
writeFileSync(
join(projectDir, '.env'),
'GEMINI_API_KEY=test-key-from-env\n',
);
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn('node', [bundlePath, '--experimental-acp'], {
cwd: rig.homeDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
...process.env,
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_API_KEY: undefined,
VERBOSE: 'true',
},
});
const input = Writable.toWeb(child.stdin!);
const output = Readable.toWeb(
child.stdout!,
) as ReadableStream<Uint8Array>;
const testClient = new MockClient();
const stream = acp.ndJsonStream(input, output);
const connection = new acp.ClientSideConnection(() => testClient, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: {
fs: { readTextFile: false, writeTextFile: false },
},
});
// 1. newSession should succeed because it finds the key in .env
const { sessionId } = await connection.newSession({
cwd: projectDir,
mcpServers: [],
});
expect(sessionId).toBeDefined();
// 2. prompt should fail because the key is invalid,
// but the error should come from the API, not the internal auth check.
await expect(
connection.prompt({
sessionId,
prompt: [{ type: 'text', text: 'hello' }],
}),
).rejects.toSatisfy((error: unknown) => {
const acpError = error as acp.RequestError;
const errorData = acpError.data as
| { error?: { message?: string } }
| undefined;
const message = String(errorData?.error?.message || acpError.message);
// It should NOT be our internal "Authentication required" message
expect(message).not.toContain('Authentication required');
// It SHOULD be an API error mentioning the invalid key
expect(message).toContain('API key not valid');
return true;
});
child.stdin!.end();
},
);
itMaybe(
'should fail with authRequired when no API key is found',
async () => {
rig.setup('acp-auth-failure');
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn('node', [bundlePath, '--experimental-acp'], {
cwd: rig.homeDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
...process.env,
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_API_KEY: undefined,
VERBOSE: 'true',
},
});
const input = Writable.toWeb(child.stdin!);
const output = Readable.toWeb(
child.stdout!,
) as ReadableStream<Uint8Array>;
const testClient = new MockClient();
const stream = acp.ndJsonStream(input, output);
const connection = new acp.ClientSideConnection(() => testClient, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: {
fs: { readTextFile: false, writeTextFile: false },
},
});
await expect(
connection.newSession({
cwd: resolve(rig.testDir!),
mcpServers: [],
}),
).rejects.toMatchObject({
message: expect.stringContaining(
'Gemini API key is missing or not configured.',
),
});
child.stdin!.end();
},
);
});
+155
View File
@@ -0,0 +1,155 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { GitService, Storage } from '@google/gemini-cli-core';
describe('Checkpointing Integration', () => {
let tmpDir: string;
let projectRoot: string;
let fakeHome: string;
let originalEnv: NodeJS.ProcessEnv;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'gemini-checkpoint-test-'),
);
projectRoot = path.join(tmpDir, 'project');
fakeHome = path.join(tmpDir, 'home');
await fs.mkdir(projectRoot, { recursive: true });
await fs.mkdir(fakeHome, { recursive: true });
// Save original env
originalEnv = { ...process.env };
// Simulate environment with NO global gitconfig
process.env['HOME'] = fakeHome;
delete process.env['GIT_CONFIG_GLOBAL'];
delete process.env['GIT_CONFIG_SYSTEM'];
});
afterEach(async () => {
// Restore env
process.env = originalEnv;
// Cleanup
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch (e) {
console.error('Failed to cleanup temp dir', e);
}
});
it('should successfully create and restore snapshots without global git config', async () => {
const storage = new Storage(projectRoot);
const gitService = new GitService(projectRoot, storage);
// 1. Initialize
await gitService.initialize();
// Verify system config empty file creation
// We need to access getHistoryDir logic or replicate it.
// Since we don't have access to private getHistoryDir, we can infer it or just trust the functional test.
// 2. Create initial state
await fs.writeFile(path.join(projectRoot, 'file1.txt'), 'version 1');
await fs.writeFile(path.join(projectRoot, 'file2.txt'), 'permanent file');
// 3. Create Snapshot
const snapshotHash = await gitService.createFileSnapshot('Checkpoint 1');
expect(snapshotHash).toBeDefined();
// 4. Modify files
await fs.writeFile(
path.join(projectRoot, 'file1.txt'),
'version 2 (BAD CHANGE)',
);
await fs.writeFile(
path.join(projectRoot, 'file3.txt'),
'new file (SHOULD BE GONE)',
);
await fs.rm(path.join(projectRoot, 'file2.txt'));
// 5. Restore
await gitService.restoreProjectFromSnapshot(snapshotHash);
// 6. Verify state
const file1Content = await fs.readFile(
path.join(projectRoot, 'file1.txt'),
'utf-8',
);
expect(file1Content).toBe('version 1');
const file2Exists = await fs
.stat(path.join(projectRoot, 'file2.txt'))
.then(() => true)
.catch(() => false);
expect(file2Exists).toBe(true);
const file2Content = await fs.readFile(
path.join(projectRoot, 'file2.txt'),
'utf-8',
);
expect(file2Content).toBe('permanent file');
const file3Exists = await fs
.stat(path.join(projectRoot, 'file3.txt'))
.then(() => true)
.catch(() => false);
expect(file3Exists).toBe(false);
});
it('should ignore user global git config and use isolated identity', async () => {
// 1. Create a fake global gitconfig with a specific user
const globalConfigPath = path.join(fakeHome, '.gitconfig');
const globalConfigContent = `[user]
name = Global User
email = global@example.com
`;
await fs.writeFile(globalConfigPath, globalConfigContent);
// Point HOME to fakeHome so git picks up this global config (if we didn't isolate it)
process.env['HOME'] = fakeHome;
// Ensure GIT_CONFIG_GLOBAL is NOT set for the process initially,
// so it would default to HOME/.gitconfig if GitService didn't override it.
delete process.env['GIT_CONFIG_GLOBAL'];
const storage = new Storage(projectRoot);
const gitService = new GitService(projectRoot, storage);
await gitService.initialize();
// 2. Create a file and snapshot
await fs.writeFile(path.join(projectRoot, 'test.txt'), 'content');
await gitService.createFileSnapshot('Snapshot with global config present');
// 3. Verify the commit author in the shadow repo
const historyDir = storage.getHistoryDir();
const { execFileSync } = await import('node:child_process');
const logOutput = execFileSync(
'git',
['log', '-1', '--pretty=format:%an <%ae>'],
{
cwd: historyDir,
env: {
...process.env,
GIT_DIR: path.join(historyDir, '.git'),
GIT_CONFIG_GLOBAL: path.join(historyDir, '.gitconfig'),
GIT_CONFIG_SYSTEM: path.join(historyDir, '.gitconfig_system_empty'),
},
encoding: 'utf-8',
},
);
expect(logOutput).toBe('Gemini CLI <gemini-cli@google.com>');
expect(logOutput).not.toContain('Global User');
});
});
+6
View File
@@ -13,6 +13,7 @@ import { mkdir, readdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
import { disableMouseTracking } from '@google/gemini-cli-core';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
@@ -72,6 +73,11 @@ export async function setup() {
}
export async function teardown() {
// Disable mouse tracking
if (process.stdout.isTTY) {
disableMouseTracking();
}
// Cleanup the test run directory unless KEEP_OUTPUT is set
if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
try {
+14 -4
View File
@@ -484,9 +484,19 @@ console.log(JSON.stringify({
'hooks-system.before-tool-selection.responses',
),
});
// Create inline hook command (works on both Unix and Windows)
const hookCommand =
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'BeforeToolSelection', toolConfig: {mode: 'ANY', allowedFunctionNames: ['read_file', 'run_shell_command']}}}))\"";
// Write hook script to file (inline node -e has quoting issues on Windows)
const hookScript = `console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'BeforeToolSelection',
toolConfig: {
mode: 'ANY',
allowedFunctionNames: ['read_file', 'run_shell_command']
}
}
}));`;
const scriptPath = join(rig.testDir!, 'before_tool_selection_hook.cjs');
writeFileSync(scriptPath, hookScript);
rig.setup('should modify tool selection with BeforeToolSelection hooks', {
settings: {
@@ -500,7 +510,7 @@ console.log(JSON.stringify({
hooks: [
{
type: 'command',
command: hookCommand,
command: `node "${scriptPath.replace(/\\/g, '/')}"`,
timeout: 5000,
},
],
+4
View File
@@ -28,6 +28,10 @@ class MockConfig {
return true;
}
getFileFilteringRespectGitIgnore() {
return true;
}
getFileFilteringRespectGeminiIgnore() {
return true;
}
+7 -1
View File
@@ -564,11 +564,17 @@ describe('run_shell_command', () => {
it('rejects invalid shell expressions', async () => {
await rig.setup('rejects invalid shell expressions', {
settings: { tools: { core: ['run_shell_command'] } },
settings: {
tools: {
core: ['run_shell_command'],
allowed: ['run_shell_command(echo)'], // Specifically allow echo
},
},
});
const invalidCommand = getInvalidCommand();
const result = await rig.run({
args: `I am testing the error handling of the run_shell_command tool. Please attempt to run the following command, which I know has invalid syntax: \`${invalidCommand}\`. If the command fails as expected, please return the word FAIL, otherwise return the word SUCCESS.`,
approvalMode: 'default', // Use default mode so safety fallback triggers confirmation
});
expect(result).toContain('FAIL');
@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I could not find the file `nonexistent-file-that-does-not-exist.txt` in the current directory or its subdirectories. Please verify the file path or name."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":25,"totalTokenCount":35,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10}]}}]}
@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hello! How can I help you today?"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":9,"totalTokenCount":14,"promptTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
@@ -0,0 +1,62 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { join } from 'node:path';
import { TestRig } from './test-helper.js';
describe('stdout-stderr-output', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should send model response to stdout and app messages to stderr', async ({
signal,
}) => {
await rig.setup('prompt-output-test', {
fakeResponsesPath: join(
import.meta.dirname,
'stdout-stderr-output.responses',
),
});
const { stdout, exitCode } = await rig.runWithStreams(['-p', 'Say hello'], {
signal,
});
expect(exitCode).toBe(0);
expect(stdout.toLowerCase()).toContain('hello');
expect(stdout).not.toMatch(/^\[ERROR\]/m);
expect(stdout).not.toMatch(/^\[INFO\]/m);
});
it('should handle missing file with message to stdout and error to stderr', async ({
signal,
}) => {
await rig.setup('error-output-test', {
fakeResponsesPath: join(
import.meta.dirname,
'stdout-stderr-output-error.responses',
),
});
const { stdout, exitCode } = await rig.runWithStreams(
['-p', '@nonexistent-file-that-does-not-exist.txt explain this'],
{ signal },
);
expect(exitCode).toBe(0);
expect(stdout.toLowerCase()).toMatch(
/could not find|not exist|does not exist/,
);
});
});
+8 -31
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"workspaces": [
"packages/*"
],
@@ -2251,7 +2251,6 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2432,7 +2431,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2466,7 +2464,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2835,7 +2832,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2869,7 +2865,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/resources": "2.0.1"
@@ -2922,7 +2917,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/resources": "2.0.1",
@@ -4128,7 +4122,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4406,7 +4399,6 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -5399,7 +5391,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8409,7 +8400,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8950,7 +8940,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -10552,7 +10541,6 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -14311,7 +14299,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14322,7 +14309,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -16559,7 +16545,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16783,8 +16768,7 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -16792,7 +16776,6 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -16965,7 +16948,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17173,7 +17155,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -17287,7 +17268,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -17300,7 +17280,6 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -18005,7 +17984,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -18021,7 +17999,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
@@ -18077,7 +18055,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -18164,7 +18142,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
@@ -18300,7 +18278,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -18323,7 +18300,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18340,7 +18317,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-nightly.20260121.97aac696f",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -26,7 +26,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -22,6 +22,7 @@ import {
type ExtensionSetting,
} from '../../config/extensions/extensionSettings.js';
import prompts from 'prompts';
import * as fs from 'node:fs';
const {
mockExtensionManager,
@@ -79,11 +80,15 @@ vi.mock('../../config/settings.js', () => ({
}));
describe('extensions configure command', () => {
let tempWorkspaceDir: string;
beforeEach(() => {
vi.spyOn(debugLogger, 'log');
vi.spyOn(debugLogger, 'error');
vi.clearAllMocks();
tempWorkspaceDir = fs.mkdtempSync('gemini-cli-test-workspace');
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
// Default behaviors
mockLoadSettings.mockReturnValue({ merged: {} });
mockGetExtensionAndManager.mockResolvedValue({
@@ -141,6 +146,7 @@ describe('extensions configure command', () => {
'TEST_VAR',
promptForSetting,
'user',
tempWorkspaceDir,
);
});
@@ -186,6 +192,7 @@ describe('extensions configure command', () => {
'VAR_1',
promptForSetting,
'user',
tempWorkspaceDir,
);
});
@@ -111,6 +111,7 @@ async function configureSpecificSetting(
settingKey,
promptForSetting,
scope,
process.cwd(),
);
}
@@ -218,6 +219,7 @@ async function configureExtensionSettings(
setting.envVar,
promptForSetting,
scope,
process.cwd(),
);
}
}
@@ -0,0 +1,28 @@
# Themes Example
This is an example of a Gemini CLI extension that adds a custom theme.
## How to use
1. Link this extension:
```bash
gemini extensions link packages/cli/src/commands/extensions/examples/themes-example
```
2. Set the theme in your settings file (`~/.gemini/config.yaml`):
```yaml
ui:
theme: 'shades-of-green-theme (themes-example)'
```
Alternatively, you can set it through the UI by running `gemini` and then
typing `/theme` and pressing Enter.
3. **Observe the Changes:**
After setting the theme, you should see the changes reflected in the Gemini
CLI's UI. The background will be a dark green, the primary text a lighter
green, and various other UI elements will display different shades of green,
as defined in this extension's `gemini-extension.json` file.
@@ -0,0 +1,29 @@
{
"name": "themes-example",
"version": "1.0.0",
"themes": [
{
"name": "shades-of-green-theme",
"type": "custom",
"background": {
"primary": "#1a362a"
},
"text": {
"primary": "#a6e3a1",
"secondary": "#6e8e7a",
"link": "#89e689"
},
"status": {
"success": "#76c076",
"warning": "#d9e689",
"error": "#b34e4e"
},
"border": {
"default": "#4a6c5a"
},
"ui": {
"comment": "#6e8e7a"
}
}
]
}
@@ -24,6 +24,7 @@ import { ExtensionUpdateState } from '../../ui/state/extensions.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const emitFeedback = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
@@ -40,6 +41,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
...actual,
coreEvents: {
emitConsoleLog,
emitFeedback,
},
debugLogger,
};
@@ -84,6 +86,42 @@ describe('extensions update command', () => {
});
describe('handleUpdate', () => {
it('should list installed extensions when requested extension is not found', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
const extensions = [
{ name: 'ext1', version: '1.0.0' },
{ name: 'ext2', version: '2.0.0' },
];
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue(extensions);
await handleUpdate({ name: 'missing-extension' });
expect(emitFeedback).toHaveBeenCalledWith(
'error',
'Extension "missing-extension" not found.\n\nInstalled extensions:\next1 (1.0.0)\next2 (2.0.0)\n\nRun "gemini extensions list" for details.',
);
expect(mockUpdateExtension).not.toHaveBeenCalled();
mockCwd.mockRestore();
});
it('should log a helpful message when no extensions are installed and requested extension is not found', async () => {
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
mockExtensionManager.prototype.loadExtensions = vi
.fn()
.mockResolvedValue([]);
await handleUpdate({ name: 'missing-extension' });
expect(emitFeedback).toHaveBeenCalledWith(
'error',
'Extension "missing-extension" not found.\n\nNo extensions installed.',
);
expect(mockUpdateExtension).not.toHaveBeenCalled();
mockCwd.mockRestore();
});
it.each([
{
state: ExtensionUpdateState.UPDATE_AVAILABLE,
+16 -3
View File
@@ -14,7 +14,7 @@ import {
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
import { getErrorMessage } from '../../utils/errors.js';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
import { debugLogger } from '@google/gemini-cli-core';
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { loadSettings } from '../../config/settings.js';
@@ -46,7 +46,21 @@ export async function handleUpdate(args: UpdateArgs) {
(extension) => extension.name === args.name,
);
if (!extension) {
debugLogger.log(`Extension "${args.name}" not found.`);
if (extensions.length === 0) {
coreEvents.emitFeedback(
'error',
`Extension "${args.name}" not found.\n\nNo extensions installed.`,
);
return;
}
const installedExtensions = extensions
.map((extension) => `${extension.name} (${extension.version})`)
.join('\n');
coreEvents.emitFeedback(
'error',
`Extension "${args.name}" not found.\n\nInstalled extensions:\n${installedExtensions}\n\nRun "gemini extensions list" for details.`,
);
return;
}
if (!extension.installMetadata) {
@@ -63,7 +77,6 @@ export async function handleUpdate(args: UpdateArgs) {
debugLogger.log(`Extension "${args.name}" is already up to date.`);
return;
}
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
const updatedExtensionInfo = (await updateExtension(
extension,
extensionManager,
+7
View File
@@ -128,6 +128,13 @@ async function addMcpServer(
settings.setValue(settingsScope, 'mcpServers', mcpServers);
if (transport === 'stdio') {
debugLogger.warn(
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
);
}
if (isExistingServer) {
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
} else {
+1 -1
View File
@@ -8,7 +8,7 @@ import { AuthType } from '@google/gemini-cli-core';
import { loadEnvironment, loadSettings } from './settings.js';
export function validateAuthMethod(authMethod: string): string | null {
loadEnvironment(loadSettings().merged);
loadEnvironment(loadSettings().merged, process.cwd());
if (
authMethod === AuthType.LOGIN_WITH_GOOGLE ||
authMethod === AuthType.COMPUTE_ADC
+7 -7
View File
@@ -588,7 +588,7 @@ describe('parseArguments', () => {
process.argv = ['node', 'script.js', 'skills', 'list'];
// Skills command enabled by default or via experimental
const settings = createTestMergedSettings({
experimental: { skills: true },
skills: { enabled: true },
});
const argv = await parseArguments(settings);
expect(argv.isCommand).toBe(true);
@@ -596,9 +596,9 @@ describe('parseArguments', () => {
it('should set isCommand to true for hooks command', async () => {
process.argv = ['node', 'script.js', 'hooks', 'migrate'];
// Hooks command enabled via tools settings
// Hooks command enabled via hooksConfig settings
const settings = createTestMergedSettings({
tools: { enableHooks: true },
hooksConfig: { enabled: true },
});
const argv = await parseArguments(settings);
expect(argv.isCommand).toBe(true);
@@ -1255,7 +1255,7 @@ describe('Approval mode tool exclusion logic', () => {
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode since it is disabled by your admin',
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
});
@@ -2928,7 +2928,7 @@ describe('loadCliConfig disableYoloMode', () => {
security: { disableYoloMode: true },
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode since it is disabled by your admin',
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
});
});
@@ -2960,7 +2960,7 @@ describe('loadCliConfig secureModeEnabled', () => {
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode since it is disabled by your admin',
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
});
@@ -2974,7 +2974,7 @@ describe('loadCliConfig secureModeEnabled', () => {
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Cannot start in YOLO mode since it is disabled by your admin',
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
});
+9 -10
View File
@@ -38,6 +38,7 @@ import {
type OutputFormat,
coreEvents,
GEMINI_MODEL_ALIAS_AUTO,
getAdminErrorMessage,
} from '@google/gemini-cli-core';
import {
type Settings,
@@ -304,11 +305,11 @@ export async function parseArguments(
yargsInstance.command(extensionsCommand);
}
if (settings.experimental?.skills || (settings.skills?.enabled ?? true)) {
if (settings.skills?.enabled ?? true) {
yargsInstance.command(skillsCommand);
}
// Register hooks command if hooks are enabled
if (settings.tools?.enableHooks) {
if (settings.hooksConfig.enabled) {
yargsInstance.command(hooksCommand);
}
@@ -432,7 +433,7 @@ export async function loadCliConfig(
const ideMode = settings.ide?.enabled ?? false;
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? false;
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
// Set the context filename in the server's memoryTool module BEFORE loading memory
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
@@ -550,7 +551,7 @@ export async function loadCliConfig(
);
}
throw new FatalConfigError(
'Cannot start in YOLO mode since it is disabled by your admin',
getAdminErrorMessage('YOLO mode', undefined /* config */),
);
}
} else if (approvalMode === ApprovalMode.YOLO) {
@@ -755,13 +756,13 @@ export async function loadCliConfig(
plan: settings.experimental?.plan,
enableEventDrivenScheduler:
settings.experimental?.enableEventDrivenScheduler,
skillsSupport:
settings.experimental?.skills || (settings.skills?.enabled ?? true),
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
disableLoopDetection: settings.model?.disableLoopDetection,
compressionThreshold: settings.model?.compressionThreshold,
folderTrust,
interactive,
@@ -791,10 +792,8 @@ export async function loadCliConfig(
acceptRawOutputRisk: argv.acceptRawOutputRisk,
modelConfigServiceConfig: settings.modelConfigs,
// TODO: loading of hooks based on workspace trust
enableHooks:
(settings.tools?.enableHooks ?? true) &&
(settings.hooksConfig?.enabled ?? true),
enableHooksUI: settings.tools?.enableHooks ?? true,
enableHooks: settings.hooksConfig.enabled,
enableHooksUI: settings.hooksConfig.enabled,
hooks: settings.hooks || {},
disabledHooks: settings.hooksConfig?.disabled || [],
projectHooks: projectHooks || {},
@@ -227,7 +227,6 @@ System using model: \${MODEL_NAME}
settings: createTestMergedSettings({
telemetry: { enabled: false },
experimental: { extensionConfig: true },
tools: { enableHooks: true },
hooksConfig: { enabled: true },
}),
requestConsent: vi.fn().mockResolvedValue(true),
@@ -306,6 +305,7 @@ System using model: \${MODEL_NAME}
'MY_VALUE',
mockRequestSetting,
ExtensionSettingScope.USER,
process.cwd(),
);
await extensionManager.restartExtension(extension);
@@ -0,0 +1,225 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
beforeAll,
afterAll,
beforeEach,
describe,
expect,
it,
vi,
afterEach,
} from 'vitest';
import { createExtension } from '../test-utils/createExtension.js';
import { ExtensionManager } from './extension-manager.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { GEMINI_DIR, type Config } from '@google/gemini-cli-core';
import { createTestMergedSettings, SettingScope } from './settings.js';
describe('ExtensionManager theme loading', () => {
let extensionManager: ExtensionManager;
let userExtensionsDir: string;
let tempHomeDir: string;
beforeAll(async () => {
tempHomeDir = await fs.promises.mkdtemp(
path.join(fs.realpathSync('/tmp'), 'gemini-cli-test-'),
);
});
afterAll(async () => {
if (tempHomeDir) {
await fs.promises.rm(tempHomeDir, { recursive: true, force: true });
}
});
beforeEach(() => {
process.env['GEMINI_CLI_HOME'] = tempHomeDir;
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
// Ensure userExtensionsDir is clean for each test
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
fs.mkdirSync(userExtensionsDir, { recursive: true });
extensionManager = new ExtensionManager({
settings: createTestMergedSettings({
experimental: { extensionConfig: true },
security: { blockGitExtensions: false },
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
}),
requestConsent: async () => true,
requestSetting: async () => '',
workspaceDir: tempHomeDir,
enabledExtensionOverrides: [],
});
vi.clearAllMocks();
themeManager.clearExtensionThemes();
themeManager.loadCustomThemes({});
themeManager.setActiveTheme(DEFAULT_THEME.name);
});
afterEach(() => {
delete process.env['GEMINI_CLI_HOME'];
});
it('should register themes from an extension when started', async () => {
const registerSpy = vi.spyOn(themeManager, 'registerExtensionThemes');
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-theme-extension',
themes: [
{
name: 'My-Awesome-Theme',
type: 'custom',
text: {
primary: '#FF00FF',
},
},
],
});
await extensionManager.loadExtensions();
const mockConfig = {
getEnableExtensionReloading: () => false,
getMcpClientManager: () => ({
startExtension: vi.fn().mockResolvedValue(undefined),
}),
getGeminiClient: () => ({
isInitialized: () => false,
updateSystemInstruction: vi.fn(),
setTools: vi.fn(),
}),
getHookSystem: () => undefined,
getWorkingDir: () => tempHomeDir,
shouldLoadMemoryFromIncludeDirectories: () => false,
getDebugMode: () => false,
getFileExclusions: () => ({
isIgnored: () => false,
}),
getGeminiMdFilePaths: () => [],
getMcpServers: () => ({}),
getAllowedMcpServers: () => [],
getSanitizationConfig: () => ({
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
}),
getShellExecutionConfig: () => ({
terminalWidth: 80,
terminalHeight: 24,
showColor: false,
pager: 'cat',
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
}),
getToolRegistry: () => ({
getTools: () => [],
}),
getProxy: () => undefined,
getFileService: () => ({
findFiles: async () => [],
}),
getExtensionLoader: () => ({
getExtensions: () => [],
}),
isTrustedFolder: () => true,
getImportFormat: () => 'tree',
} as unknown as Config;
await extensionManager.start(mockConfig);
expect(registerSpy).toHaveBeenCalledWith('my-theme-extension', [
{
name: 'My-Awesome-Theme',
type: 'custom',
text: {
primary: '#FF00FF',
},
},
]);
});
it('should revert to default theme when extension is stopped', async () => {
const extensionName = 'my-theme-extension';
const themeName = 'My-Awesome-Theme';
const namespacedThemeName = `${themeName} (${extensionName})`;
createExtension({
extensionsDir: userExtensionsDir,
name: extensionName,
themes: [
{
name: themeName,
type: 'custom',
text: {
primary: '#FF00FF',
},
},
],
});
await extensionManager.loadExtensions();
const mockConfig = {
getWorkingDir: () => tempHomeDir,
shouldLoadMemoryFromIncludeDirectories: () => false,
getWorkspaceContext: () => ({
getDirectories: () => [],
}),
getDebugMode: () => false,
getFileService: () => ({
findFiles: async () => [],
}),
getExtensionLoader: () => ({
getExtensions: () => [],
}),
isTrustedFolder: () => true,
getImportFormat: () => 'tree',
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
getDiscoveryMaxDirs: () => 200,
getMcpClientManager: () => ({
getMcpInstructions: () => '',
startExtension: vi.fn().mockResolvedValue(undefined),
stopExtension: vi.fn().mockResolvedValue(undefined),
}),
setUserMemory: vi.fn(),
setGeminiMdFileCount: vi.fn(),
setGeminiMdFilePaths: vi.fn(),
getEnableExtensionReloading: () => true,
getGeminiClient: () => ({
isInitialized: () => false,
updateSystemInstruction: vi.fn(),
setTools: vi.fn(),
}),
getHookSystem: () => undefined,
getProxy: () => undefined,
getAgentRegistry: () => ({
reload: vi.fn().mockResolvedValue(undefined),
}),
} as unknown as Config;
await extensionManager.start(mockConfig);
// Set the active theme to the one from the extension
themeManager.setActiveTheme(namespacedThemeName);
expect(themeManager.getActiveTheme().name).toBe(namespacedThemeName);
// Stop the extension
await extensionManager.disableExtension(extensionName, SettingScope.User);
// Check that the active theme has reverted to the default
expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name);
});
});
+67 -4
View File
@@ -69,6 +69,7 @@ import {
ExtensionSettingScope,
} from './extensions/extensionSettings.js';
import type { EventEmitter } from 'node:stream';
import { themeManager } from '../ui/themes/theme-manager.js';
interface ExtensionManagerParams {
enabledExtensionOverrides?: string[];
@@ -143,6 +144,26 @@ export class ExtensionManager extends ExtensionLoader {
previousExtensionConfig?: ExtensionConfig,
): Promise<GeminiCLIExtension> {
if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(installMetadata.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
);
}
},
);
if (!extensionAllowed) {
throw new Error(
`Installing extension from source "${installMetadata.source}" is not allowed by the "allowedExtensions" security setting.`,
);
}
} else if (
(installMetadata.type === 'git' ||
installMetadata.type === 'github-release') &&
this.settings.security.blockGitExtensions
@@ -151,6 +172,7 @@ export class ExtensionManager extends ExtensionLoader {
'Installing extensions from remote sources is disallowed by your current settings.',
);
}
const isUpdate = !!previousExtensionConfig;
let newExtensionConfig: ExtensionConfig | null = null;
let localSourcePath: string | undefined;
@@ -468,6 +490,20 @@ Would you like to attempt to install via "git clone" instead?`,
);
}
protected override async startExtension(extension: GeminiCLIExtension) {
await super.startExtension(extension);
if (extension.themes) {
themeManager.registerExtensionThemes(extension.name, extension.themes);
}
}
protected override async stopExtension(extension: GeminiCLIExtension) {
await super.stopExtension(extension);
if (extension.themes) {
themeManager.unregisterExtensionThemes(extension.name, extension.themes);
}
}
/**
* Loads all installed extensions, should only be called once.
*/
@@ -507,10 +543,39 @@ Would you like to attempt to install via "git clone" instead?`,
const installMetadata = loadInstallMetadata(extensionDir);
let effectiveExtensionPath = extensionDir;
if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
if (!installMetadata?.source) {
throw new Error(
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
);
}
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(installMetadata?.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
);
}
},
);
if (!extensionAllowed) {
debugLogger.warn(
`Failed to load extension ${extensionDir}. This extension is not allowed by the "allowedExtensions" security setting.`,
);
return null;
}
} else if (
(installMetadata?.type === 'git' ||
installMetadata?.type === 'github-release') &&
this.settings.security.blockGitExtensions
) {
debugLogger.warn(
`Failed to load extension ${extensionDir}. Extensions from remote sources is disallowed by your current settings.`,
);
return null;
}
@@ -624,10 +689,7 @@ Would you like to attempt to install via "git clone" instead?`,
};
let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
if (
this.settings.tools.enableHooks &&
this.settings.hooksConfig.enabled
) {
if (this.settings.hooksConfig.enabled) {
hooks = await this.loadExtensionHooks(
effectiveExtensionPath,
hydrationContext,
@@ -698,6 +760,7 @@ Would you like to attempt to install via "git clone" instead?`,
resolvedSettings,
skills,
agents: agentLoadResult.agents,
themes: config.themes,
};
this.loadedExtensions = [...this.loadedExtensions, extension];
+92
View File
@@ -622,6 +622,7 @@ describe('extension tests', () => {
});
it('should not load github extensions if blockGitExtensions is set', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-ext',
@@ -645,6 +646,73 @@ describe('extension tests', () => {
const extension = extensions.find((e) => e.name === 'my-ext');
expect(extension).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Extensions from remote sources is disallowed by your current settings.',
),
);
consoleSpy.mockRestore();
});
it('should load allowed extensions if the allowlist is set.', async () => {
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-ext',
version: '1.0.0',
installMetadata: {
type: 'git',
source: 'http://allowed.com/foo/bar',
},
});
const extensionAllowlistSetting = createTestMergedSettings({
security: {
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
},
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: extensionAllowlistSetting,
});
const extensions = await extensionManager.loadExtensions();
expect(extensions).toHaveLength(1);
expect(extensions[0].name).toBe('my-ext');
});
it('should not load disallowed extensions if the allowlist is set.', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
name: 'my-ext',
version: '1.0.0',
installMetadata: {
type: 'git',
source: 'http://notallowed.com/foo/bar',
},
});
const extensionAllowlistSetting = createTestMergedSettings({
security: {
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
},
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: extensionAllowlistSetting,
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'my-ext');
expect(extension).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'This extension is not allowed by the "allowedExtensions" security setting',
),
);
consoleSpy.mockRestore();
});
it('should not load any extensions if admin.extensions.enabled is false', async () => {
@@ -1116,6 +1184,30 @@ describe('extension tests', () => {
);
});
it('should not install a disallowed extension if the allowlist is set', async () => {
const gitUrl = 'https://somehost.com/somerepo.git';
const allowedExtensionsSetting = createTestMergedSettings({
security: {
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
},
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: allowedExtensionsSetting,
});
await extensionManager.loadExtensions();
await expect(
extensionManager.installOrUpdateExtension({
source: gitUrl,
type: 'git',
}),
).rejects.toThrow(
`Installing extension from source "${gitUrl}" is not allowed by the "allowedExtensions" security setting.`,
);
});
it('should prompt for trust if workspace is not trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
+6
View File
@@ -7,6 +7,7 @@
import type {
MCPServerConfig,
ExtensionInstallMetadata,
CustomTheme,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -27,6 +28,11 @@ export interface ExtensionConfig {
contextFileName?: string | string[];
excludeTools?: string[];
settings?: ExtensionSetting[];
/**
* Custom themes contributed by this extension.
* These themes will be registered when the extension is activated.
*/
themes?: CustomTheme[];
}
export interface ExtensionUpdateInfo {
@@ -398,6 +398,35 @@ describe('extensionSettings', () => {
expect(actualContent).toBe('VAR1="a value with spaces"\n');
});
it('should not set sensitive settings if the value is empty during initial setup', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [
{
name: 's1',
description: 'd1',
envVar: 'SENSITIVE_VAR',
sensitive: true,
},
],
};
mockRequestSetting.mockResolvedValue('');
await maybePromptForSettings(
config,
'12345',
mockRequestSetting,
undefined,
undefined,
);
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
});
it('should not attempt to clear secrets if keychain is unavailable', async () => {
// Arrange
const mockIsAvailable = vi.fn().mockResolvedValue(false);
@@ -738,5 +767,42 @@ describe('extensionSettings', () => {
const lines = actualContent.split('\n').filter((line) => line.length > 0);
expect(lines).toHaveLength(3); // Should only have the three variables
});
it('should delete a sensitive setting if the new value is empty', async () => {
mockRequestSetting.mockResolvedValue('');
await updateSetting(
config,
'12345',
'VAR2',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
);
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
expect(await userKeychain.getSecret('VAR2')).toBeNull();
});
it('should not throw if deleting a non-existent sensitive setting with empty value', async () => {
mockRequestSetting.mockResolvedValue('');
// Ensure it doesn't exist first
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
await userKeychain.deleteSecret('VAR2');
await updateSetting(
config,
'12345',
'VAR2',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
);
// Should complete without error
});
});
});
@@ -112,7 +112,7 @@ export async function maybePromptForSettings(
const nonSensitiveSettings: Record<string, string> = {};
for (const setting of settings) {
const value = allSettings[setting.envVar];
if (value === undefined) {
if (value === undefined || value === '') {
continue;
}
if (setting.sensitive) {
@@ -207,7 +207,7 @@ export async function updateSetting(
settingKey: string,
requestSetting: (setting: ExtensionSetting) => Promise<string>,
scope: ExtensionSettingScope,
workspaceDir?: string,
workspaceDir: string,
): Promise<void> {
const { name: extensionName, settings } = extensionConfig;
if (!settings || settings.length === 0) {
@@ -230,7 +230,15 @@ export async function updateSetting(
);
if (settingToUpdate.sensitive) {
await keychain.setSecret(settingToUpdate.envVar, newValue);
if (newValue) {
await keychain.setSecret(settingToUpdate.envVar, newValue);
} else {
try {
await keychain.deleteSecret(settingToUpdate.envVar);
} catch {
// Ignore if secret does not exist
}
}
return;
}
+48 -3
View File
@@ -72,6 +72,15 @@ export enum Command {
OPEN_EXTERNAL_EDITOR = 'input.openExternalEditor',
PASTE_CLIPBOARD = 'input.paste',
BACKGROUND_SHELL_ESCAPE = 'backgroundShellEscape',
BACKGROUND_SHELL_SELECT = 'backgroundShellSelect',
TOGGLE_BACKGROUND_SHELL = 'toggleBackgroundShell',
TOGGLE_BACKGROUND_SHELL_LIST = 'toggleBackgroundShellList',
KILL_BACKGROUND_SHELL = 'backgroundShell.kill',
UNFOCUS_BACKGROUND_SHELL = 'backgroundShell.unfocus',
UNFOCUS_BACKGROUND_SHELL_LIST = 'backgroundShell.listUnfocus',
SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING = 'backgroundShell.unfocusWarning',
// App Controls
SHOW_ERROR_DETAILS = 'app.showErrorDetails',
SHOW_FULL_TODOS = 'app.showFullTodos',
@@ -85,6 +94,7 @@ export enum Command {
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
CLEAR_SCREEN = 'app.clearScreen',
RESTART_APP = 'app.restart',
SUSPEND_APP = 'app.suspend',
}
/**
@@ -138,7 +148,6 @@ export const defaultKeyBindings: KeyBindingConfig = {
],
[Command.MOVE_LEFT]: [
{ key: 'left', shift: false, alt: false, ctrl: false, cmd: false },
{ key: 'b', ctrl: true },
],
[Command.MOVE_RIGHT]: [
{ key: 'right', shift: false, alt: false, ctrl: false, cmd: false },
@@ -170,8 +179,15 @@ export const defaultKeyBindings: KeyBindingConfig = {
],
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
[Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
[Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
[Command.UNDO]: [
{ key: 'z', cmd: true, shift: false },
{ key: 'z', alt: true, shift: false },
],
[Command.REDO]: [
{ key: 'z', ctrl: true, shift: true },
{ key: 'z', cmd: true, shift: true },
{ key: 'z', alt: true, shift: true },
],
// Scrolling
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
@@ -257,6 +273,16 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
[Command.CYCLE_APPROVAL_MODE]: [{ key: 'tab', shift: true }],
[Command.TOGGLE_BACKGROUND_SHELL]: [{ key: 'b', ctrl: true }],
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: [{ key: 'l', ctrl: true }],
[Command.KILL_BACKGROUND_SHELL]: [{ key: 'k', ctrl: true }],
[Command.UNFOCUS_BACKGROUND_SHELL]: [{ key: 'tab', shift: true }],
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [{ key: 'tab', shift: false }],
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [
{ key: 'tab', shift: false },
],
[Command.BACKGROUND_SHELL_SELECT]: [{ key: 'return' }],
[Command.BACKGROUND_SHELL_ESCAPE]: [{ key: 'escape' }],
[Command.SHOW_MORE_LINES]: [
{ key: 'o', ctrl: true },
{ key: 's', ctrl: true },
@@ -265,6 +291,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
[Command.RESTART_APP]: [{ key: 'r' }],
[Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }],
};
interface CommandCategory {
@@ -370,10 +397,19 @@ export const commandCategories: readonly CommandCategory[] = [
Command.TOGGLE_YOLO,
Command.CYCLE_APPROVAL_MODE,
Command.SHOW_MORE_LINES,
Command.TOGGLE_BACKGROUND_SHELL,
Command.TOGGLE_BACKGROUND_SHELL_LIST,
Command.KILL_BACKGROUND_SHELL,
Command.BACKGROUND_SHELL_SELECT,
Command.BACKGROUND_SHELL_ESCAPE,
Command.UNFOCUS_BACKGROUND_SHELL,
Command.UNFOCUS_BACKGROUND_SHELL_LIST,
Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING,
Command.FOCUS_SHELL_INPUT,
Command.UNFOCUS_SHELL_INPUT,
Command.CLEAR_SCREEN,
Command.RESTART_APP,
Command.SUSPEND_APP,
],
},
];
@@ -460,8 +496,17 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).',
[Command.SHOW_MORE_LINES]:
'Expand a height-constrained response to show additional lines when not in alternate buffer mode.',
[Command.BACKGROUND_SHELL_SELECT]: 'Enter',
[Command.BACKGROUND_SHELL_ESCAPE]: 'Esc',
[Command.TOGGLE_BACKGROUND_SHELL]: 'Ctrl+B',
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: 'Ctrl+L',
[Command.KILL_BACKGROUND_SHELL]: 'Ctrl+K',
[Command.UNFOCUS_BACKGROUND_SHELL]: 'Shift+Tab',
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: 'Tab',
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: 'Tab',
[Command.FOCUS_SHELL_INPUT]: 'Focus the shell input from the gemini input.',
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
[Command.RESTART_APP]: 'Restart the application.',
[Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).',
};
@@ -164,7 +164,6 @@ describe('Policy Engine Integration Tests', () => {
it('should handle complex mixed configurations', async () => {
const settings: Settings = {
tools: {
autoAccept: true, // Allows read-only tools
allowed: ['custom-tool', 'my-server__special-tool'],
exclude: ['glob', 'dangerous-tool'],
},
@@ -438,7 +437,6 @@ describe('Policy Engine Integration Tests', () => {
it('should verify priority ordering works correctly in practice', async () => {
const settings: Settings = {
tools: {
autoAccept: true, // Priority 50
allowed: ['specific-tool'], // Priority 100
exclude: ['blocked-tool'], // Priority 200
},
@@ -614,7 +612,6 @@ describe('Policy Engine Integration Tests', () => {
it('should verify rules are created with correct priorities', async () => {
const settings: Settings = {
tools: {
autoAccept: true,
allowed: ['tool1', 'tool2'],
exclude: ['tool3'],
},
@@ -124,7 +124,6 @@ describe('settings-validation', () => {
},
tools: {
sandbox: 'inherit',
autoAccept: false,
},
};
+116 -33
View File
@@ -29,10 +29,11 @@ vi.mock('./settings.js', async (importActual) => {
});
// Mock trustedFolders
import * as trustedFolders from './trustedFolders.js';
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi
.fn()
.mockReturnValue({ isTrusted: true, source: 'file' }),
isWorkspaceTrusted: vi.fn(),
isFolderTrustEnabled: vi.fn(),
loadTrustedFolders: vi.fn(),
}));
vi.mock('./settingsSchema.js', async (importOriginal) => {
@@ -151,7 +152,7 @@ describe('Settings Loading and Merging', () => {
(mockFsExistsSync as Mock).mockReturnValue(false);
(fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON
(mockFsMkdirSync as Mock).mockImplementation(() => undefined);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({
isTrusted: true,
source: 'file',
});
@@ -1635,7 +1636,7 @@ describe('Settings Loading and Merging', () => {
});
it('should NOT merge workspace settings when workspace is not trusted', () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({
isTrusted: false,
source: 'file',
});
@@ -1666,23 +1667,60 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
expect(settings.merged.ui?.theme).toBe('dark'); // User setting
});
it('should NOT merge workspace settings when workspace trust is undefined', () => {
vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({
isTrusted: undefined,
source: undefined,
});
(mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = {
ui: { theme: 'dark' },
tools: { sandbox: false },
context: { fileName: 'USER.md' },
};
const workspaceSettingsContent = {
tools: { sandbox: true },
context: { fileName: 'WORKSPACE.md' },
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify(workspaceSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
});
});
describe('loadEnvironment', () => {
function setup({
isFolderTrustEnabled = true,
isWorkspaceTrustedValue = true,
isWorkspaceTrustedValue = true as boolean | undefined,
}) {
delete process.env['TESTTEST']; // reset
const geminiEnvPath = path.resolve(path.join(GEMINI_DIR, '.env'));
const geminiEnvPath = path.resolve(
path.join(MOCK_WORKSPACE_DIR, GEMINI_DIR, '.env'),
);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({
isTrusted: isWorkspaceTrustedValue,
source: 'file',
});
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
[USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()),
);
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => {
const normalizedP = path.resolve(p.toString());
return [path.resolve(USER_SETTINGS_PATH), geminiEnvPath].includes(
normalizedP,
);
});
const userSettingsContent: Settings = {
ui: {
theme: 'dark',
@@ -1698,9 +1736,10 @@ describe('Settings Loading and Merging', () => {
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
const normalizedP = path.resolve(p.toString());
if (normalizedP === path.resolve(USER_SETTINGS_PATH))
return JSON.stringify(userSettingsContent);
if (p === geminiEnvPath) return 'TESTTEST=1234';
if (normalizedP === geminiEnvPath) return 'TESTTEST=1234';
return '{}';
},
);
@@ -1708,14 +1747,34 @@ describe('Settings Loading and Merging', () => {
it('sets environment variables from .env files', () => {
setup({ isFolderTrustEnabled: false, isWorkspaceTrustedValue: true });
loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);
const settings = {
security: { folderTrust: { enabled: false } },
} as Settings;
loadEnvironment(settings, MOCK_WORKSPACE_DIR, isWorkspaceTrusted);
expect(process.env['TESTTEST']).toEqual('1234');
});
it('does not load env files from untrusted spaces', () => {
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);
const settings = {
security: { folderTrust: { enabled: true } },
} as Settings;
loadEnvironment(settings, MOCK_WORKSPACE_DIR, isWorkspaceTrusted);
expect(process.env['TESTTEST']).not.toEqual('1234');
});
it('does not load env files when trust is undefined', () => {
delete process.env['TESTTEST'];
// isWorkspaceTrusted returns {isTrusted: undefined} for matched rules with no trust value, or no matching rules.
setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: undefined });
const settings = {
security: { folderTrust: { enabled: true } },
} as Settings;
const mockTrustFn = vi.fn().mockReturnValue({ isTrusted: undefined });
loadEnvironment(settings, MOCK_WORKSPACE_DIR, mockTrustFn);
expect(process.env['TESTTEST']).not.toEqual('1234');
});
@@ -1731,7 +1790,7 @@ describe('Settings Loading and Merging', () => {
mockFsExistsSync.mockReturnValue(true);
mockFsReadFileSync = vi.mocked(fs.readFileSync);
mockFsReadFileSync.mockReturnValue('{}');
vi.mocked(isWorkspaceTrusted).mockReturnValue({
vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({
isTrusted: true,
source: undefined,
});
@@ -2156,7 +2215,7 @@ describe('Settings Loading and Merging', () => {
// 2. Now, set remote admin settings.
loadedSettings.setRemoteAdminSettings({
secureModeEnabled: true,
strictModeDisabled: false,
mcpSetting: { mcpEnabled: false },
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
});
@@ -2197,7 +2256,7 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
const newRemoteSettings = {
secureModeEnabled: true,
strictModeDisabled: false,
mcpSetting: { mcpEnabled: false },
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
};
@@ -2213,7 +2272,7 @@ describe('Settings Loading and Merging', () => {
// Verify that calling setRemoteAdminSettings with partial data overwrites previous remote settings
// and missing properties revert to schema defaults.
loadedSettings.setRemoteAdminSettings({ secureModeEnabled: false });
loadedSettings.setRemoteAdminSettings({ strictModeDisabled: true });
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false); // Defaulting to false if missing
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false); // Defaulting to false if missing
@@ -2271,9 +2330,9 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
// Set remote settings with only secureModeEnabled
// Set remote settings with only strictModeDisabled (false -> secureModeEnabled: true)
loadedSettings.setRemoteAdminSettings({
secureModeEnabled: true,
strictModeDisabled: false,
});
// Verify secureModeEnabled is updated, others default to false
@@ -2286,8 +2345,8 @@ describe('Settings Loading and Merging', () => {
mcpSetting: { mcpEnabled: false },
});
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled reverts to default:false)
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled defaults to true if strictModeDisabled is missing)
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
@@ -2297,23 +2356,47 @@ describe('Settings Loading and Merging', () => {
});
// Verify extensionsEnabled is updated, others remain defaults
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
// Verify that missing strictModeDisabled falls back to secureModeEnabled
loadedSettings.setRemoteAdminSettings({
secureModeEnabled: false,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
loadedSettings.setRemoteAdminSettings({
secureModeEnabled: true,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
// Verify strictModeDisabled takes precedence over secureModeEnabled
loadedSettings.setRemoteAdminSettings({
strictModeDisabled: false,
secureModeEnabled: false,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
loadedSettings.setRemoteAdminSettings({
strictModeDisabled: true,
secureModeEnabled: true,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
});
it('should set skills based on advancedFeaturesEnabled', () => {
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
const loadedSettings = loadSettings();
loadedSettings.setRemoteAdminSettings({
cliFeatureSetting: {
advancedFeaturesEnabled: true,
unmanagedCapabilitiesEnabled: true,
},
});
expect(loadedSettings.merged.admin.skills?.enabled).toBe(true);
loadedSettings.setRemoteAdminSettings({
cliFeatureSetting: {
advancedFeaturesEnabled: false,
unmanagedCapabilitiesEnabled: false,
},
});
expect(loadedSettings.merged.admin.skills?.enabled).toBe(false);
@@ -2337,12 +2420,12 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
});
it('should force secureModeEnabled to false if undefined, overriding schema defaults', () => {
// Mock schema to have secureModeEnabled default to true to verify the override
it('should force secureModeEnabled to true if undefined, overriding schema defaults', () => {
// Mock schema to have secureModeEnabled default to false to verify the override
const originalSchema = getSettingsSchema();
const modifiedSchema = JSON.parse(JSON.stringify(originalSchema));
if (modifiedSchema.admin?.properties?.secureModeEnabled) {
modifiedSchema.admin.properties.secureModeEnabled.default = true;
modifiedSchema.admin.properties.secureModeEnabled.default = false;
}
vi.mocked(getSettingsSchema).mockReturnValue(modifiedSchema);
@@ -2352,13 +2435,13 @@ describe('Settings Loading and Merging', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
// Pass a non-empty object that doesn't have secureModeEnabled
// Pass a non-empty object that doesn't have strictModeDisabled
loadedSettings.setRemoteAdminSettings({
mcpSetting: {},
});
// It should be forced to false by the logic, overriding the mock default of true
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
// It should be forced to true by the logic (default secure), overriding the mock default of false
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
} finally {
vi.mocked(getSettingsSchema).mockReturnValue(originalSchema);
}
+30 -12
View File
@@ -50,7 +50,9 @@ import {
formatValidationError,
} from './settings-validation.js';
function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
export function getMergeStrategyForPath(
path: string[],
): MergeStrategy | undefined {
let current: SettingDefinition | undefined = undefined;
let currentSchema: SettingsSchema | undefined = getSettingsSchema();
let parent: SettingDefinition | undefined = undefined;
@@ -348,7 +350,12 @@ export class LoadedSettings {
setRemoteAdminSettings(remoteSettings: FetchAdminControlsResponse): void {
const admin: Settings['admin'] = {};
const { secureModeEnabled, mcpSetting, cliFeatureSetting } = remoteSettings;
const {
secureModeEnabled,
strictModeDisabled,
mcpSetting,
cliFeatureSetting,
} = remoteSettings;
if (Object.keys(remoteSettings).length === 0) {
this._remoteAdminSettings = { admin };
@@ -356,15 +363,20 @@ export class LoadedSettings {
return;
}
admin.secureModeEnabled = secureModeEnabled ?? false;
if (strictModeDisabled !== undefined) {
admin.secureModeEnabled = !strictModeDisabled;
} else if (secureModeEnabled !== undefined) {
admin.secureModeEnabled = secureModeEnabled;
} else {
admin.secureModeEnabled = true;
}
admin.mcp = { enabled: mcpSetting?.mcpEnabled ?? false };
admin.extensions = {
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled ?? false,
};
if (cliFeatureSetting?.advancedFeaturesEnabled !== undefined) {
admin.skills = { enabled: cliFeatureSetting.advancedFeaturesEnabled };
}
admin.skills = {
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
};
this._remoteAdminSettings = { admin };
this._merged = this.computeMergedSettings();
@@ -422,10 +434,15 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void {
}
}
export function loadEnvironment(settings: Settings): void {
const envFilePath = findEnvFile(process.cwd());
export function loadEnvironment(
settings: Settings,
workspaceDir: string,
isWorkspaceTrustedFn = isWorkspaceTrusted,
): void {
const envFilePath = findEnvFile(workspaceDir);
const trustResult = isWorkspaceTrustedFn(settings, workspaceDir);
if (!isWorkspaceTrusted(settings).isTrusted) {
if (trustResult.isTrusted !== true) {
return;
}
@@ -590,7 +607,8 @@ export function loadSettings(
userSettings,
);
const isTrusted =
isWorkspaceTrusted(initialTrustCheckSettings as Settings).isTrusted ?? true;
isWorkspaceTrusted(initialTrustCheckSettings as Settings, workspaceDir)
.isTrusted ?? false;
// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings(
@@ -603,7 +621,7 @@ export function loadSettings(
// loadEnvironment depends on settings so we have to create a temp version of
// the settings to avoid a cycle
loadEnvironment(tempMergedSettings);
loadEnvironment(tempMergedSettings, workspaceDir);
// Check for any fatal errors before proceeding
const fatalErrors = settingsErrors.filter((e) => e.severity === 'error');
+59 -40
View File
@@ -9,19 +9,17 @@
// to regenerate the settings reference in `docs/get-started/configuration.md`.
// --------------------------------------------------------------------------
import type {
MCPServerConfig,
BugCommandSettings,
TelemetrySettings,
AuthType,
AgentOverride,
} from '@google/gemini-cli-core';
import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
DEFAULT_MODEL_CONFIGS,
type MCPServerConfig,
type BugCommandSettings,
type TelemetrySettings,
type AuthType,
type AgentOverride,
type CustomTheme,
} from '@google/gemini-cli-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import type { SessionRetentionSettings } from './settings.js';
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
@@ -353,6 +351,26 @@ const SETTINGS_SCHEMA = {
'The color theme for the UI. See the CLI themes guide for available options.',
showInDialog: false,
},
autoThemeSwitching: {
type: 'boolean',
label: 'Auto Theme Switching',
category: 'UI',
requiresRestart: false,
default: true,
description:
'Automatically switch between default light and dark themes based on terminal background color.',
showInDialog: true,
},
terminalBackgroundPollingInterval: {
type: 'number',
label: 'Terminal Background Polling Interval',
category: 'UI',
requiresRestart: false,
default: 60,
description:
'Interval in seconds to poll the terminal background color.',
showInDialog: true,
},
customThemes: {
type: 'object',
label: 'Custom Themes',
@@ -526,6 +544,16 @@ const SETTINGS_SCHEMA = {
description: 'Show the model name in the chat for each model turn.',
showInDialog: true,
},
showUserIdentity: {
type: 'boolean',
label: 'Show User Identity',
category: 'UI',
requiresRestart: false,
default: true,
description:
"Show the logged-in user's identity (e.g. email) in the UI.",
showInDialog: true,
},
useAlternateBuffer: {
type: 'boolean',
label: 'Use Alternate Screen Buffer',
@@ -731,6 +759,16 @@ const SETTINGS_SCHEMA = {
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
showInDialog: true,
},
disableLoopDetection: {
type: 'boolean',
label: 'Disable Loop Detection',
category: 'Model',
requiresRestart: true,
default: false,
description:
'Disable automatic detection and prevention of infinite loops.',
showInDialog: true,
},
skipNextSpeakerCheck: {
type: 'boolean',
label: 'Skip Next Speaker Check',
@@ -1033,17 +1071,6 @@ const SETTINGS_SCHEMA = {
},
},
},
autoAccept: {
type: 'boolean',
label: 'Auto Accept',
category: 'Tools',
requiresRestart: false,
default: false,
description: oneLine`
Automatically accept and execute tool calls that are considered safe (e.g., read-only operations).
`,
showInDialog: true,
},
approvalMode: {
type: 'enum',
label: 'Approval Mode',
@@ -1171,16 +1198,6 @@ const SETTINGS_SCHEMA = {
`,
showInDialog: true,
},
enableHooks: {
type: 'boolean',
label: 'Enable Hooks System (Experimental)',
category: 'Advanced',
requiresRestart: true,
default: true,
description:
'Enables the hooks system experiment. When disabled, the hooks system is completely deactivated regardless of other settings.',
showInDialog: false,
},
},
},
@@ -1270,6 +1287,17 @@ const SETTINGS_SCHEMA = {
description: 'Blocks installing and loading extensions from Git.',
showInDialog: true,
},
allowedExtensions: {
type: 'array',
label: 'Extension Source Regex Allowlist',
category: 'Security',
requiresRestart: true,
default: [] as string[],
description:
'List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.',
showInDialog: true,
items: { type: 'string' },
},
folderTrust: {
type: 'object',
label: 'Folder Trust',
@@ -1490,15 +1518,6 @@ const SETTINGS_SCHEMA = {
description: 'Enable Just-In-Time (JIT) context loading.',
showInDialog: false,
},
skills: {
type: 'boolean',
label: 'Agent Skills (Deprecated)',
category: 'Experimental',
requiresRestart: true,
default: false,
description: '[Deprecated] Enable Agent Skills (experimental).',
showInDialog: false,
},
useOSC52Paste: {
type: 'boolean',
label: 'Use OSC 52 Paste',
@@ -1602,11 +1621,11 @@ const SETTINGS_SCHEMA = {
type: 'boolean',
label: 'Enable Hooks',
category: 'Advanced',
requiresRestart: false,
requiresRestart: true,
default: true,
description:
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
showInDialog: false,
showInDialog: true,
},
disabled: {
type: 'array',
@@ -1,158 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { loadCliConfig, parseArguments } from './config.js';
import * as trustedFolders from './trustedFolders.js';
import { loadServerHierarchicalMemory } from '@google/gemini-cli-core';
import { type Settings, createTestMergedSettings } from './settings.js';
vi.mock('./trustedFolders.js');
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
loadServerHierarchicalMemory: vi.fn(),
getPty: vi.fn().mockResolvedValue({ name: 'test-pty' }),
getVersion: vi.fn().mockResolvedValue('0.0.0-test'),
};
});
describe('Agent Skills Backward Compatibility', () => {
const originalArgv = process.argv;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(trustedFolders.isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
} as unknown as trustedFolders.TrustResult);
});
afterEach(() => {
process.argv = originalArgv;
});
describe('loadCliConfig', () => {
it('should default skillsSupport to true when no settings are present', async () => {
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
process.argv = ['node', 'gemini'];
const settings = createTestMergedSettings({});
const config = await loadCliConfig(
settings,
'test-session',
await parseArguments(settings),
);
expect(
(
config as unknown as { isSkillsSupportEnabled: () => boolean }
).isSkillsSupportEnabled(),
).toBe(true);
});
it('should prioritize skills.enabled=false from settings', async () => {
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
const settings = createTestMergedSettings({
skills: { enabled: false },
} as unknown as Settings);
process.argv = ['node', 'gemini'];
const config = await loadCliConfig(
settings,
'test-session',
await parseArguments(settings),
);
expect(
(
config as unknown as { isSkillsSupportEnabled: () => boolean }
).isSkillsSupportEnabled(),
).toBe(false);
});
it('should support legacy experimental.skills=true from settings', async () => {
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
const settings = createTestMergedSettings({
experimental: { skills: true },
} as unknown as Settings);
process.argv = ['node', 'gemini'];
const config = await loadCliConfig(
settings,
'test-session',
await parseArguments(settings),
);
expect(
(
config as unknown as { isSkillsSupportEnabled: () => boolean }
).isSkillsSupportEnabled(),
).toBe(true);
});
it('should prioritize legacy experimental.skills=true over new skills.enabled=false', async () => {
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
const settings = createTestMergedSettings({
skills: { enabled: false },
experimental: { skills: true },
} as unknown as Settings);
process.argv = ['node', 'gemini'];
const config = await loadCliConfig(
settings,
'test-session',
await parseArguments(settings),
);
expect(
(
config as unknown as { isSkillsSupportEnabled: () => boolean }
).isSkillsSupportEnabled(),
).toBe(true);
});
it('should still be enabled by default if legacy experimental.skills is false (since new default is true)', async () => {
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
const settings = createTestMergedSettings({
experimental: { skills: false },
} as unknown as Settings);
process.argv = ['node', 'gemini'];
const config = await loadCliConfig(
settings,
'test-session',
await parseArguments(settings),
);
expect(
(
config as unknown as { isSkillsSupportEnabled: () => boolean }
).isSkillsSupportEnabled(),
).toBe(true);
});
});
});
@@ -326,6 +326,19 @@ describe('isWorkspaceTrusted', () => {
});
});
it('should use workspaceDir instead of process.cwd() when provided', () => {
mockCwd = '/home/user/untrusted';
const workspaceDir = '/home/user/projectA';
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
// process.cwd() is untrusted, but workspaceDir is trusted
expect(isWorkspaceTrusted(mockSettings, workspaceDir)).toEqual({
isTrusted: true,
source: 'file',
});
});
it('should handle path normalization', () => {
mockCwd = '/home/user/projectA';
mockRules[`/home/user/../user/${path.basename('/home/user/projectA')}`] =
+4 -2
View File
@@ -227,6 +227,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
}
function getWorkspaceTrustFromLocalConfig(
workspaceDir: string,
trustConfig?: Record<string, TrustLevel>,
): TrustResult {
const folders = loadTrustedFolders();
@@ -241,7 +242,7 @@ function getWorkspaceTrustFromLocalConfig(
);
}
const isTrusted = folders.isPathTrusted(process.cwd(), configToUse);
const isTrusted = folders.isPathTrusted(workspaceDir, configToUse);
return {
isTrusted,
source: isTrusted !== undefined ? 'file' : undefined,
@@ -250,6 +251,7 @@ function getWorkspaceTrustFromLocalConfig(
export function isWorkspaceTrusted(
settings: Settings,
workspaceDir: string = process.cwd(),
trustConfig?: Record<string, TrustLevel>,
): TrustResult {
if (!isFolderTrustEnabled(settings)) {
@@ -262,5 +264,5 @@ export function isWorkspaceTrusted(
}
// Fall back to the local user configuration
return getWorkspaceTrustFromLocalConfig(trustConfig);
return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig);
}
+17 -15
View File
@@ -16,11 +16,10 @@ import type { ArgumentsCamelCase, CommandModule } from 'yargs';
import type { MergedSettings } from './config/settings.js';
import type { MockInstance } from 'vitest';
const { mockRunExitCleanup, mockDebugLogger } = vi.hoisted(() => ({
const { mockRunExitCleanup, mockCoreEvents } = vi.hoisted(() => ({
mockRunExitCleanup: vi.fn(),
mockDebugLogger: {
log: vi.fn(),
error: vi.fn(),
mockCoreEvents: {
emitFeedback: vi.fn(),
},
}));
@@ -28,7 +27,7 @@ vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
return {
...actual,
debugLogger: mockDebugLogger,
coreEvents: mockCoreEvents,
};
});
@@ -55,8 +54,7 @@ describe('deferred', () => {
describe('runDeferredCommand', () => {
it('should do nothing if no deferred command is set', async () => {
await runDeferredCommand(createMockSettings());
expect(mockDebugLogger.log).not.toHaveBeenCalled();
expect(mockDebugLogger.error).not.toHaveBeenCalled();
expect(mockCoreEvents.emitFeedback).not.toHaveBeenCalled();
expect(mockExit).not.toHaveBeenCalled();
});
@@ -85,8 +83,9 @@ describe('deferred', () => {
const settings = createMockSettings({ mcp: { enabled: false } });
await runDeferredCommand(settings);
expect(mockDebugLogger.error).toHaveBeenCalledWith(
'Error: MCP is disabled by your admin.',
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'MCP is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
expect(mockRunExitCleanup).toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
@@ -102,8 +101,9 @@ describe('deferred', () => {
const settings = createMockSettings({ extensions: { enabled: false } });
await runDeferredCommand(settings);
expect(mockDebugLogger.error).toHaveBeenCalledWith(
'Error: Extensions are disabled by your admin.',
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Extensions is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
expect(mockRunExitCleanup).toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
@@ -119,8 +119,9 @@ describe('deferred', () => {
const settings = createMockSettings({ skills: { enabled: false } });
await runDeferredCommand(settings);
expect(mockDebugLogger.error).toHaveBeenCalledWith(
'Error: Agent skills are disabled by your admin.',
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
expect(mockRunExitCleanup).toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
@@ -183,8 +184,9 @@ describe('deferred', () => {
const mcpSettings = createMockSettings({ mcp: { enabled: false } });
await runDeferredCommand(mcpSettings);
expect(mockDebugLogger.error).toHaveBeenCalledWith(
'Error: MCP is disabled by your admin.',
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'MCP is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
);
});
+17 -4
View File
@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { ArgumentsCamelCase, CommandModule } from 'yargs';
import { debugLogger, ExitCodes } from '@google/gemini-cli-core';
import {
coreEvents,
ExitCodes,
getAdminErrorMessage,
} from '@google/gemini-cli-core';
import { runExitCleanup } from './utils/cleanup.js';
import type { MergedSettings } from './config/settings.js';
import process from 'node:process';
@@ -30,7 +34,10 @@ export async function runDeferredCommand(settings: MergedSettings) {
const commandName = deferredCommand.commandName;
if (commandName === 'mcp' && adminSettings?.mcp?.enabled === false) {
debugLogger.error('Error: MCP is disabled by your admin.');
coreEvents.emitFeedback(
'error',
getAdminErrorMessage('MCP', undefined /* config */),
);
await runExitCleanup();
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
}
@@ -39,13 +46,19 @@ export async function runDeferredCommand(settings: MergedSettings) {
commandName === 'extensions' &&
adminSettings?.extensions?.enabled === false
) {
debugLogger.error('Error: Extensions are disabled by your admin.');
coreEvents.emitFeedback(
'error',
getAdminErrorMessage('Extensions', undefined /* config */),
);
await runExitCleanup();
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
}
if (commandName === 'skills' && adminSettings?.skills?.enabled === false) {
debugLogger.error('Error: Agent skills are disabled by your admin.');
coreEvents.emitFeedback(
'error',
getAdminErrorMessage('Agent skills', undefined /* config */),
);
await runExitCleanup();
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
}
+25 -16
View File
@@ -32,6 +32,10 @@ import {
runExitCleanup,
registerTelemetryConfig,
} from './utils/cleanup.js';
import {
cleanupToolOutputFiles,
cleanupExpiredSessions,
} from './utils/sessionCleanup.js';
import {
type Config,
type ResumedSessionData,
@@ -71,7 +75,6 @@ import {
} from './core/initializer.js';
import { validateAuthMethod } from './config/auth.js';
import { runZedIntegration } from './zed-integration/zedIntegration.js';
import { cleanupExpiredSessions } from './utils/sessionCleanup.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
@@ -95,6 +98,7 @@ import { deleteSession, listSessions } from './utils/sessions.js';
import { createPolicyUpdater } from './config/policy.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { profiler } from './ui/components/DebugProfiler.js';
@@ -225,19 +229,21 @@ export async function startInteractiveUI(
settings.merged.general.debugKeystrokeLogging
}
>
<ScrollProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
initializationResult={initializationResult}
/>
</VimModeProvider>
</SessionStatsProvider>
</ScrollProvider>
<TerminalProvider>
<ScrollProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
initializationResult={initializationResult}
/>
</VimModeProvider>
</SessionStatsProvider>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</SettingsContext.Provider>
@@ -320,7 +326,10 @@ export async function main() {
);
});
await cleanupCheckpoints();
await Promise.all([
cleanupCheckpoints(),
cleanupToolOutputFiles(settings.merged),
]);
const parseArgsHandle = startupProfiler.start('parse_arguments');
const argv = await parseArguments(settings.merged);
@@ -367,7 +376,7 @@ export async function main() {
) {
settings.setValue(
SettingScope.User,
'selectedAuthType',
'security.auth.selectedType',
AuthType.COMPUTE_ADC,
);
}
@@ -38,6 +38,11 @@ import type { LoadedSettings } from './config/settings.js';
// Mock core modules
vi.mock('./ui/hooks/atCommandProcessor.js');
const mockRegisterActivityLogger = vi.hoisted(() => vi.fn());
vi.mock('./utils/activityLogger.js', () => ({
registerActivityLogger: mockRegisterActivityLogger,
}));
const mockCoreEvents = vi.hoisted(() => ({
on: vi.fn(),
off: vi.fn(),
@@ -253,12 +258,61 @@ describe('runNonInteractive', () => {
[{ text: 'Test input' }],
expect.any(AbortSignal),
'prompt-id-1',
undefined,
false,
'Test input',
);
expect(getWrittenOutput()).toBe('Hello World\n');
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
// so we no longer expect shutdownTelemetry to be called directly here
});
it('should register activity logger when GEMINI_CLI_ACTIVITY_LOG_FILE is set', async () => {
vi.stubEnv('GEMINI_CLI_ACTIVITY_LOG_FILE', '/tmp/test.jsonl');
const events: ServerGeminiStreamEvent[] = [
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
},
];
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents(events),
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-activity-logger',
});
expect(mockRegisterActivityLogger).toHaveBeenCalledWith(mockConfig);
vi.unstubAllEnvs();
});
it('should not register activity logger when GEMINI_CLI_ACTIVITY_LOG_FILE is not set', async () => {
vi.stubEnv('GEMINI_CLI_ACTIVITY_LOG_FILE', '');
const events: ServerGeminiStreamEvent[] = [
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
},
];
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents(events),
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-activity-logger-off',
});
expect(mockRegisterActivityLogger).not.toHaveBeenCalled();
vi.unstubAllEnvs();
});
it('should handle a single tool call and respond', async () => {
const toolCallEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallRequest,
@@ -323,6 +377,9 @@ describe('runNonInteractive', () => {
[{ text: 'Tool response' }],
expect.any(AbortSignal),
'prompt-id-2',
undefined,
false,
undefined,
);
expect(getWrittenOutput()).toBe('Final answer\n');
});
@@ -480,6 +537,9 @@ describe('runNonInteractive', () => {
],
expect.any(AbortSignal),
'prompt-id-3',
undefined,
false,
undefined,
);
expect(getWrittenOutput()).toBe('Sorry, let me try again.\n');
});
@@ -619,6 +679,9 @@ describe('runNonInteractive', () => {
processedParts,
expect.any(AbortSignal),
'prompt-id-7',
undefined,
false,
rawInput,
);
// 6. Assert the final output is correct
@@ -652,6 +715,9 @@ describe('runNonInteractive', () => {
[{ text: 'Test input' }],
expect.any(AbortSignal),
'prompt-id-1',
undefined,
false,
'Test input',
);
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify(
@@ -782,6 +848,9 @@ describe('runNonInteractive', () => {
[{ text: 'Empty response test' }],
expect.any(AbortSignal),
'prompt-id-empty',
undefined,
false,
'Empty response test',
);
// This should output JSON with empty response but include stats
@@ -916,6 +985,9 @@ describe('runNonInteractive', () => {
[{ text: 'Prompt from command' }],
expect.any(AbortSignal),
'prompt-id-slash',
undefined,
false,
'/testcommand',
);
expect(getWrittenOutput()).toBe('Response from command\n');
@@ -959,6 +1031,9 @@ describe('runNonInteractive', () => {
[{ text: 'Slash command output' }],
expect.any(AbortSignal),
'prompt-id-slash',
undefined,
false,
'/help',
);
expect(getWrittenOutput()).toBe('Response to slash command\n');
handleSlashCommandSpy.mockRestore();
@@ -1133,6 +1208,9 @@ describe('runNonInteractive', () => {
[{ text: '/unknowncommand' }],
expect.any(AbortSignal),
'prompt-id-unknown',
undefined,
false,
'/unknowncommand',
);
expect(getWrittenOutput()).toBe('Response to unknown\n');
+11
View File
@@ -70,6 +70,14 @@ export async function runNonInteractive({
coreEvents.emitConsoleLog(msg.type, msg.content);
},
});
if (config.storage && process.env['GEMINI_CLI_ACTIVITY_LOG_FILE']) {
const { registerActivityLogger } = await import(
'./utils/activityLogger.js'
);
registerActivityLogger(config);
}
const { stdout: workingStdout } = createWorkingStdio();
const textOutput = new TextOutput(workingStdout);
@@ -293,6 +301,9 @@ export async function runNonInteractive({
currentMessages[0]?.parts || [],
abortController.signal,
prompt_id,
undefined,
false,
turnCount === 1 ? input : undefined,
);
let responseText = '';
@@ -119,11 +119,12 @@ describe('BuiltinCommandLoader', () => {
getEnableHooks: () => false,
getEnableHooksUI: () => false,
getExtensionsEnabled: vi.fn().mockReturnValue(true),
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
isAgentsEnabled: vi.fn().mockReturnValue(false),
getMcpEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]),
isAdminEnabled: vi.fn().mockReturnValue(true),
}),
} as unknown as Config;
@@ -260,11 +261,12 @@ describe('BuiltinCommandLoader profile', () => {
getEnableHooks: () => false,
getEnableHooksUI: () => false,
getExtensionsEnabled: vi.fn().mockReturnValue(true),
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
isAgentsEnabled: vi.fn().mockReturnValue(false),
getMcpEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]),
isAdminEnabled: vi.fn().mockReturnValue(true),
}),
} as unknown as Config;
});
@@ -12,7 +12,11 @@ import {
type CommandContext,
} from '../ui/commands/types.js';
import type { MessageActionReturn, Config } from '@google/gemini-cli-core';
import { isNightly, startupProfiler } from '@google/gemini-cli-core';
import {
isNightly,
startupProfiler,
getAdminErrorMessage,
} from '@google/gemini-cli-core';
import { aboutCommand } from '../ui/commands/aboutCommand.js';
import { agentsCommand } from '../ui/commands/agentsCommand.js';
import { authCommand } from '../ui/commands/authCommand.js';
@@ -47,6 +51,7 @@ import { themeCommand } from '../ui/commands/themeCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { skillsCommand } from '../ui/commands/skillsCommand.js';
import { settingsCommand } from '../ui/commands/settingsCommand.js';
import { shellsCommand } from '../ui/commands/shellsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
@@ -101,7 +106,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
): Promise<MessageActionReturn> => ({
type: 'message',
messageType: 'error',
content: 'Extensions are disabled by your admin.',
content: getAdminErrorMessage(
'Extensions',
this.config ?? undefined,
),
}),
},
]
@@ -126,7 +134,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
): Promise<MessageActionReturn> => ({
type: 'message',
messageType: 'error',
content: 'MCP is disabled by your admin.',
content: getAdminErrorMessage('MCP', this.config ?? undefined),
}),
},
]
@@ -157,13 +165,17 @@ export class BuiltinCommandLoader implements ICommandLoader {
): Promise<MessageActionReturn> => ({
type: 'message',
messageType: 'error',
content: 'Agent skills are disabled by your admin.',
content: getAdminErrorMessage(
'Agent skills',
this.config ?? undefined,
),
}),
},
]
: [skillsCommand]
: []),
settingsCommand,
shellsCommand,
vimCommand,
setupGithubCommand,
terminalSetupCommand,
+11 -2
View File
@@ -9,12 +9,13 @@ import * as path from 'node:path';
import {
type MCPServerConfig,
type ExtensionInstallMetadata,
type ExtensionSetting,
type CustomTheme,
} from '@google/gemini-cli-core';
import {
EXTENSIONS_CONFIG_FILENAME,
INSTALL_METADATA_FILENAME,
} from '../config/extensions/variables.js';
import type { ExtensionSetting } from '../config/extensions/extensionSettings.js';
export function createExtension({
extensionsDir = 'extensions-dir',
@@ -25,12 +26,20 @@ export function createExtension({
mcpServers = {} as Record<string, MCPServerConfig>,
installMetadata = undefined as ExtensionInstallMetadata | undefined,
settings = undefined as ExtensionSetting[] | undefined,
themes = undefined as CustomTheme[] | undefined,
} = {}): string {
const extDir = path.join(extensionsDir, name);
fs.mkdirSync(extDir, { recursive: true });
fs.writeFileSync(
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
JSON.stringify({ name, version, contextFileName, mcpServers, settings }),
JSON.stringify({
name,
version,
contextFileName,
mcpServers,
settings,
themes,
}),
);
if (addContextFile) {
+20 -10
View File
@@ -27,6 +27,7 @@ import {
import { type HistoryItemToolGroup, StreamingState } from '../ui/types.js';
import { ToolActionsProvider } from '../ui/contexts/ToolActionsContext.js';
import { AskUserActionsProvider } from '../ui/contexts/AskUserActionsContext.js';
import { TerminalProvider } from '../ui/contexts/TerminalContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
@@ -157,6 +158,9 @@ const baseMockUiState = {
terminalHeight: 40,
currentModel: 'gemini-pro',
terminalBackgroundColor: undefined,
activePtyId: undefined,
backgroundShells: new Map(),
backgroundShellHeight: 0,
};
export const mockAppState: AppState = {
@@ -201,7 +205,11 @@ const mockUIActions: UIActions = {
handleApiKeyCancel: vi.fn(),
setBannerVisible: vi.fn(),
setEmbeddedShellFocused: vi.fn(),
dismissBackgroundShell: vi.fn(),
setActiveBackgroundShellPid: vi.fn(),
setIsBackgroundShellListOpen: vi.fn(),
setAuthContext: vi.fn(),
handleWarning: vi.fn(),
handleRestart: vi.fn(),
handleNewAgentsSelect: vi.fn(),
};
@@ -310,16 +318,18 @@ export const renderWithProviders = (
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<ScrollProvider>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ScrollProvider>
<TerminalProvider>
<ScrollProvider>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
+1 -1
View File
@@ -88,6 +88,7 @@ describe('App', () => {
defaultText: 'Mock Banner Text',
warningText: '',
},
backgroundShells: new Map(),
};
it('should render main content and composer when not quitting', () => {
@@ -234,7 +235,6 @@ describe('App', () => {
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Action Required'); // From ToolConfirmationQueue
expect(lastFrame()).toContain('1 of 1');
expect(lastFrame()).toContain('Composer');
expect(lastFrame()).toMatchSnapshot();
});
+331 -117
View File
@@ -21,6 +21,7 @@ import { act, useContext, type ReactElement } from 'react';
import { AppContainer } from './AppContainer.js';
import { SettingsContext } from './contexts/SettingsContext.js';
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
import { MessageType } from './types.js';
import {
type Config,
makeFakeConfig,
@@ -28,6 +29,8 @@ import {
type UserFeedbackPayload,
type ResumedSessionData,
AuthType,
UserAccountManager,
type ContentGeneratorConfig,
type AgentDefinition,
} from '@google/gemini-cli-core';
@@ -44,6 +47,11 @@ const mockIdeClient = vi.hoisted(() => ({
getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
}));
// Mock UserAccountManager
const mockUserAccountManager = vi.hoisted(() => ({
getCachedGoogleAccount: vi.fn().mockReturnValue(null),
}));
// Mock stdout
const mocks = vi.hoisted(() => ({
mockStdout: { write: vi.fn() },
@@ -73,6 +81,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
})),
enableMouseEvents: vi.fn(),
disableMouseEvents: vi.fn(),
UserAccountManager: vi
.fn()
.mockImplementation(() => mockUserAccountManager),
FileDiscoveryService: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
})),
@@ -146,6 +157,12 @@ vi.mock('./components/shared/text-buffer.js');
vi.mock('./hooks/useLogger.js');
vi.mock('./hooks/useInputHistoryStore.js');
vi.mock('./hooks/useHookDisplayState.js');
vi.mock('./hooks/useTerminalTheme.js', () => ({
useTerminalTheme: vi.fn(),
}));
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
// Mock external utilities
vi.mock('../utils/events.js');
@@ -174,7 +191,6 @@ import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import { measureElement } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
@@ -185,6 +201,7 @@ import {
disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
@@ -248,6 +265,26 @@ describe('AppContainer State Management', () => {
const mockedUseKeypress = useKeypress as Mock;
const mockedUseInputHistoryStore = useInputHistoryStore as Mock;
const mockedUseHookDisplayState = useHookDisplayState as Mock;
const mockedUseTerminalTheme = useTerminalTheme as Mock;
const DEFAULT_GEMINI_STREAM_MOCK = {
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
handleApprovalModeChange: vi.fn(),
activePtyId: null,
loopDetectionConfirmationRequest: null,
backgroundShellCount: 0,
isBackgroundShellVisible: false,
toggleBackgroundShell: vi.fn(),
backgroundCurrentShell: vi.fn(),
backgroundShells: new Map(),
registerBackgroundShell: vi.fn(),
dismissBackgroundShell: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
@@ -313,14 +350,7 @@ describe('AppContainer State Management', () => {
handleNewMessage: vi.fn(),
clearConsoleMessages: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
mockedUseFolderTrust.mockReturnValue({
isFolderTrustDialogOpen: false,
@@ -347,7 +377,9 @@ describe('AppContainer State Management', () => {
mockedUseTextBuffer.mockReturnValue({
text: '',
setText: vi.fn(),
// Add other properties if AppContainer uses them
lines: [''],
cursor: [0, 0],
handleInput: vi.fn().mockReturnValue(false),
});
mockedUseLogger.mockReturnValue({
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
@@ -362,6 +394,7 @@ describe('AppContainer State Management', () => {
currentLoadingPhrase: '',
});
mockedUseHookDisplayState.mockReturnValue([]);
mockedUseTerminalTheme.mockReturnValue(undefined);
// Mock Config
mockConfig = makeFakeConfig();
@@ -395,6 +428,7 @@ describe('AppContainer State Management', () => {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
showUserIdentity: true,
},
useAlternateBuffer: false,
},
@@ -466,6 +500,162 @@ describe('AppContainer State Management', () => {
});
});
describe('Authentication Check', () => {
it('displays correct message for LOGIN_WITH_GOOGLE auth type', async () => {
// Explicitly mock implementation to ensure we control the instance
(UserAccountManager as unknown as Mock).mockImplementation(
() => mockUserAccountManager,
);
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
'test@example.com',
);
const mockAddItem = vi.fn();
mockedUseHistory.mockReturnValue({
history: [],
addItem: mockAddItem,
updateItem: vi.fn(),
clearItems: vi.fn(),
loadHistory: vi.fn(),
});
// Explicitly enable showUserIdentity
mockSettings.merged.ui = {
...mockSettings.merged.ui,
showUserIdentity: true,
};
// Need to ensure config.getContentGeneratorConfig() returns appropriate authType
const authConfig = makeFakeConfig();
// Mock getTargetDir as well since makeFakeConfig might not set it up fully for the component
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
mockExtensionManager,
);
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
} as unknown as ContentGeneratorConfig);
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
let unmount: () => void;
await act(async () => {
const result = renderAppContainer({ config: authConfig });
unmount = result.unmount;
});
await waitFor(() => {
expect(UserAccountManager).toHaveBeenCalled();
expect(
mockUserAccountManager.getCachedGoogleAccount,
).toHaveBeenCalled();
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
text: 'Logged in with Google: test@example.com (Plan: Standard Tier)',
}),
);
});
await act(async () => {
unmount!();
});
});
it('displays correct message for USE_GEMINI auth type', async () => {
// Explicitly mock implementation to ensure we control the instance
(UserAccountManager as unknown as Mock).mockImplementation(
() => mockUserAccountManager,
);
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(null);
const mockAddItem = vi.fn();
mockedUseHistory.mockReturnValue({
history: [],
addItem: mockAddItem,
updateItem: vi.fn(),
clearItems: vi.fn(),
loadHistory: vi.fn(),
});
const authConfig = makeFakeConfig();
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
mockExtensionManager,
);
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: AuthType.USE_GEMINI,
} as unknown as ContentGeneratorConfig);
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
let unmount: () => void;
await act(async () => {
const result = renderAppContainer({ config: authConfig });
unmount = result.unmount;
});
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('Authenticated with gemini-api-key'),
}),
);
});
await act(async () => {
unmount!();
});
});
it('does not display authentication message if showUserIdentity is false', async () => {
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
'test@example.com',
);
const mockAddItem = vi.fn();
mockedUseHistory.mockReturnValue({
history: [],
addItem: mockAddItem,
updateItem: vi.fn(),
clearItems: vi.fn(),
loadHistory: vi.fn(),
});
mockSettings.merged.ui = {
...mockSettings.merged.ui,
showUserIdentity: false,
};
const authConfig = makeFakeConfig();
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
mockExtensionManager,
);
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
} as unknown as ContentGeneratorConfig);
let unmount: () => void;
await act(async () => {
const result = renderAppContainer({ config: authConfig });
unmount = result.unmount;
});
// Give it some time to potentially call addItem
await new Promise((resolve) => setTimeout(resolve, 100));
expect(mockAddItem).not.toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
}),
);
await act(async () => {
unmount!();
});
});
});
describe('Context Providers', () => {
it('provides AppContext with correct values', async () => {
let unmount: () => void;
@@ -1015,12 +1205,9 @@ describe('AppContainer State Management', () => {
// Mock the streaming state as Active
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Some thought' },
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1056,12 +1243,9 @@ describe('AppContainer State Management', () => {
// Mock the streaming state
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Some thought' },
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1128,12 +1312,9 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought
const thoughtSubject = 'Processing request';
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: thoughtSubject },
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1169,14 +1350,7 @@ describe('AppContainer State Management', () => {
} as unknown as LoadedSettings;
// Mock the streaming state as Idle with no thought
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
// Act: Render the container
const { unmount } = renderAppContainer({
@@ -1213,12 +1387,9 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought
const thoughtSubject = 'Confirm tool execution';
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'waiting_for_confirmation',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: thoughtSubject },
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1270,16 +1441,11 @@ describe('AppContainer State Management', () => {
// Mock an active shell pty but not focused
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Executing shell command' },
cancelOngoingRequest: vi.fn(),
pendingToolCalls: [],
handleApprovalModeChange: vi.fn(),
activePtyId: 'pty-1',
loopDetectionConfirmationRequest: null,
lastOutputTime: startTime + 100, // Trigger aggressive delay
retryStatus: null,
});
@@ -1334,12 +1500,9 @@ describe('AppContainer State Management', () => {
// Mock an active shell pty with redirection active
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Executing shell command' },
cancelOngoingRequest: vi.fn(),
pendingToolCalls: [
{
request: {
@@ -1349,9 +1512,7 @@ describe('AppContainer State Management', () => {
status: 'executing',
} as unknown as TrackedToolCall,
],
handleApprovalModeChange: vi.fn(),
activePtyId: 'pty-1',
loopDetectionConfirmationRequest: null,
lastOutputTime: startTime,
retryStatus: null,
});
@@ -1409,16 +1570,11 @@ describe('AppContainer State Management', () => {
// Mock an active shell pty with NO output since operation started (silent)
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Executing shell command' },
cancelOngoingRequest: vi.fn(),
pendingToolCalls: [],
handleApprovalModeChange: vi.fn(),
activePtyId: 'pty-1',
loopDetectionConfirmationRequest: null,
lastOutputTime: startTime, // lastOutputTime <= operationStartTime
retryStatus: null,
});
@@ -1465,12 +1621,9 @@ describe('AppContainer State Management', () => {
// Mock an active shell pty but not focused
let lastOutputTime = startTime + 1000;
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Executing shell command' },
cancelOngoingRequest: vi.fn(),
activePtyId: 'pty-1',
lastOutputTime,
}));
@@ -1491,12 +1644,9 @@ describe('AppContainer State Management', () => {
// Update lastOutputTime to simulate new output
lastOutputTime = startTime + 21000;
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: 'Executing shell command' },
cancelOngoingRequest: vi.fn(),
activePtyId: 'pty-1',
lastOutputTime,
}));
@@ -1556,12 +1706,9 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought with a short subject
const shortTitle = 'Short';
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: shortTitle },
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1600,12 +1747,9 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought
const title = 'Test Title';
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: { subject: title },
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1643,12 +1787,8 @@ describe('AppContainer State Management', () => {
// Mock the streaming state
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
});
// Act: Render the container
@@ -1750,12 +1890,7 @@ describe('AppContainer State Management', () => {
mockedMeasureElement.mockReturnValue({ width: 80, height: 10 }); // Footer is taller than the screen
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
...DEFAULT_GEMINI_STREAM_MOCK,
activePtyId: 'some-id',
});
@@ -1774,7 +1909,7 @@ describe('AppContainer State Management', () => {
});
describe('Keyboard Input Handling (CTRL+C / CTRL+D)', () => {
let handleGlobalKeypress: (key: Key) => void;
let handleGlobalKeypress: (key: Key) => boolean;
let mockHandleSlashCommand: Mock;
let mockCancelOngoingRequest: Mock;
let rerender: () => void;
@@ -1809,9 +1944,11 @@ describe('AppContainer State Management', () => {
beforeEach(() => {
// Capture the keypress handler from the AppContainer
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
handleGlobalKeypress = callback;
});
mockedUseKeypress.mockImplementation(
(callback: (key: Key) => boolean) => {
handleGlobalKeypress = callback;
},
);
// Mock slash command handler
mockHandleSlashCommand = vi.fn();
@@ -1827,11 +1964,7 @@ describe('AppContainer State Management', () => {
// Mock request cancellation
mockCancelOngoingRequest = vi.fn();
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
...DEFAULT_GEMINI_STREAM_MOCK,
cancelOngoingRequest: mockCancelOngoingRequest,
});
@@ -1839,6 +1972,9 @@ describe('AppContainer State Management', () => {
mockedUseTextBuffer.mockReturnValue({
text: '',
setText: vi.fn(),
lines: [''],
cursor: [0, 0],
handleInput: vi.fn().mockReturnValue(false),
});
vi.useFakeTimers();
@@ -1852,11 +1988,8 @@ describe('AppContainer State Management', () => {
describe('CTRL+C', () => {
it('should cancel ongoing request on first press', async () => {
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: mockCancelOngoingRequest,
});
await setupKeypressTest();
@@ -1891,7 +2024,7 @@ describe('AppContainer State Management', () => {
// Advance timer past the reset threshold
act(() => {
vi.advanceTimersByTime(1001);
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
});
pressKey({ name: 'c', ctrl: true });
@@ -1901,19 +2034,6 @@ describe('AppContainer State Management', () => {
});
describe('CTRL+D', () => {
it('should do nothing if text buffer is not empty', async () => {
mockedUseTextBuffer.mockReturnValue({
text: 'some text',
setText: vi.fn(),
});
await setupKeypressTest();
pressKey({ name: 'd', ctrl: true }, 2);
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
unmount();
});
it('should quit on second press if buffer is empty', async () => {
await setupKeypressTest();
@@ -1928,6 +2048,50 @@ describe('AppContainer State Management', () => {
unmount();
});
it('should NOT quit if buffer is not empty (bubbles from InputPrompt)', async () => {
mockedUseTextBuffer.mockReturnValue({
text: 'some text',
setText: vi.fn(),
lines: ['some text'],
cursor: [0, 9], // At the end
handleInput: vi.fn().mockReturnValue(false),
});
await setupKeypressTest();
// Capture return value
let result = true;
const originalPressKey = (key: Partial<Key>) => {
act(() => {
result = handleGlobalKeypress({
name: 'd',
shift: false,
alt: false,
ctrl: true,
cmd: false,
...key,
} as Key);
});
rerender();
};
originalPressKey({ name: 'd', ctrl: true });
// AppContainer's handler should return true if it reaches it
expect(result).toBe(true);
// But it should only be called once, so count is 1, not quitting yet.
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
originalPressKey({ name: 'd', ctrl: true });
// Now count is 2, it should quit.
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
'/quit',
undefined,
undefined,
false,
);
unmount();
});
it('should reset press count after a timeout', async () => {
await setupKeypressTest();
@@ -1936,7 +2100,7 @@ describe('AppContainer State Management', () => {
// Advance timer past the reset threshold
act(() => {
vi.advanceTimersByTime(1001);
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
});
pressKey({ name: 'd', ctrl: true });
@@ -1947,7 +2111,7 @@ describe('AppContainer State Management', () => {
});
describe('Copy Mode (CTRL+S)', () => {
let handleGlobalKeypress: (key: Key) => void;
let handleGlobalKeypress: (key: Key) => boolean;
let rerender: () => void;
let unmount: () => void;
@@ -1977,9 +2141,11 @@ describe('AppContainer State Management', () => {
beforeEach(() => {
mocks.mockStdout.write.mockClear();
mockedUseKeypress.mockImplementation((callback: (key: Key) => void) => {
handleGlobalKeypress = callback;
});
mockedUseKeypress.mockImplementation(
(callback: (key: Key) => boolean) => {
handleGlobalKeypress = callback;
},
);
vi.useFakeTimers();
});
@@ -2332,6 +2498,59 @@ describe('AppContainer State Management', () => {
expect(capturedUIState.activeHooks).toEqual(mockHooks);
unmount!();
});
it('handles consent request events', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
const handler = mockCoreEvents.on.mock.calls.find(
(call: unknown[]) => call[0] === CoreEvent.ConsentRequest,
)?.[1];
expect(handler).toBeDefined();
const onConfirm = vi.fn();
const payload = {
prompt: 'Do you consent?',
onConfirm,
};
act(() => {
handler(payload);
});
expect(capturedUIState.authConsentRequest).toBeDefined();
expect(capturedUIState.authConsentRequest?.prompt).toBe(
'Do you consent?',
);
act(() => {
capturedUIState.authConsentRequest?.onConfirm(true);
});
expect(onConfirm).toHaveBeenCalledWith(true);
expect(capturedUIState.authConsentRequest).toBeNull();
unmount!();
});
it('unsubscribes from ConsentRequest on unmount', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
unmount!();
expect(mockCoreEvents.off).toHaveBeenCalledWith(
CoreEvent.ConsentRequest,
expect.any(Function),
);
});
});
describe('Shell Interaction', () => {
@@ -2343,12 +2562,7 @@ describe('AppContainer State Management', () => {
});
mockedUseGeminiStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
...DEFAULT_GEMINI_STREAM_MOCK,
activePtyId: 'some-pty-id', // Make sure activePtyId is set
});
+251 -107
View File
@@ -27,13 +27,10 @@ import {
type HistoryItemWithoutId,
type HistoryItemToolGroup,
AuthState,
type ConfirmationRequest,
} from './types.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import {
AskUserActionsProvider,
type AskUserState,
} from './contexts/AskUserActionsContext.js';
import {
type EditorType,
type Config,
@@ -47,6 +44,7 @@ import {
getErrorMessage,
getAllGeminiMdFilenames,
AuthType,
UserAccountManager,
clearCachedCredentialFile,
type ResumedSessionData,
recordExitFail,
@@ -67,8 +65,7 @@ import {
SessionStartSource,
SessionEndReason,
generateSummary,
MessageBusType,
type AskUserRequest,
type ConsentRequestPayload,
type AgentsDiscoveredPayload,
ChangeAuthRequestedError,
} from '@google/gemini-cli-core';
@@ -96,6 +93,7 @@ import { computeTerminalTitle } from '../utils/windowTitle.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { type BackgroundShell } from './hooks/shellCommandProcessor.js';
import { useVim } from './hooks/vim.js';
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
import { type InitializationResult } from '../core/initializer.js';
@@ -135,6 +133,7 @@ import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
import { useBanner } from './hooks/useBanner.js';
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js';
import {
WARNING_PROMPT_DURATION_MS,
QUEUE_ERROR_DISPLAY_DURATION_MS,
@@ -142,6 +141,7 @@ import {
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
import { isSlashCommand } from './utils/commandUtils.js';
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
return pendingHistoryItems.some((item) => {
@@ -186,11 +186,58 @@ const SHELL_HEIGHT_PADDING = 10;
export const AppContainer = (props: AppContainerProps) => {
const { config, initializationResult, resumedSessionData } = props;
const settings = useSettings();
const historyManager = useHistory({
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
});
const { addItem } = historyManager;
const authCheckPerformed = useRef(false);
useEffect(() => {
if (authCheckPerformed.current) return;
authCheckPerformed.current = true;
if (resumedSessionData || settings.merged.ui.showUserIdentity === false) {
return;
}
const authType = config.getContentGeneratorConfig()?.authType;
// Run this asynchronously to avoid blocking the event loop.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
try {
const userAccountManager = new UserAccountManager();
const email = userAccountManager.getCachedGoogleAccount();
const tierName = config.getUserTierName();
if (authType) {
let message =
authType === AuthType.LOGIN_WITH_GOOGLE
? email
? `Logged in with Google: ${email}`
: 'Logged in with Google'
: `Authenticated with ${authType}`;
if (tierName) {
message += ` (Plan: ${tierName})`;
}
addItem({
type: MessageType.INFO,
text: message,
});
}
} catch (_e) {
// Ignore errors during initial auth check
}
})();
}, [
config,
resumedSessionData,
settings.merged.ui.showUserIdentity,
addItem,
]);
useMemoryMonitor(historyManager);
const settings = useSettings();
const isAlternateBuffer = useAlternateBuffer();
const [corgiMode, setCorgiMode] = useState(false);
const [debugMessage, setDebugMessage] = useState<string>('');
@@ -209,6 +256,10 @@ export const AppContainer = (props: AppContainerProps) => {
);
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false);
const toggleBackgroundShellRef = useRef<() => void>(() => {});
const isBackgroundShellVisibleRef = useRef<boolean>(false);
const backgroundShellsRef = useRef<Map<number, BackgroundShell>>(new Map());
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
const [shellModeActive, setShellModeActive] = useState(false);
@@ -288,11 +339,6 @@ export const AppContainer = (props: AppContainerProps) => {
AgentDefinition | undefined
>();
// AskUser dialog state
const [askUserRequest, setAskUserRequest] = useState<AskUserState | null>(
null,
);
const openAgentConfigDialog = useCallback(
(name: string, displayName: string, definition: AgentDefinition) => {
setSelectedAgentName(name);
@@ -310,56 +356,6 @@ export const AppContainer = (props: AppContainerProps) => {
setSelectedAgentDefinition(undefined);
}, []);
// Subscribe to ASK_USER_REQUEST messages from the message bus
useEffect(() => {
const messageBus = config.getMessageBus();
const handler = (msg: AskUserRequest) => {
setAskUserRequest({
questions: msg.questions,
correlationId: msg.correlationId,
});
};
messageBus.subscribe(MessageBusType.ASK_USER_REQUEST, handler);
return () => {
messageBus.unsubscribe(MessageBusType.ASK_USER_REQUEST, handler);
};
}, [config]);
// Handler to submit ask_user answers
const handleAskUserSubmit = useCallback(
async (answers: { [questionIndex: string]: string }) => {
if (!askUserRequest) return;
const messageBus = config.getMessageBus();
await messageBus.publish({
type: MessageBusType.ASK_USER_RESPONSE,
correlationId: askUserRequest.correlationId,
answers,
});
setAskUserRequest(null);
},
[config, askUserRequest],
);
// Handler to cancel ask_user dialog
const handleAskUserCancel = useCallback(async () => {
if (!askUserRequest) return;
const messageBus = config.getMessageBus();
await messageBus.publish({
type: MessageBusType.ASK_USER_RESPONSE,
correlationId: askUserRequest.correlationId,
answers: {},
cancelled: true,
});
setAskUserRequest(null);
}, [config, askUserRequest]);
const toggleDebugProfiler = useCallback(
() => setShowDebugProfiler((prev) => !prev),
[],
@@ -439,6 +435,12 @@ export const AppContainer = (props: AppContainerProps) => {
registerCleanup(async () => {
// Turn off mouse scroll.
disableMouseEvents();
// Kill all background shells
for (const pid of backgroundShellsRef.current.keys()) {
ShellExecutionService.kill(pid);
}
const ideClient = await IdeClient.getInstance();
await ideClient.disconnect();
@@ -531,6 +533,14 @@ export const AppContainer = (props: AppContainerProps) => {
shellModeActive,
getPreferredEditor,
});
const bufferRef = useRef(buffer);
useEffect(() => {
bufferRef.current = buffer;
}, [buffer]);
const stableSetText = useCallback((text: string) => {
bufferRef.current.setText(text);
}, []);
// Initialize input history from logger (past sessions)
useEffect(() => {
@@ -592,6 +602,9 @@ export const AppContainer = (props: AppContainerProps) => {
initializationResult.themeError,
);
// Poll for terminal background color changes to auto-switch theme
useTerminalTheme(handleThemeSelect, config);
const {
authState,
setAuthState,
@@ -787,6 +800,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
const { toggleVimEnabled } = useVimMode();
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
() => {},
);
const slashCommandActions = useMemo(
() => ({
openAuthDialog: () => setAuthState(AuthState.Updating),
@@ -810,7 +827,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
toggleDebugProfiler,
dispatchExtensionStateUpdate,
addConfirmUpdateExtensionRequest,
setText: (text: string) => buffer.setText(text),
toggleBackgroundShell: () => {
toggleBackgroundShellRef.current();
if (!isBackgroundShellVisibleRef.current) {
setEmbeddedShellFocused(true);
if (backgroundShellsRef.current.size > 1) {
setIsBackgroundShellListOpenRef.current(true);
} else {
setIsBackgroundShellListOpenRef.current(false);
}
}
},
setText: stableSetText,
}),
[
setAuthState,
@@ -828,7 +856,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openPermissionsDialog,
addConfirmUpdateExtensionRequest,
toggleDebugProfiler,
buffer,
stableSetText,
],
);
@@ -837,7 +865,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
slashCommands,
pendingHistoryItems: pendingSlashCommandHistoryItems,
commandContext,
confirmationRequest,
confirmationRequest: commandConfirmationRequest,
} = useSlashCommandProcessor(
config,
settings,
@@ -854,6 +882,26 @@ Logging in with Google... Restarting Gemini CLI to continue.
setCustomDialog,
);
const [authConsentRequest, setAuthConsentRequest] =
useState<ConfirmationRequest | null>(null);
useEffect(() => {
const handleConsentRequest = (payload: ConsentRequestPayload) => {
setAuthConsentRequest({
prompt: payload.prompt,
onConfirm: (confirmed: boolean) => {
setAuthConsentRequest(null);
payload.onConfirm(confirmed);
},
});
};
coreEvents.on(CoreEvent.ConsentRequest, handleConsentRequest);
return () => {
coreEvents.off(CoreEvent.ConsentRequest, handleConsentRequest);
};
}, []);
const performMemoryRefresh = useCallback(async () => {
historyManager.addItem(
{
@@ -941,6 +989,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
activePtyId,
loopDetectionConfirmationRequest,
lastOutputTime,
backgroundShellCount,
isBackgroundShellVisible,
toggleBackgroundShell,
backgroundCurrentShell,
backgroundShells,
dismissBackgroundShell,
retryStatus,
} = useGeminiStream(
config.getGeminiClient(),
@@ -963,7 +1017,30 @@ Logging in with Google... Restarting Gemini CLI to continue.
embeddedShellFocused,
);
toggleBackgroundShellRef.current = toggleBackgroundShell;
isBackgroundShellVisibleRef.current = isBackgroundShellVisible;
backgroundShellsRef.current = backgroundShells;
const {
activeBackgroundShellPid,
setIsBackgroundShellListOpen,
isBackgroundShellListOpen,
setActiveBackgroundShellPid,
backgroundShellHeight,
} = useBackgroundShellManager({
backgroundShells,
backgroundShellCount,
isBackgroundShellVisible,
activePtyId,
embeddedShellFocused,
setEmbeddedShellFocused,
terminalHeight,
});
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
const lastOutputTimeRef = useRef(0);
useEffect(() => {
lastOutputTimeRef.current = lastOutputTime;
}, [lastOutputTime]);
@@ -1112,7 +1189,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
// Compute available terminal height based on controls measurement
const availableTerminalHeight = Math.max(
0,
terminalHeight - controlsHeight - staticExtraHeight - 2,
terminalHeight -
controlsHeight -
staticExtraHeight -
2 -
backgroundShellHeight,
);
config.setShellExecutionConfig({
@@ -1336,7 +1417,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (ctrlCPressCount > 1) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
handleSlashCommand('/quit', undefined, undefined, false);
} else {
} else if (ctrlCPressCount > 0) {
ctrlCTimerRef.current = setTimeout(() => {
setCtrlCPressCount(0);
ctrlCTimerRef.current = null;
@@ -1355,7 +1436,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (ctrlDPressCount > 1) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
handleSlashCommand('/quit', undefined, undefined, false);
} else {
} else if (ctrlDPressCount > 0) {
ctrlDTimerRef.current = setTimeout(() => {
setCtrlDPressCount(0);
ctrlDTimerRef.current = null;
@@ -1396,7 +1477,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
});
const handleGlobalKeypress = useCallback(
(key: Key) => {
(key: Key): boolean => {
if (copyModeEnabled) {
setCopyModeEnabled(false);
enableMouseEvents();
@@ -1416,10 +1497,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
if (keyMatchers[Command.QUIT](key)) {
// Skip when ask_user dialog is open (use Esc to cancel instead)
if (askUserRequest) {
return;
}
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
// This should happen regardless of the count.
cancelOngoingRequest?.();
@@ -1427,9 +1504,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
setCtrlCPressCount((prev) => prev + 1);
return true;
} else if (keyMatchers[Command.EXIT](key)) {
if (buffer.text.length > 0) {
return false;
}
setCtrlDPressCount((prev) => prev + 1);
return true;
}
@@ -1443,6 +1517,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
setShowErrorDetails((prev) => !prev);
return true;
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
handleWarning('Undo has been moved to Cmd + Z or Alt/Opt + Z');
return true;
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
setShowFullTodos((prev) => !prev);
return true;
@@ -1469,9 +1546,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
setConstrainHeight(false);
return true;
} else if (
keyMatchers[Command.UNFOCUS_SHELL_INPUT](key) &&
activePtyId &&
embeddedShellFocused
keyMatchers[Command.FOCUS_SHELL_INPUT](key) &&
(activePtyId || (isBackgroundShellVisible && backgroundShells.size > 0))
) {
if (key.name === 'tab' && key.shift) {
// Always change focus
@@ -1479,26 +1555,72 @@ Logging in with Google... Restarting Gemini CLI to continue.
return true;
}
if (embeddedShellFocused) {
handleWarning('Press Shift+Tab to focus out.');
return true;
}
const now = Date.now();
// If the shell hasn't produced output in the last 100ms, it's considered idle.
const isIdle = now - lastOutputTimeRef.current >= 100;
if (isIdle) {
if (isIdle && !activePtyId) {
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
tabFocusTimeoutRef.current = setTimeout(() => {
tabFocusTimeoutRef.current = null;
// If the shell produced output since the tab press, we assume it handled the tab
// (e.g. autocomplete) so we should not toggle focus.
if (lastOutputTimeRef.current > now) {
handleWarning('Press Shift+Tab to focus out.');
return;
toggleBackgroundShell();
if (!isBackgroundShellVisible) {
// We are about to show it, so focus it
setEmbeddedShellFocused(true);
if (backgroundShells.size > 1) {
setIsBackgroundShellListOpen(true);
}
setEmbeddedShellFocused(false);
}, 100);
} else {
// We are about to hide it
tabFocusTimeoutRef.current = setTimeout(() => {
tabFocusTimeoutRef.current = null;
// If the shell produced output since the tab press, we assume it handled the tab
// (e.g. autocomplete) so we should not toggle focus.
if (lastOutputTimeRef.current > now) {
handleWarning('Press Shift+Tab to focus out.');
return;
}
setEmbeddedShellFocused(false);
}, 100);
}
return true;
}
handleWarning('Press Shift+Tab to focus out.');
// Not idle, just focus it
setEmbeddedShellFocused(true);
return true;
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
if (activePtyId) {
backgroundCurrentShell();
// After backgrounding, we explicitly do NOT show or focus the background UI.
} else {
if (isBackgroundShellVisible && !embeddedShellFocused) {
setEmbeddedShellFocused(true);
} else {
toggleBackgroundShell();
// Toggle focus based on intent: if we were hiding, unfocus; if showing, focus.
if (!isBackgroundShellVisible && backgroundShells.size > 0) {
setEmbeddedShellFocused(true);
if (backgroundShells.size > 1) {
setIsBackgroundShellListOpen(true);
}
} else {
setEmbeddedShellFocused(false);
}
}
}
return true;
} else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) {
if (backgroundShells.size > 0 && isBackgroundShellVisible) {
if (!embeddedShellFocused) {
setEmbeddedShellFocused(true);
}
setIsBackgroundShellListOpen(true);
}
return true;
}
return false;
@@ -1510,11 +1632,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
config,
ideContextState,
setCtrlCPressCount,
buffer.text.length,
setCtrlDPressCount,
handleSlashCommand,
cancelOngoingRequest,
askUserRequest,
activePtyId,
embeddedShellFocused,
settings.merged.general.debugKeystrokeLogging,
@@ -1522,6 +1642,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
setCopyModeEnabled,
copyModeEnabled,
isAlternateBuffer,
backgroundCurrentShell,
toggleBackgroundShell,
backgroundShells,
isBackgroundShellVisible,
setIsBackgroundShellListOpen,
lastOutputTimeRef,
tabFocusTimeoutRef,
handleWarning,
],
);
@@ -1535,7 +1662,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
const paddedTitle = computeTerminalTitle({
streamingState,
thoughtSubject: thought?.subject,
isConfirming: !!confirmationRequest || shouldShowActionRequiredTitle,
isConfirming:
!!commandConfirmationRequest || shouldShowActionRequiredTitle,
isSilentWorking: shouldShowSilentWorkingTitle,
folderName: basename(config.getTargetDir()),
showThoughts: !!settings.merged.ui.showStatusInTitle,
@@ -1551,7 +1679,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
}, [
streamingState,
thought,
confirmationRequest,
commandConfirmationRequest,
shouldShowActionRequiredTitle,
shouldShowSilentWorkingTitle,
settings.merged.ui.showStatusInTitle,
@@ -1627,11 +1755,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
const nightly = props.version.includes('nightly');
const dialogsVisible =
!!askUserRequest ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
adminSettingsChanged ||
!!confirmationRequest ||
!!commandConfirmationRequest ||
!!authConsentRequest ||
!!customDialog ||
confirmUpdateExtensionRequests.length > 0 ||
!!loopDetectionConfirmationRequest ||
@@ -1741,7 +1869,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
slashCommands,
pendingSlashCommandHistoryItems,
commandContext,
confirmationRequest,
commandConfirmationRequest,
authConsentRequest,
confirmUpdateExtensionRequests,
loopDetectionConfirmationRequest,
geminiMdFileCount,
@@ -1802,6 +1931,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
isRestarting,
extensionsUpdateState,
activePtyId,
backgroundShellCount,
isBackgroundShellVisible,
embeddedShellFocused,
showDebugProfiler,
customDialog,
@@ -1811,6 +1942,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
bannerVisible,
terminalBackgroundColor: config.getTerminalBackground(),
settingsNonce,
backgroundShells,
activeBackgroundShellPid,
backgroundShellHeight,
isBackgroundShellListOpen,
adminSettingsChanged,
newAgents,
}),
@@ -1839,7 +1974,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
slashCommands,
pendingSlashCommandHistoryItems,
commandContext,
confirmationRequest,
commandConfirmationRequest,
authConsentRequest,
confirmUpdateExtensionRequests,
loopDetectionConfirmationRequest,
geminiMdFileCount,
@@ -1900,6 +2036,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
currentModel,
extensionsUpdateState,
activePtyId,
backgroundShellCount,
isBackgroundShellVisible,
historyManager,
embeddedShellFocused,
showDebugProfiler,
@@ -1912,6 +2050,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
bannerVisible,
config,
settingsNonce,
backgroundShellHeight,
isBackgroundShellListOpen,
activeBackgroundShellPid,
backgroundShells,
adminSettingsChanged,
newAgents,
],
@@ -1959,7 +2101,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeySubmit,
handleApiKeyCancel,
setBannerVisible,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
setAuthContext,
handleRestart: async () => {
if (process.send) {
@@ -2031,7 +2177,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeySubmit,
handleApiKeyCancel,
setBannerVisible,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
setAuthContext,
newAgents,
config,
@@ -2062,15 +2212,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
}}
>
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
<AskUserActionsProvider
request={askUserRequest}
onSubmit={handleAskUserSubmit}
onCancel={handleAskUserCancel}
>
<ShellFocusContext.Provider value={isFocused}>
<App />
</ShellFocusContext.Provider>
</AskUserActionsProvider>
<ShellFocusContext.Provider value={isFocused}>
<App />
</ShellFocusContext.Provider>
</ToolActionsProvider>
</AppContext.Provider>
</ConfigContext.Provider>
@@ -125,7 +125,7 @@ Tips for getting started:
4. /help for more information.
HistoryItemDisplay
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required 1 of 1
│ Action Required
│ │
│ ? ls list directory │
│ │
@@ -92,6 +92,7 @@ describe('bugCommand', () => {
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
},
});
@@ -106,6 +107,7 @@ describe('bugCommand', () => {
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Model Version:** gemini-pro
* **Auth Type:** oauth-personal
* **Memory Usage:** 100 MB
* **Terminal Name:** Test Terminal
* **Terminal Background:** #000000
@@ -133,6 +135,7 @@ describe('bugCommand', () => {
getHistory: () => history,
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
@@ -178,6 +181,7 @@ describe('bugCommand', () => {
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
},
});
@@ -192,6 +196,7 @@ describe('bugCommand', () => {
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Model Version:** gemini-pro
* **Auth Type:** vertex-ai
* **Memory Usage:** 100 MB
* **Terminal Name:** Test Terminal
* **Terminal Background:** #000000
@@ -54,6 +54,7 @@ export const bugCommand: SlashCommand = {
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
? 'Supported'
: 'Unsupported';
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
let info = `
* **CLI Version:** ${cliVersion}
@@ -62,6 +63,7 @@ export const bugCommand: SlashCommand = {
* **Operating System:** ${osVersion}
* **Sandbox Environment:** ${sandboxEnv}
* **Model Version:** ${modelVersion}
* **Auth Type:** ${authType}
* **Memory Usage:** ${memoryUsage}
* **Terminal Name:** ${terminalName}
* **Terminal Background:** ${terminalBgColor}
@@ -67,6 +67,9 @@ describe('directoryCommand', () => {
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
getGeminiClient: vi.fn().mockReturnValue({
addDirectoryContext: vi.fn(),
getChatRecordingService: vi.fn().mockReturnValue({
recordDirectories: vi.fn(),
}),
}),
getWorkingDir: () => path.resolve('/test/dir'),
shouldLoadMemoryFromIncludeDirectories: () => false,
@@ -57,6 +57,13 @@ async function finishAddingDirectories(
const gemini = config.getGeminiClient();
if (gemini) {
await gemini.addDirectoryContext();
// Persist directories to session file for resume support
const chatRecordingService = gemini.getChatRecordingService();
const workspaceContext = config.getWorkspaceContext();
chatRecordingService?.recordDirectories(
workspaceContext.getDirectories(),
);
}
addItem({
type: MessageType.INFO,
@@ -11,6 +11,7 @@ import { MessageType } from '../types.js';
import type { HookRegistryEntry } from '@google/gemini-cli-core';
import { HookType, HookEventName, ConfigSource } from '@google/gemini-cli-core';
import type { CommandContext } from './types.js';
import { SettingScope } from '../../config/settings.js';
describe('hooksCommand', () => {
let mockContext: CommandContext;
@@ -29,11 +30,13 @@ describe('hooksCommand', () => {
hooksConfig?: {
disabled?: string[];
};
tools?: {
enableHooks?: boolean;
};
};
setValue: ReturnType<typeof vi.fn>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
workspace: { path: string; settings: any };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
user: { path: string; settings: any };
forScope: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
@@ -56,6 +59,17 @@ describe('hooksCommand', () => {
};
// Create mock settings
const mockUser = {
path: '/mock/user.json',
settings: { hooksConfig: { disabled: [] } },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
const mockWorkspace = {
path: '/mock/workspace.json',
settings: { hooksConfig: { disabled: [] } },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
mockSettings = {
merged: {
hooksConfig: {
@@ -63,7 +77,15 @@ describe('hooksCommand', () => {
},
},
setValue: vi.fn(),
};
workspace: mockWorkspace,
user: mockUser,
forScope: vi.fn((scope) => {
if (scope === SettingScope.User) return mockUser;
if (scope === SettingScope.Workspace) return mockWorkspace;
return mockUser;
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
// Create mock context with config and settings
mockContext = createMockCommandContext({
@@ -162,8 +184,8 @@ describe('hooksCommand', () => {
it('should display panel when no hooks are configured', async () => {
mockHookSystem.getAllHooks.mockReturnValue([]);
(mockContext.services.settings.merged as Record<string, unknown>)[
'tools'
] = { enableHooks: true };
'hooksConfig'
] = { enabled: true };
const panelCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'panel',
@@ -190,8 +212,8 @@ describe('hooksCommand', () => {
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
(mockContext.services.settings.merged as Record<string, unknown>)[
'tools'
] = { enableHooks: true };
'hooksConfig'
] = { enabled: true };
const panelCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'panel',
@@ -272,11 +294,12 @@ describe('hooksCommand', () => {
});
it('should enable a hook and update settings', async () => {
// Update the context's settings with disabled hooks
mockContext.services.settings.merged.hooksConfig.disabled = [
// Update the user settings with disabled hooks
mockSettings.user.settings.hooksConfig.disabled = [
'test-hook',
'other-hook',
];
mockSettings.workspace.settings.hooksConfig.disabled = [];
const enableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'enable',
@@ -288,7 +311,7 @@ describe('hooksCommand', () => {
const result = await enableCmd.action(mockContext, 'test-hook');
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String),
SettingScope.User,
'hooksConfig.disabled',
['other-hook'],
);
@@ -299,28 +322,8 @@ describe('hooksCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Hook "test-hook" enabled successfully.',
});
});
it('should handle error when enabling hook fails', async () => {
mockSettings.setValue.mockImplementationOnce(() => {
throw new Error('Failed to save settings');
});
const enableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'enable',
);
if (!enableCmd?.action) {
throw new Error('enable command must have an action');
}
const result = await enableCmd.action(mockContext, 'test-hook');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Failed to enable hook: Failed to save settings',
content:
'Hook "test-hook" enabled by removing it from the disabled list in user (/mock/user.json) and workspace (/mock/workspace.json) settings.',
});
});
@@ -332,7 +335,7 @@ describe('hooksCommand', () => {
const hookEntry = createMockHook(
'./hooks/test.sh',
HookEventName.BeforeTool,
true,
false, // Must be disabled for enable completion
);
hookEntry.config.name = 'friendly-name';
@@ -404,7 +407,9 @@ describe('hooksCommand', () => {
});
it('should disable a hook and update settings', async () => {
mockContext.services.settings.merged.hooksConfig.disabled = [];
// Ensure not disabled anywhere
mockSettings.workspace.settings.hooksConfig.disabled = [];
mockSettings.user.settings.hooksConfig.disabled = [];
const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable',
@@ -415,8 +420,9 @@ describe('hooksCommand', () => {
const result = await disableCmd.action(mockContext, 'test-hook');
// Should default to workspace if present
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String),
SettingScope.Workspace,
'hooksConfig.disabled',
['test-hook'],
);
@@ -427,13 +433,14 @@ describe('hooksCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Hook "test-hook" disabled successfully.',
content:
'Hook "test-hook" disabled by adding it to the disabled list in workspace (/mock/workspace.json) settings.',
});
});
it('should synchronize with hook system even if hook is already in disabled list', async () => {
// Update the context's settings with the hook already disabled
mockContext.services.settings.merged.hooksConfig.disabled = ['test-hook'];
it('should return info when hook is already disabled', async () => {
// Update the context's settings with the hook already disabled in Workspace
mockSettings.workspace.settings.hooksConfig.disabled = ['test-hook'];
const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable',
@@ -445,38 +452,29 @@ describe('hooksCommand', () => {
const result = await disableCmd.action(mockContext, 'test-hook');
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
'test-hook',
false,
);
expect(mockConfig.updateDisabledHooks).toHaveBeenCalled();
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Hook "test-hook" disabled successfully.',
content: 'Hook "test-hook" is already disabled.',
});
});
it('should handle error when disabling hook fails', async () => {
mockContext.services.settings.merged.hooksConfig.disabled = [];
mockSettings.setValue.mockImplementationOnce(() => {
throw new Error('Failed to save settings');
});
it('should complete hook names using friendly names', () => {
const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable',
)!;
const hookEntry = createMockHook(
'./hooks/test.sh',
HookEventName.BeforeTool,
true, // Must be enabled for disable completion
);
if (!disableCmd?.action) {
throw new Error('disable command must have an action');
}
hookEntry.config.name = 'friendly-name';
const result = await disableCmd.action(mockContext, 'test-hook');
mockHookSystem.getAllHooks.mockReturnValue([hookEntry]);
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Failed to disable hook: Failed to save settings',
});
const completions = disableCmd.completion!(mockContext, 'frie');
expect(completions).toContain('friendly-name');
});
});
@@ -513,50 +511,52 @@ describe('hooksCommand', () => {
expect(result).toEqual([]);
});
it('should return matching hook names', () => {
it('should return matching hook names based on status', () => {
const mockHooks: HookRegistryEntry[] = [
createMockHook('test-hook-1', HookEventName.BeforeTool, true),
createMockHook('test-hook-2', HookEventName.AfterTool, true),
createMockHook('other-hook', HookEventName.AfterAgent, false),
createMockHook('test-hook-enabled', HookEventName.BeforeTool, true),
createMockHook('test-hook-disabled', HookEventName.AfterTool, false),
];
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
const enableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'enable',
);
if (!enableCmd?.completion) {
throw new Error('enable command must have completion');
}
)!;
const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable',
)!;
const result = enableCmd.completion(mockContext, 'test');
expect(result).toEqual(['test-hook-1', 'test-hook-2']);
const enableResult = enableCmd.completion!(mockContext, 'test');
expect(enableResult).toEqual(['test-hook-disabled']);
const disableResult = disableCmd.completion!(mockContext, 'test');
expect(disableResult).toEqual(['test-hook-enabled']);
});
it('should return all hook names when partial is empty', () => {
it('should return all relevant hook names when partial is empty', () => {
const mockHooks: HookRegistryEntry[] = [
createMockHook('hook-1', HookEventName.BeforeTool, true),
createMockHook('hook-2', HookEventName.AfterTool, true),
createMockHook('hook-enabled', HookEventName.BeforeTool, true),
createMockHook('hook-disabled', HookEventName.AfterTool, false),
];
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
const enableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'enable',
);
if (!enableCmd?.completion) {
throw new Error('enable command must have completion');
}
)!;
const disableCmd = hooksCommand.subCommands!.find(
(cmd) => cmd.name === 'disable',
)!;
const result = enableCmd.completion(mockContext, '');
expect(result).toEqual(['hook-1', 'hook-2']);
expect(enableCmd.completion!(mockContext, '')).toEqual(['hook-disabled']);
expect(disableCmd.completion!(mockContext, '')).toEqual(['hook-enabled']);
});
it('should handle hooks without command name gracefully', () => {
const mockHooks: HookRegistryEntry[] = [
createMockHook('test-hook', HookEventName.BeforeTool, true),
createMockHook('test-hook', HookEventName.BeforeTool, false),
{
...createMockHook('', HookEventName.AfterTool, true),
...createMockHook('', HookEventName.AfterTool, false),
config: { command: '', type: HookType.Command, timeout: 30 },
},
];
@@ -636,7 +636,7 @@ describe('hooksCommand', () => {
const result = await enableAllCmd.action(mockContext, '');
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
expect.any(String),
expect.any(String), // enableAll uses legacy logic so it might return 'Workspace' or 'User' depending on ternary
'hooksConfig.disabled',
[],
);
+65 -74
View File
@@ -12,7 +12,9 @@ import type {
MessageActionReturn,
} from '@google/gemini-cli-core';
import { getErrorMessage } from '@google/gemini-cli-core';
import { SettingScope } from '../../config/settings.js';
import { SettingScope, isLoadableSettingScope } from '../../config/settings.js';
import { enableHook, disableHook } from '../../utils/hookSettings.js';
import { renderHookActionFeedback } from '../../utils/hookUtils.js';
/**
* Display a formatted list of hooks with their status
@@ -74,39 +76,23 @@ async function enableAction(
};
}
// Get current disabled hooks from settings
const settings = context.services.settings;
const disabledHooks = settings.merged.hooksConfig.disabled;
// Remove from disabled list if present
const newDisabledHooks = disabledHooks.filter(
(name: string) => name !== hookName,
const result = enableHook(settings, hookName);
if (result.status === 'success') {
hookSystem.setHookEnabled(hookName, true);
}
const feedback = renderHookActionFeedback(
result,
(label, path) => `${label} (${path})`,
);
// Update settings (setValue automatically saves)
try {
const scope = settings.workspace
? SettingScope.Workspace
: SettingScope.User;
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
// Update core config so re-initialization (e.g. extension reload) respects the change
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
// Enable in hook system
hookSystem.setHookEnabled(hookName, true);
return {
type: 'message',
messageType: 'info',
content: `Hook "${hookName}" enabled successfully.`,
};
} catch (error) {
return {
type: 'message',
messageType: 'error',
content: `Failed to enable hook: ${getErrorMessage(error)}`,
};
}
return {
type: 'message',
messageType: result.status === 'error' ? 'error' : 'info',
content: feedback,
};
}
/**
@@ -143,44 +129,31 @@ async function disableAction(
};
}
// Get current disabled hooks from settings
const settings = context.services.settings;
const disabledHooks = settings.merged.hooksConfig.disabled;
// Add to disabled list if not already present
try {
if (!disabledHooks.includes(hookName)) {
const newDisabledHooks = [...disabledHooks, hookName];
const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User;
const scope = settings.workspace
? SettingScope.Workspace
: SettingScope.User;
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
}
const result = disableHook(settings, hookName, scope);
// Update core config so re-initialization (e.g. extension reload) respects the change
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
// Always disable in hook system to ensure in-memory state matches settings
if (result.status === 'success') {
hookSystem.setHookEnabled(hookName, false);
return {
type: 'message',
messageType: 'info',
content: `Hook "${hookName}" disabled successfully.`,
};
} catch (error) {
return {
type: 'message',
messageType: 'error',
content: `Failed to disable hook: ${getErrorMessage(error)}`,
};
}
const feedback = renderHookActionFeedback(
result,
(label, path) => `${label} (${path})`,
);
return {
type: 'message',
messageType: result.status === 'error' ? 'error' : 'info',
content: feedback,
};
}
/**
* Completion function for hook names
* Completion function for enabled hook names (to be disabled)
*/
function completeHookNames(
function completeEnabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
@@ -191,8 +164,30 @@ function completeHookNames(
if (!hookSystem) return [];
const allHooks = hookSystem.getAllHooks();
const hookNames = allHooks.map((hook) => getHookDisplayName(hook));
return hookNames.filter((name) => name.startsWith(partialArg));
return allHooks
.filter((hook) => hook.enabled)
.map((hook) => getHookDisplayName(hook))
.filter((name) => name.startsWith(partialArg));
}
/**
* Completion function for disabled hook names (to be enabled)
*/
function completeDisabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
if (!config) return [];
const hookSystem = config.getHookSystem();
if (!hookSystem) return [];
const allHooks = hookSystem.getAllHooks();
return allHooks
.filter((hook) => !hook.enabled)
.map((hook) => getHookDisplayName(hook))
.filter((name) => name.startsWith(partialArg));
}
/**
@@ -247,13 +242,12 @@ async function enableAllAction(
}
try {
const scope = settings.workspace
? SettingScope.Workspace
: SettingScope.User;
settings.setValue(scope, 'hooksConfig.disabled', []);
// Update core config so re-initialization (e.g. extension reload) respects the change
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
const scopes = [SettingScope.Workspace, SettingScope.User];
for (const scope of scopes) {
if (isLoadableSettingScope(scope)) {
settings.setValue(scope, 'hooksConfig.disabled', []);
}
}
for (const hook of disabledHooks) {
const hookName = getHookDisplayName(hook);
@@ -325,9 +319,6 @@ async function disableAllAction(
: SettingScope.User;
settings.setValue(scope, 'hooksConfig.disabled', allHookNames);
// Update core config so re-initialization (e.g. extension reload) respects the change
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
for (const hook of enabledHooks) {
const hookName = getHookDisplayName(hook);
hookSystem.setHookEnabled(hookName, false);
@@ -361,7 +352,7 @@ const enableCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: enableAction,
completion: completeHookNames,
completion: completeDisabledHookNames,
};
const disableCommand: SlashCommand = {
@@ -370,7 +361,7 @@ const disableCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: disableAction,
completion: completeHookNames,
completion: completeEnabledHookNames,
};
const enableAllCommand: SlashCommand = {
@@ -10,6 +10,7 @@ import {
type OpenCustomDialogActionReturn,
} from './types.js';
import { TriageDuplicates } from '../components/triage/TriageDuplicates.js';
import { TriageIssues } from '../components/triage/TriageIssues.js';
export const oncallCommand: SlashCommand = {
name: 'oncall',
@@ -49,5 +50,63 @@ export const oncallCommand: SlashCommand = {
};
},
},
{
name: 'audit',
description: 'Triage issues labeled as status/need-triage',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args): Promise<OpenCustomDialogActionReturn> => {
const { config } = context.services;
if (!config) {
throw new Error('Config not available');
}
let limit = 100;
let until: string | undefined;
if (args && args.trim().length > 0) {
const argArray = args.trim().split(/\s+/);
for (let i = 0; i < argArray.length; i++) {
const arg = argArray[i];
if (arg === '--until') {
if (i + 1 >= argArray.length) {
throw new Error('Flag --until requires a value (YYYY-MM-DD).');
}
const val = argArray[i + 1];
if (!/^\d{4}-\d{2}-\d{2}$/.test(val)) {
throw new Error(
`Invalid date format for --until: "${val}". Expected YYYY-MM-DD.`,
);
}
until = val;
i++;
} else if (arg.startsWith('--')) {
throw new Error(`Unknown flag: ${arg}`);
} else {
const parsedLimit = parseInt(arg, 10);
if (!isNaN(parsedLimit) && parsedLimit > 0) {
limit = parsedLimit;
} else {
throw new Error(
`Invalid argument: "${arg}". Expected a positive number or --until flag.`,
);
}
}
}
}
return {
type: 'custom_dialog',
component: (
<TriageIssues
config={config}
initialLimit={limit}
until={until}
onExit={() => context.ui.removeComponent()}
/>
),
};
},
},
],
};
@@ -14,14 +14,14 @@ import { type HistoryItem } from '../types.js';
import { convertSessionToHistoryFormats } from '../hooks/useSessionBrowser.js';
import { revertFileChanges } from '../utils/rewindFileOps.js';
import { RewindOutcome } from '../components/RewindConfirmation.js';
import { checkExhaustive } from '../../utils/checks.js';
import type { Content } from '@google/genai';
import type {
ChatRecordingService,
GeminiClient,
import {
checkExhaustive,
coreEvents,
debugLogger,
type ChatRecordingService,
type GeminiClient,
} from '@google/gemini-cli-core';
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
/**
* Helper function to handle the core logic of rewinding a conversation.
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { shellsCommand } from './shellsCommand.js';
import type { CommandContext } from './types.js';
describe('shellsCommand', () => {
it('should call toggleBackgroundShell', async () => {
const toggleBackgroundShell = vi.fn();
const context = {
ui: {
toggleBackgroundShell,
},
} as unknown as CommandContext;
if (shellsCommand.action) {
await shellsCommand.action(context, '');
}
expect(toggleBackgroundShell).toHaveBeenCalled();
});
it('should have correct name and altNames', () => {
expect(shellsCommand.name).toBe('shells');
expect(shellsCommand.altNames).toContain('bashes');
});
it('should auto-execute', () => {
expect(shellsCommand.autoExecute).toBe(true);
});
});
@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandKind, type SlashCommand } from './types.js';
export const shellsCommand: SlashCommand = {
name: 'shells',
altNames: ['bashes'],
kind: CommandKind.BUILT_IN,
description: 'Toggle background shells view',
autoExecute: true,
action: async (context) => {
context.ui.toggleBackgroundShell();
},
};
@@ -58,6 +58,7 @@ describe('skillsCommand', () => {
(name: string) => skills.find((s) => s.name === name) ?? null,
),
}),
getContentGenerator: vi.fn(),
} as unknown as Config,
settings: {
merged: createTestMergedSettings({ skills: { disabled: [] } }),
@@ -367,7 +368,7 @@ describe('skillsCommand', () => {
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
text: 'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
}),
expect.any(Number),
);
@@ -385,7 +386,7 @@ describe('skillsCommand', () => {
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
text: 'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
}),
expect.any(Number),
);
+14 -6
View File
@@ -11,13 +11,15 @@ import {
CommandKind,
} from './types.js';
import {
MessageType,
type HistoryItemSkillsList,
type HistoryItemInfo,
type HistoryItemSkillsList,
MessageType,
} from '../types.js';
import { SettingScope } from '../../config/settings.js';
import { enableSkill, disableSkill } from '../../utils/skillSettings.js';
import { disableSkill, enableSkill } from '../../utils/skillSettings.js';
import { getAdminErrorMessage } from '@google/gemini-cli-core';
import { renderSkillActionFeedback } from '../../utils/skillUtils.js';
import { SettingScope } from '../../config/settings.js';
async function listAction(
context: CommandContext,
@@ -83,7 +85,10 @@ async function disableAction(
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
text: getAdminErrorMessage(
'Agent skills',
context.services.config ?? undefined,
),
},
Date.now(),
);
@@ -141,7 +146,10 @@ async function enableAction(
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
text: getAdminErrorMessage(
'Agent skills',
context.services.config ?? undefined,
),
},
Date.now(),
);
@@ -12,6 +12,17 @@ import { MessageType } from '../types.js';
import { formatDuration } from '../utils/formatters.js';
import type { Config } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
UserAccountManager: vi.fn().mockImplementation(() => ({
getCachedGoogleAccount: vi.fn().mockReturnValue('mock@example.com'),
})),
};
});
describe('statsCommand', () => {
let mockContext: CommandContext;
const startTime = new Date('2025-07-14T10:00:00.000Z');
@@ -40,6 +51,9 @@ describe('statsCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.STATS,
duration: expectedDuration,
selectedAuthType: '',
tier: undefined,
userEmail: 'mock@example.com',
});
});
@@ -48,8 +62,10 @@ describe('statsCommand', () => {
const mockQuota = { buckets: [] };
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
const mockGetUserTierName = vi.fn().mockReturnValue('Basic');
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
getUserTierName: mockGetUserTierName,
} as unknown as Config;
await statsCommand.action(mockContext, '');
@@ -58,6 +74,7 @@ describe('statsCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
quotas: mockQuota,
tier: 'Basic',
}),
);
});
@@ -73,6 +90,9 @@ describe('statsCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.MODEL_STATS,
selectedAuthType: '',
tier: undefined,
userEmail: 'mock@example.com',
});
});

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