Compare commits

...

47 Commits

Author SHA1 Message Date
Christian Gunderman afa763d985 test: trigger e2e workflow with prompt change 2026-03-02 16:15:28 -08:00
Christian Gunderman 37726d92b2 PR feedback. 2026-03-02 16:13:24 -08:00
Christian Gunderman 3be302afe3 Update scripts/changed_prompt.js
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-02 15:46:34 -08:00
Christian Gunderman 95a49be043 ci(evals): rename script and fix yamllint errors 2026-03-02 15:36:54 -08:00
Christian Gunderman 31cd47f230 ci(evals): use origin remote for trigger check 2026-03-02 15:25:04 -08:00
Christian Gunderman 4e1895986f ci(evals): add error logging to trigger script 2026-03-02 15:23:50 -08:00
Christian Gunderman 4462dbb861 refactor(ci): clean up check_evals_trigger.js 2026-03-02 15:22:53 -08:00
Christian Gunderman 90d0ea00c5 ci(evals): only run evals if prompts or tools changed 2026-03-02 15:19:54 -08:00
Adib234 01927a36d1 feat(plan): support annotating plans with feedback for iteration (#20876) 2026-03-02 23:03:59 +00:00
Shreya Keshive 06ddfa5c4c feat(admin): enable 30 day default retention for chat history & remove warning (#20853) 2026-03-02 22:44:49 +00:00
Christian Gunderman 3f7ef816f1 fix(core): increase default headers timeout to 5 minutes (#20890) 2026-03-02 22:36:58 +00:00
Jerop Kipruto d05ba11a31 refactor(core): replace manual syncPlanModeTools with declarative policy rules (#20596) 2026-03-02 22:30:50 +00:00
Hamdanbinhashim e43b1cff58 docs: fix broken markdown links in main README.md (#20300) 2026-03-02 21:51:52 +00:00
Allen Hutchison bb6d1a2775 feat(core): add tool name validation in TOML policy files (#19281)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-02 21:47:21 +00:00
Nayana Parameswarappa dd9ccc9807 Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider (#20121) 2026-03-02 21:37:44 +00:00
Pyush Sinha 8133d63ac6 refactor(cli): fully remove React anti patterns, improve type safety and fix UX oversights in SettingsDialog.tsx (#18963)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-03-02 21:30:58 +00:00
Sandy Tao 18d0375a7f feat(core): support authenticated A2A agent card discovery (#20622)
Co-authored-by: Adam Weidman <adamfweidman@google.com>
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com>
2026-03-02 21:29:31 +00:00
Keith Guerin 31ca57ec94 feat: redesign header to be compact with ASCII icon (#18713)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-03-02 21:12:17 +00:00
Abhi b7a8f0d1f9 fix(core): ensure subagents use qualified MCP tool names (#20801) 2026-03-02 21:12:13 +00:00
Abdul Tawab 1502e5cbc3 style(cli) : Dialog pattern for /hooks Command (#17930) 2026-03-02 21:12:05 +00:00
Christian Gunderman 7ca3a33f8b Subagent activity UX. (#17570) 2026-03-02 21:04:31 +00:00
Sandy Tao ce5a2d0760 feat(core): truncate large MCP tool output (#19365) 2026-03-02 21:01:49 +00:00
Sam Roberts aa321b3d8c Update CODEOWNERS for README.md reviewers (#20860) 2026-03-02 20:54:05 +00:00
David Pierce 3a7a6e1540 Add install as an option when extension is selected. (#20358) 2026-03-02 20:41:16 +00:00
Tommaso Sciortino 66530e44c8 document node limitation for shift+tab (#20877) 2026-03-02 20:31:52 +00:00
Christian Gunderman b034dcd412 Do not block CI on evals (#20870) 2026-03-02 20:31:02 +00:00
Aishanee Shah 659301ff83 feat(core): centralize read_file limits and update gemini-3 description (#20619) 2026-03-02 20:11:58 +00:00
Sandy Tao 446a4316c4 feat(core): implement HTTP authentication support for A2A remote agents (#20510)
Co-authored-by: Adam Weidman <adamfweidman@google.com>
2026-03-02 19:59:48 +00:00
Tommaso Sciortino 48412a068e Add /unassign support (#20864)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
2026-03-02 19:54:26 +00:00
Adib234 2e1efaebe4 fix(plan): deflake plan mode integration tests (#20477) 2026-03-02 19:51:44 +00:00
Sandy Tao 7c9fceba7f fix(core): reduce LLM-based loop detection false positives (#20701) 2026-03-02 19:08:15 +00:00
Adam Weidman 740efa2ac2 Merge User and Agent Card Descriptions #20849 (#20850) 2026-03-02 17:59:29 +00:00
Abhi 703759cfae fix(cli): allow sub-agent confirmation requests in UI while preventing background flicker (#20722) 2026-03-01 02:39:25 +00:00
Sehoon Shon 0063581e47 feat(skills): add github-issue-creator skill (#20709) 2026-02-28 23:22:22 +00:00
Sehoon Shon 6757d4b5c5 fix(cli): resolve autoThemeSwitching when background hasn't changed but theme mismatches (#20706) 2026-02-28 23:22:10 +00:00
Sandy Tao a153ff587b refactor(core): Extract tool parameter names as constants (#20460) 2026-02-28 21:27:54 +00:00
N. Taylor Mullen cd3a8c3f07 fix(cli): reset themeManager between tests to ensure isolation (#20598) 2026-02-28 19:45:31 +00:00
kartik b2214a6676 fix: acp/zed race condition between MCP initialisation and prompt (#20205)
Signed-off-by: Kartik Angiras <angiraskartik@gmail.com>
2026-02-28 17:33:08 +00:00
gemini-cli-robot 6c65a2d813 Changelog for v0.32.0-preview.0 (#20627)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-02-28 16:03:50 +00:00
Jagjeevan Kashid fae0639ba2 fix: use full paths for ACP diff payloads (#19539)
Signed-off-by: Jagjeevan Kashid <jagjeevandev97@gmail.com>
2026-02-28 15:54:44 +00:00
gemini-cli-robot 76f70d65ff Changelog for v0.31.0 (#20634)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-02-28 03:45:07 +00:00
gemini-cli-robot fb6ff847dd chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 (#20644) 2026-02-28 02:13:48 +00:00
Gal Zahavi 1ca5c05d0d fix(github): use robot PAT for automated PRs to pass CLA check (#20641) 2026-02-28 01:13:58 +00:00
Gal Zahavi 0c6c9c6a62 chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 (#20637) 2026-02-28 00:51:22 +00:00
Sehoon Shon a1367e9cdd fix(core): parse raw ASCII buffer strings in Gaxios errors (#20626) 2026-02-27 23:57:32 +00:00
Tommaso Sciortino c89d4f9c6c docs: add Windows PowerShell equivalents for environments and scripting (#20333) 2026-02-27 23:41:47 +00:00
Aditya Sharma 08ee136132 docs: fix typo in installation documentation (#20153) 2026-02-27 23:04:52 +00:00
181 changed files with 7147 additions and 4332 deletions
@@ -0,0 +1,76 @@
---
name: github-issue-creator
description:
Use this skill when asked to create a GitHub issue. It handles different issue
types (bug, feature, etc.) using repository templates and ensures proper
labeling.
---
# GitHub Issue Creator
This skill guides the creation of high-quality GitHub issues that adhere to the
repository's standards and use the appropriate templates.
## Workflow
Follow these steps to create a GitHub issue:
1. **Identify Issue Type**: Determine if the request is a bug report, feature
request, or other category.
2. **Locate Template**: Search for issue templates in
`.github/ISSUE_TEMPLATE/`.
- `bug_report.yml`
- `feature_request.yml`
- `website_issue.yml`
- If no relevant YAML template is found, look for `.md` templates in the same
directory.
3. **Read Template**: Read the content of the identified template file to
understand the required fields.
4. **Draft Content**: Draft the issue title and body/fields.
- If using a YAML template (form), prepare values for each `id` defined in
the template.
- If using a Markdown template, follow its structure exactly.
- **Default Label**: Always include the `🔒 maintainer only` label unless the
user explicitly requests otherwise.
5. **Create Issue**: Use the `gh` CLI to create the issue.
- **CRITICAL:** To avoid shell escaping and formatting issues with
multi-line Markdown or complex text, ALWAYS write the description/body to
a temporary file first.
**For Markdown Templates or Simple Body:**
```bash
# 1. Write the drafted content to a temporary file
# 2. Create the issue using the --body-file flag
gh issue create --title "Succinct title" --body-file <temp_file_path> --label "🔒 maintainer only"
# 3. Remove the temporary file
rm <temp_file_path>
```
**For YAML Templates (Forms):**
While `gh issue create` supports `--body-file`, YAML forms usually expect
key-value pairs via flags if you want to bypass the interactive prompt.
However, the most reliable non-interactive way to ensure formatting is
preserved for long text fields is to use the `--body` or `--body-file` if the
form has been converted to a standard body, OR to use the `--field` flags
for YAML forms.
*Note: For the `gemini-cli` repository which uses YAML forms, you can often
submit the content as a single body if a specific field-based submission is
not required by the automation.*
6. **Verify**: Confirm the issue was created successfully and provide the link
to the user.
## Principles
- **Clarity**: Titles should be descriptive and follow project conventions.
- **Defensive Formatting**: Always use temporary files with `--body-file` to
prevent newline and special character issues.
- **Maintainer Priority**: Default to internal/maintainer labels to keep the
backlog organized.
- **Completeness**: Provide all requested information (e.g., version info,
reproduction steps).
+1
View File
@@ -14,3 +14,4 @@
# Docs have a dedicated approver group in addition to maintainers
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
+8
View File
@@ -290,6 +290,7 @@ jobs:
with:
ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}'
fetch-depth: 0
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -302,7 +303,14 @@ jobs:
- name: 'Build project'
run: 'npm run build'
- name: 'Check if evals should run'
id: 'check_evals'
run: |
SHOULD_RUN=$(node scripts/changed_prompt.js)
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
- name: 'Run Evals (Required to pass)'
if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
run: 'npm run test:always_passing_evals'
-1
View File
@@ -117,7 +117,6 @@ jobs:
name: 'Slow E2E - Win'
runs-on: 'gemini-cli-windows-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
+41 -1
View File
@@ -25,7 +25,7 @@ jobs:
if: |-
github.repository == 'google-gemini/gemini-cli' &&
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '/assign')
(contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/unassign'))
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
@@ -38,6 +38,7 @@ jobs:
permission-issues: 'write'
- name: 'Assign issue to user'
if: "contains(github.event.comment.body, '/assign')"
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
@@ -108,3 +109,42 @@ jobs:
issue_number: issueNumber,
body: `👋 @${commenter}, you've been assigned to this issue! Thank you for taking the time to contribute. Make sure to check out our [contributing guidelines](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md).`
});
- name: 'Unassign issue from user'
if: "contains(github.event.comment.body, '/unassign')"
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |
const issueNumber = context.issue.number;
const commenter = context.actor;
const owner = context.repo.owner;
const repo = context.repo.repo;
const commentBody = context.payload.comment.body.trim();
if (commentBody !== '/unassign') {
return;
}
const issue = await github.rest.issues.get({
owner: owner,
repo: repo,
issue_number: issueNumber,
});
const isAssigned = issue.data.assignees.some(assignee => assignee.login === commenter);
if (isAssigned) {
await github.rest.issues.removeAssignees({
owner: owner,
repo: repo,
issue_number: issueNumber,
assignees: [commenter]
});
await github.rest.issues.createComment({
owner: owner,
repo: repo,
issue_number: issueNumber,
body: `👋 @${commenter}, you have been unassigned from this issue.`
});
}
+1 -1
View File
@@ -145,7 +145,7 @@ jobs:
branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
pr-title: 'chore/release: bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
pr-body: 'Automated version bump for nightly release.'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ steps.vars.outputs.is_dry_run }}'
working-directory: './release'
+2 -1
View File
@@ -335,6 +335,7 @@ jobs:
name: 'Create Nightly PR'
needs: ['publish-stable', 'calculate-versions']
runs-on: 'ubuntu-latest'
environment: "${{ github.event.inputs.environment || 'prod' }}"
permissions:
contents: 'write'
pull-requests: 'write'
@@ -397,7 +398,7 @@ jobs:
branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
pr-body: 'Automated version bump to prepare for the next nightly release.'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ github.event.inputs.dry_run }}'
- name: 'Create Issue on Failure'
+7 -4
View File
@@ -75,11 +75,14 @@ Replace `<PR_NUMBER>` with your pull request number. Authors are encouraged to
run this on their own PRs for self-review, and reviewers should use it to
augment their manual review process.
### Self assigning issues
### Self-assigning and unassigning issues
To assign an issue to yourself, simply add a comment with the text `/assign`.
The comment must contain only that text and nothing else. This command will
assign the issue to you, provided it is not already assigned.
To assign an issue to yourself, simply add a comment with the text `/assign`. To
unassign yourself from an issue, add a comment with the text `/unassign`.
The comment must contain only that text and nothing else. These commands will
assign or unassign the issue as requested, provided the conditions are met
(e.g., an issue must be unassigned to be assigned).
Please note that you can have a maximum of 3 issues assigned to you at any given
time.
+11 -9
View File
@@ -282,14 +282,14 @@ gemini
quickly.
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
auth configuration.
- [**Configuration Guide**](./docs/get-started/configuration.md) - Settings and
- [**Configuration Guide**](./docs/reference/configuration.md) - Settings and
customization.
- [**Keyboard Shortcuts**](./docs/cli/keyboard-shortcuts.md) - Productivity
tips.
- [**Keyboard Shortcuts**](./docs/reference/keyboard-shortcuts.md) -
Productivity tips.
### Core Features
- [**Commands Reference**](./docs/cli/commands.md) - All slash commands
- [**Commands Reference**](./docs/reference/commands.md) - All slash commands
(`/help`, `/chat`, etc).
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
reusable commands.
@@ -323,15 +323,16 @@ gemini
- [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a
corporate environment.
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
- [**Tools API Development**](./docs/core/tools-api.md) - Create custom tools.
- [**Tools API Development**](./docs/reference/tools-api.md) - Create custom
tools.
- [**Local development**](./docs/local-development.md) - Local development
tooling.
### Troubleshooting & Support
- [**Troubleshooting Guide**](./docs/troubleshooting.md) - Common issues and
solutions.
- [**FAQ**](./docs/faq.md) - Frequently asked questions.
- [**Troubleshooting Guide**](./docs/resources/troubleshooting.md) - Common
issues and solutions.
- [**FAQ**](./docs/resources/faq.md) - Frequently asked questions.
- Use `/bug` command to report issues directly from the CLI.
### Using MCP Servers
@@ -377,7 +378,8 @@ for planned features and priorities.
### Uninstall
See the [Uninstall Guide](docs/cli/uninstall.md) for removal instructions.
See the [Uninstall Guide](./docs/resources/uninstall.md) for removal
instructions.
## 📄 Legal
+21
View File
@@ -18,6 +18,27 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.31.0 - 2026-02-27
- **Gemini 3.1 Pro Preview:** Gemini CLI now supports the new Gemini 3.1 Pro
Preview model
([#19676](https://github.com/google-gemini/gemini-cli/pull/19676) by
@sehoon38).
- **Experimental Browser Agent:** We've introduced a new experimental browser
agent to interact with web pages
([#19284](https://github.com/google-gemini/gemini-cli/pull/19284) by
@gsquared94).
- **Policy Engine Updates:** The policy engine now supports project-level
policies, MCP server wildcards, and tool annotation matching
([#18682](https://github.com/google-gemini/gemini-cli/pull/18682) by
@Abhijit-2592,
[#20024](https://github.com/google-gemini/gemini-cli/pull/20024) by @jerop).
- **Web Fetch Improvements:** We've implemented an experimental direct web fetch
feature and added rate limiting to mitigate DDoS risks
([#19557](https://github.com/google-gemini/gemini-cli/pull/19557) by @mbleigh,
[#19567](https://github.com/google-gemini/gemini-cli/pull/19567) by
@mattKorwel).
## Announcements: v0.30.0 - 2026-02-25
- **SDK & Custom Skills:** Introduced the initial SDK package, enabling dynamic
+389 -310
View File
@@ -1,4 +1,4 @@
# Latest stable release: v0.30.1
# Latest stable release: v0.31.0
Released: February 27, 2026
@@ -11,326 +11,405 @@ npm install -g @google/gemini-cli
## Highlights
- **SDK & Custom Skills**: Introduced the initial SDK package, dynamic system
instructions, `SessionContext` for SDK tool calls, and support for custom
skills.
- **Policy Engine Enhancements**: Added a `--policy` flag for user-defined
policies, strict seatbelt profiles, and transitioned away from
`--allowed-tools`.
- **UI & Themes**: Introduced a generic searchable list for settings and
extensions, added Solarized Dark and Light themes, text wrapping capabilities
to markdown tables, and a clean UI toggle prototype.
- **Vim Support & Ctrl-Z**: Improved Vim support to provide a more complete
experience and added support for Ctrl-Z suspension.
- **Plan Mode & Tools**: Plan Mode now supports project exploration without
planning and skills can be enabled in plan mode. Tool output masking is
enabled by default, and core tool definitions have been centralized.
- **Gemini 3.1 Pro Preview:** Gemini CLI now supports the new Gemini 3.1 Pro
Preview model.
- **Experimental Browser Agent:** We've introduced a new experimental browser
agent to directly interact with web pages and retrieve context.
- **Policy Engine Updates:** The policy engine has been expanded to support
project-level policies, MCP server wildcards, and tool annotation matching,
providing greater control over tool executions.
- **Web Fetch Enhancements:** A new experimental direct web fetch tool has been
implemented, alongside rate-limiting features for enhanced security.
- **Improved Plan Mode:** Plan Mode now includes support for custom storage
directories, automatic model switching, and summarizing work after execution.
## What's Changed
- fix(patch): cherry-pick 58df1c6 to release/v0.30.0-pr-20374 [CONFLICTS] by
@gemini-cli-robot in
[#20567](https://github.com/google-gemini/gemini-cli/pull/20567)
- feat(ux): added text wrapping capabilities to markdown tables by @devr0306 in
[#18240](https://github.com/google-gemini/gemini-cli/pull/18240)
- Revert "fix(mcp): ensure MCP transport is closed to prevent memory leaks" by
@skeshive in [#18771](https://github.com/google-gemini/gemini-cli/pull/18771)
- chore(release): bump version to 0.30.0-nightly.20260210.a2174751d by
@gemini-cli-robot in
[#18772](https://github.com/google-gemini/gemini-cli/pull/18772)
- chore: cleanup unused and add unlisted dependencies in packages/core by
@adamfweidman in
[#18762](https://github.com/google-gemini/gemini-cli/pull/18762)
- chore(core): update activate_skill prompt verbiage to be more direct by
@NTaylorMullen in
[#18605](https://github.com/google-gemini/gemini-cli/pull/18605)
- Add autoconfigure memory usage setting to the dialog by @jacob314 in
[#18510](https://github.com/google-gemini/gemini-cli/pull/18510)
- fix(core): prevent race condition in policy persistence by @braddux in
[#18506](https://github.com/google-gemini/gemini-cli/pull/18506)
- fix(evals): prevent false positive in hierarchical memory test by
@Abhijit-2592 in
[#18777](https://github.com/google-gemini/gemini-cli/pull/18777)
- test(evals): mark all `save_memory` evals as `USUALLY_PASSES` due to
unreliability by @jerop in
[#18786](https://github.com/google-gemini/gemini-cli/pull/18786)
- feat(cli): add setting to hide shortcuts hint UI by @LyalinDotCom in
[#18562](https://github.com/google-gemini/gemini-cli/pull/18562)
- feat(core): formalize 5-phase sequential planning workflow by @jerop in
[#18759](https://github.com/google-gemini/gemini-cli/pull/18759)
- Introduce limits for search results. by @gundermanc in
[#18767](https://github.com/google-gemini/gemini-cli/pull/18767)
- fix(cli): allow closing debug console after auto-open via flicker by
- Use ranged reads and limited searches and fuzzy editing improvements by
@gundermanc in
[#19240](https://github.com/google-gemini/gemini-cli/pull/19240)
- Fix bottom border color by @jacob314 in
[#19266](https://github.com/google-gemini/gemini-cli/pull/19266)
- Release note generator fix by @g-samroberts in
[#19363](https://github.com/google-gemini/gemini-cli/pull/19363)
- test(evals): add behavioral tests for tool output masking by @NTaylorMullen in
[#19172](https://github.com/google-gemini/gemini-cli/pull/19172)
- docs: clarify preflight instructions in GEMINI.md by @NTaylorMullen in
[#19377](https://github.com/google-gemini/gemini-cli/pull/19377)
- feat(cli): add gemini --resume hint on exit by @Mag1ck in
[#16285](https://github.com/google-gemini/gemini-cli/pull/16285)
- fix: optimize height calculations for ask_user dialog by @jackwotherspoon in
[#19017](https://github.com/google-gemini/gemini-cli/pull/19017)
- feat(cli): add Alt+D for forward word deletion by @scidomino in
[#19300](https://github.com/google-gemini/gemini-cli/pull/19300)
- Disable failing eval test by @chrstnb in
[#19455](https://github.com/google-gemini/gemini-cli/pull/19455)
- fix(cli): support legacy onConfirm callback in ToolActionsContext by
@SandyTao520 in
[#18795](https://github.com/google-gemini/gemini-cli/pull/18795)
- feat(masking): enable tool output masking by default by @abhipatel12 in
[#18564](https://github.com/google-gemini/gemini-cli/pull/18564)
- perf(ui): optimize table rendering by memoizing styled characters by @devr0306
in [#18770](https://github.com/google-gemini/gemini-cli/pull/18770)
- feat: multi-line text answers in ask-user tool by @jackwotherspoon in
[#18741](https://github.com/google-gemini/gemini-cli/pull/18741)
- perf(cli): truncate large debug logs and limit message history by @mattKorwel
in [#18663](https://github.com/google-gemini/gemini-cli/pull/18663)
- fix(core): complete MCP discovery when configured servers are skipped by
[#19369](https://github.com/google-gemini/gemini-cli/pull/19369)
- chore(deps): bump tar from 7.5.7 to 7.5.8 by @.github/dependabot.yml[bot] in
[#19367](https://github.com/google-gemini/gemini-cli/pull/19367)
- fix(plan): allow safe fallback when experiment setting for plan is not enabled
but approval mode at startup is plan by @Adib234 in
[#19439](https://github.com/google-gemini/gemini-cli/pull/19439)
- Add explicit color-convert dependency by @chrstnb in
[#19460](https://github.com/google-gemini/gemini-cli/pull/19460)
- feat(devtools): migrate devtools package into monorepo by @SandyTao520 in
[#18936](https://github.com/google-gemini/gemini-cli/pull/18936)
- fix(core): clarify plan mode constraints and exit mechanism by @jerop in
[#19438](https://github.com/google-gemini/gemini-cli/pull/19438)
- feat(cli): add macOS run-event notifications (interactive only) by
@LyalinDotCom in
[#18586](https://github.com/google-gemini/gemini-cli/pull/18586)
- fix(core): cache CLI version to ensure consistency during sessions by
@sehoon38 in [#18793](https://github.com/google-gemini/gemini-cli/pull/18793)
- fix(cli): resolve double rendering in shpool and address vscode lint warnings
by @braddux in
[#18704](https://github.com/google-gemini/gemini-cli/pull/18704)
- feat(plan): document and validate Plan Mode policy overrides by @jerop in
[#18825](https://github.com/google-gemini/gemini-cli/pull/18825)
- Fix pressing any key to exit select mode. by @jacob314 in
[#18421](https://github.com/google-gemini/gemini-cli/pull/18421)
- fix(cli): update F12 behavior to only open drawer if browser fails by
[#19056](https://github.com/google-gemini/gemini-cli/pull/19056)
- Changelog for v0.29.0 by @gemini-cli-robot in
[#19361](https://github.com/google-gemini/gemini-cli/pull/19361)
- fix(ui): preventing empty history items from being added by @devr0306 in
[#19014](https://github.com/google-gemini/gemini-cli/pull/19014)
- Changelog for v0.30.0-preview.0 by @gemini-cli-robot in
[#19364](https://github.com/google-gemini/gemini-cli/pull/19364)
- feat(core): add support for MCP progress updates by @NTaylorMullen in
[#19046](https://github.com/google-gemini/gemini-cli/pull/19046)
- fix(core): ensure directory exists before writing conversation file by
@godwiniheuwa in
[#18429](https://github.com/google-gemini/gemini-cli/pull/18429)
- fix(ui): move margin from top to bottom in ToolGroupMessage by @imadraude in
[#17198](https://github.com/google-gemini/gemini-cli/pull/17198)
- fix(cli): treat unknown slash commands as regular input instead of showing
error by @skyvanguard in
[#17393](https://github.com/google-gemini/gemini-cli/pull/17393)
- feat(core): experimental in-progress steering hints (2 of 2) by @joshualitt in
[#19307](https://github.com/google-gemini/gemini-cli/pull/19307)
- docs(plan): add documentation for plan mode command by @Adib234 in
[#19467](https://github.com/google-gemini/gemini-cli/pull/19467)
- fix(core): ripgrep fails when pattern looks like ripgrep flag by @syvb in
[#18858](https://github.com/google-gemini/gemini-cli/pull/18858)
- fix(cli): disable auto-completion on Shift+Tab to preserve mode cycling by
@NTaylorMullen in
[#19451](https://github.com/google-gemini/gemini-cli/pull/19451)
- use issuer instead of authorization_endpoint for oauth discovery by
@garrettsparks in
[#17332](https://github.com/google-gemini/gemini-cli/pull/17332)
- feat(cli): include `/dir add` directories in @ autocomplete suggestions by
@jasmeetsb in [#19246](https://github.com/google-gemini/gemini-cli/pull/19246)
- feat(admin): Admin settings should only apply if adminControlsApplicable =
true and fetch errors should be fatal by @skeshive in
[#19453](https://github.com/google-gemini/gemini-cli/pull/19453)
- Format strict-development-rules command by @g-samroberts in
[#19484](https://github.com/google-gemini/gemini-cli/pull/19484)
- feat(core): centralize compatibility checks and add TrueColor detection by
@spencer426 in
[#19478](https://github.com/google-gemini/gemini-cli/pull/19478)
- Remove unused files and update index and sidebar. by @g-samroberts in
[#19479](https://github.com/google-gemini/gemini-cli/pull/19479)
- Migrate core render util to use xterm.js as part of the rendering loop. by
@jacob314 in [#19044](https://github.com/google-gemini/gemini-cli/pull/19044)
- Changelog for v0.30.0-preview.1 by @gemini-cli-robot in
[#19496](https://github.com/google-gemini/gemini-cli/pull/19496)
- build: replace deprecated built-in punycode with userland package by @jacob314
in [#19502](https://github.com/google-gemini/gemini-cli/pull/19502)
- Speculative fixes to try to fix react error. by @jacob314 in
[#19508](https://github.com/google-gemini/gemini-cli/pull/19508)
- fix spacing by @jacob314 in
[#19494](https://github.com/google-gemini/gemini-cli/pull/19494)
- fix(core): ensure user rejections update tool outcome for telemetry by
@abhiasap in [#18982](https://github.com/google-gemini/gemini-cli/pull/18982)
- fix(acp): Initialize config (#18897) by @Mervap in
[#18898](https://github.com/google-gemini/gemini-cli/pull/18898)
- fix(core): add error logging for IDE fetch failures by @yuvrajangadsingh in
[#17981](https://github.com/google-gemini/gemini-cli/pull/17981)
- feat(acp): support set_mode interface (#18890) by @Mervap in
[#18891](https://github.com/google-gemini/gemini-cli/pull/18891)
- fix(core): robust workspace-based IDE connection discovery by @ehedlund in
[#18443](https://github.com/google-gemini/gemini-cli/pull/18443)
- Deflake windows tests. by @jacob314 in
[#19511](https://github.com/google-gemini/gemini-cli/pull/19511)
- Fix: Avoid tool confirmation timeout when no UI listeners are present by
@pdHaku0 in [#17955](https://github.com/google-gemini/gemini-cli/pull/17955)
- format md file by @scidomino in
[#19474](https://github.com/google-gemini/gemini-cli/pull/19474)
- feat(cli): add experimental.useOSC52Copy setting by @scidomino in
[#19488](https://github.com/google-gemini/gemini-cli/pull/19488)
- feat(cli): replace loading phrases boolean with enum setting by @LyalinDotCom
in [#19347](https://github.com/google-gemini/gemini-cli/pull/19347)
- Update skill to adjust for generated results. by @g-samroberts in
[#19500](https://github.com/google-gemini/gemini-cli/pull/19500)
- Fix message too large issue. by @gundermanc in
[#19499](https://github.com/google-gemini/gemini-cli/pull/19499)
- fix(core): prevent duplicate tool approval entries in auto-saved.toml by
@Abhijit-2592 in
[#19487](https://github.com/google-gemini/gemini-cli/pull/19487)
- fix(core): resolve crash in ClearcutLogger when os.cpus() is empty by @Adib234
in [#19555](https://github.com/google-gemini/gemini-cli/pull/19555)
- chore(core): improve encapsulation and remove unused exports by @adamfweidman
in [#19556](https://github.com/google-gemini/gemini-cli/pull/19556)
- Revert "Add generic searchable list to back settings and extensions (… by
@chrstnb in [#19434](https://github.com/google-gemini/gemini-cli/pull/19434)
- fix(core): improve error type extraction for telemetry by @yunaseoul in
[#19565](https://github.com/google-gemini/gemini-cli/pull/19565)
- fix: remove extra padding in Composer by @jackwotherspoon in
[#19529](https://github.com/google-gemini/gemini-cli/pull/19529)
- feat(plan): support configuring custom plans storage directory by @jerop in
[#19577](https://github.com/google-gemini/gemini-cli/pull/19577)
- Migrate files to resource or references folder. by @g-samroberts in
[#19503](https://github.com/google-gemini/gemini-cli/pull/19503)
- feat(policy): implement project-level policy support by @Abhijit-2592 in
[#18682](https://github.com/google-gemini/gemini-cli/pull/18682)
- feat(core): Implement parallel FC for read only tools. by @joshualitt in
[#18791](https://github.com/google-gemini/gemini-cli/pull/18791)
- chore(skills): adds pr-address-comments skill to work on PR feedback by
@mbleigh in [#19576](https://github.com/google-gemini/gemini-cli/pull/19576)
- refactor(sdk): introduce session-based architecture by @mbleigh in
[#19180](https://github.com/google-gemini/gemini-cli/pull/19180)
- fix(ci): add fallback JSON extraction to issue triage workflow by @bdmorgan in
[#19593](https://github.com/google-gemini/gemini-cli/pull/19593)
- feat(core): refine Edit and WriteFile tool schemas for Gemini 3 by
@SandyTao520 in
[#18829](https://github.com/google-gemini/gemini-cli/pull/18829)
- feat(plan): allow skills to be enabled in plan mode by @Adib234 in
[#18817](https://github.com/google-gemini/gemini-cli/pull/18817)
- docs(plan): add documentation for plan mode tools by @jerop in
[#18827](https://github.com/google-gemini/gemini-cli/pull/18827)
- Remove experimental note in extension settings docs by @chrstnb in
[#18822](https://github.com/google-gemini/gemini-cli/pull/18822)
- Update prompt and grep tool definition to limit context size by @gundermanc in
[#18780](https://github.com/google-gemini/gemini-cli/pull/18780)
- docs(plan): add `ask_user` tool documentation by @jerop in
[#18830](https://github.com/google-gemini/gemini-cli/pull/18830)
- Revert unintended credentials exposure by @Adib234 in
[#18840](https://github.com/google-gemini/gemini-cli/pull/18840)
- feat(core): update internal utility models to Gemini 3 by @SandyTao520 in
[#18773](https://github.com/google-gemini/gemini-cli/pull/18773)
- feat(a2a): add value-resolver for auth credential resolution by @adamfweidman
in [#18653](https://github.com/google-gemini/gemini-cli/pull/18653)
- Removed getPlainTextLength by @devr0306 in
[#18848](https://github.com/google-gemini/gemini-cli/pull/18848)
- More grep prompt tweaks by @gundermanc in
[#18846](https://github.com/google-gemini/gemini-cli/pull/18846)
- refactor(cli): Reactive useSettingsStore hook by @psinha40898 in
[#14915](https://github.com/google-gemini/gemini-cli/pull/14915)
- fix(mcp): Ensure that stdio MCP server execution has the `GEMINI_CLI=1` env
variable populated. by @richieforeman in
[#18832](https://github.com/google-gemini/gemini-cli/pull/18832)
- fix(core): improve headless mode detection for flags and query args by @galz10
in [#18855](https://github.com/google-gemini/gemini-cli/pull/18855)
- refactor(cli): simplify UI and remove legacy inline tool confirmation logic by
@abhipatel12 in
[#18566](https://github.com/google-gemini/gemini-cli/pull/18566)
- feat(cli): deprecate --allowed-tools and excludeTools in favor of policy
engine by @Abhijit-2592 in
[#18508](https://github.com/google-gemini/gemini-cli/pull/18508)
- fix(workflows): improve maintainer detection for automated PR actions by
@bdmorgan in [#18869](https://github.com/google-gemini/gemini-cli/pull/18869)
- refactor(cli): consolidate useToolScheduler and delete legacy implementation
by @abhipatel12 in
[#18567](https://github.com/google-gemini/gemini-cli/pull/18567)
- Update changelog for v0.28.0 and v0.29.0-preview0 by @g-samroberts in
[#18819](https://github.com/google-gemini/gemini-cli/pull/18819)
- fix(core): ensure sub-agents are registered regardless of tools.allowed by
[#19476](https://github.com/google-gemini/gemini-cli/pull/19476)
- Changelog for v0.30.0-preview.3 by @gemini-cli-robot in
[#19585](https://github.com/google-gemini/gemini-cli/pull/19585)
- fix(plan): exclude EnterPlanMode tool from YOLO mode by @Adib234 in
[#19570](https://github.com/google-gemini/gemini-cli/pull/19570)
- chore: resolve build warnings and update dependencies by @mattKorwel in
[#18880](https://github.com/google-gemini/gemini-cli/pull/18880)
- feat(ui): add source indicators to slash commands by @ehedlund in
[#18839](https://github.com/google-gemini/gemini-cli/pull/18839)
- docs: refine Plan Mode documentation structure and workflow by @jerop in
[#19644](https://github.com/google-gemini/gemini-cli/pull/19644)
- Docs: Update release information regarding Gemini 3.1 by @jkcinouye in
[#19568](https://github.com/google-gemini/gemini-cli/pull/19568)
- fix(security): rate limit web_fetch tool to mitigate DDoS via prompt injection
by @mattKorwel in
[#19567](https://github.com/google-gemini/gemini-cli/pull/19567)
- Add initial implementation of /extensions explore command by @chrstnb in
[#19029](https://github.com/google-gemini/gemini-cli/pull/19029)
- fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) by
@maximus12793 in
[#19038](https://github.com/google-gemini/gemini-cli/pull/19038)
- Search updates by @alisa-alisa in
[#19482](https://github.com/google-gemini/gemini-cli/pull/19482)
- feat(cli): add support for numpad SS3 sequences by @scidomino in
[#19659](https://github.com/google-gemini/gemini-cli/pull/19659)
- feat(cli): enhance folder trust with configuration discovery and security
warnings by @galz10 in
[#19492](https://github.com/google-gemini/gemini-cli/pull/19492)
- feat(ui): improve startup warnings UX with dismissal and show-count limits by
@spencer426 in
[#19584](https://github.com/google-gemini/gemini-cli/pull/19584)
- feat(a2a): Add API key authentication provider by @adamfweidman in
[#19548](https://github.com/google-gemini/gemini-cli/pull/19548)
- Send accepted/removed lines with ACCEPT_FILE telemetry. by @gundermanc in
[#19670](https://github.com/google-gemini/gemini-cli/pull/19670)
- feat(models): support Gemini 3.1 Pro Preview and fixes by @sehoon38 in
[#19676](https://github.com/google-gemini/gemini-cli/pull/19676)
- feat(plan): enforce read-only constraints in Plan Mode by @mattKorwel in
[#19433](https://github.com/google-gemini/gemini-cli/pull/19433)
- fix(cli): allow perfect match @scripts/test-windows-paths.js completions to
submit on Enter by @spencer426 in
[#19562](https://github.com/google-gemini/gemini-cli/pull/19562)
- fix(core): treat 503 Service Unavailable as retryable quota error by @sehoon38
in [#19642](https://github.com/google-gemini/gemini-cli/pull/19642)
- Update sidebar.json for to allow top nav tabs. by @g-samroberts in
[#19595](https://github.com/google-gemini/gemini-cli/pull/19595)
- security: strip deceptive Unicode characters from terminal output by @ehedlund
in [#19026](https://github.com/google-gemini/gemini-cli/pull/19026)
- Fixes 'input.on' is not a function error in Gemini CLI by @gundermanc in
[#19691](https://github.com/google-gemini/gemini-cli/pull/19691)
- Revert "feat(ui): add source indicators to slash commands" by @ehedlund in
[#19695](https://github.com/google-gemini/gemini-cli/pull/19695)
- security: implement deceptive URL detection and disclosure in tool
confirmations by @ehedlund in
[#19288](https://github.com/google-gemini/gemini-cli/pull/19288)
- fix(core): restore auth consent in headless mode and add unit tests by
@ehedlund in [#19689](https://github.com/google-gemini/gemini-cli/pull/19689)
- Fix unsafe assertions in code_assist folder. by @gundermanc in
[#19706](https://github.com/google-gemini/gemini-cli/pull/19706)
- feat(cli): make JetBrains warning more specific by @jacob314 in
[#19687](https://github.com/google-gemini/gemini-cli/pull/19687)
- fix(cli): extensions dialog UX polish by @jacob314 in
[#19685](https://github.com/google-gemini/gemini-cli/pull/19685)
- fix(cli): use getDisplayString for manual model selection in dialog by
@sehoon38 in [#19726](https://github.com/google-gemini/gemini-cli/pull/19726)
- feat(policy): repurpose "Always Allow" persistence to workspace level by
@Abhijit-2592 in
[#19707](https://github.com/google-gemini/gemini-cli/pull/19707)
- fix(cli): re-enable CLI banner by @sehoon38 in
[#19741](https://github.com/google-gemini/gemini-cli/pull/19741)
- Disallow and suppress unsafe assignment by @gundermanc in
[#19736](https://github.com/google-gemini/gemini-cli/pull/19736)
- feat(core): migrate read_file to 1-based start_line/end_line parameters by
@adamfweidman in
[#19526](https://github.com/google-gemini/gemini-cli/pull/19526)
- feat(cli): improve CTRL+O experience for both standard and alternate screen
buffer (ASB) modes by @jwhelangoog in
[#19010](https://github.com/google-gemini/gemini-cli/pull/19010)
- Utilize pipelining of grep_search -> read_file to eliminate turns by
@gundermanc in
[#19574](https://github.com/google-gemini/gemini-cli/pull/19574)
- refactor(core): remove unsafe type assertions in error utils (Phase 1.1) by
@mattKorwel in
[#18870](https://github.com/google-gemini/gemini-cli/pull/18870)
- Show notification when there's a conflict with an extensions command by
@chrstnb in [#17890](https://github.com/google-gemini/gemini-cli/pull/17890)
- fix(cli): dismiss '?' shortcuts help on hotkeys and active states by
@LyalinDotCom in
[#18583](https://github.com/google-gemini/gemini-cli/pull/18583)
- fix(core): prioritize conditional policy rules and harden Plan Mode by
@Abhijit-2592 in
[#18882](https://github.com/google-gemini/gemini-cli/pull/18882)
- feat(core): refine Plan Mode system prompt for agentic execution by
[#19750](https://github.com/google-gemini/gemini-cli/pull/19750)
- Disallow unsafe returns. by @gundermanc in
[#19767](https://github.com/google-gemini/gemini-cli/pull/19767)
- fix(cli): filter subagent sessions from resume history by @abhipatel12 in
[#19698](https://github.com/google-gemini/gemini-cli/pull/19698)
- chore(lint): fix lint errors seen when running npm run lint by @abhipatel12 in
[#19844](https://github.com/google-gemini/gemini-cli/pull/19844)
- feat(core): remove unnecessary login verbiage from Code Assist auth by
@NTaylorMullen in
[#18799](https://github.com/google-gemini/gemini-cli/pull/18799)
- feat(plan): create metrics for usage of `AskUser` tool by @Adib234 in
[#18820](https://github.com/google-gemini/gemini-cli/pull/18820)
- feat(cli): support Ctrl-Z suspension by @scidomino in
[#18931](https://github.com/google-gemini/gemini-cli/pull/18931)
- fix(github-actions): use robot PAT for release creation to trigger release
notes by @SandyTao520 in
[#18794](https://github.com/google-gemini/gemini-cli/pull/18794)
- feat: add strict seatbelt profiles and remove unusable closed profiles by
[#19861](https://github.com/google-gemini/gemini-cli/pull/19861)
- fix(plan): time share by approval mode dashboard reporting negative time
shares by @Adib234 in
[#19847](https://github.com/google-gemini/gemini-cli/pull/19847)
- fix(core): allow any preview model in quota access check by @bdmorgan in
[#19867](https://github.com/google-gemini/gemini-cli/pull/19867)
- fix(core): prevent omission placeholder deletions in replace/write_file by
@nsalerni in [#19870](https://github.com/google-gemini/gemini-cli/pull/19870)
- fix(core): add uniqueness guard to edit tool by @Shivangisharma4 in
[#19890](https://github.com/google-gemini/gemini-cli/pull/19890)
- refactor(config): remove enablePromptCompletion from settings by @sehoon38 in
[#19974](https://github.com/google-gemini/gemini-cli/pull/19974)
- refactor(core): move session conversion logic to core by @abhipatel12 in
[#19972](https://github.com/google-gemini/gemini-cli/pull/19972)
- Fix: Persist manual model selection on restart #19864 by @Nixxx19 in
[#19891](https://github.com/google-gemini/gemini-cli/pull/19891)
- fix(core): increase default retry attempts and add quota error backoff by
@sehoon38 in [#19949](https://github.com/google-gemini/gemini-cli/pull/19949)
- feat(core): add policy chain support for Gemini 3.1 by @sehoon38 in
[#19991](https://github.com/google-gemini/gemini-cli/pull/19991)
- Updates command reference and /stats command. by @g-samroberts in
[#19794](https://github.com/google-gemini/gemini-cli/pull/19794)
- Fix for silent failures in non-interactive mode by @owenofbrien in
[#19905](https://github.com/google-gemini/gemini-cli/pull/19905)
- fix(plan): allow plan mode writes on Windows and fix prompt paths by @Adib234
in [#19658](https://github.com/google-gemini/gemini-cli/pull/19658)
- fix(core): prevent OAuth server crash on unexpected requests by @reyyanxahmed
in [#19668](https://github.com/google-gemini/gemini-cli/pull/19668)
- feat: Map tool kinds to explicit ACP.ToolKind values and update test … by
@sripasg in [#19547](https://github.com/google-gemini/gemini-cli/pull/19547)
- chore: restrict gemini-automted-issue-triage to only allow echo by @galz10 in
[#20047](https://github.com/google-gemini/gemini-cli/pull/20047)
- Allow ask headers longer than 16 chars by @scidomino in
[#20041](https://github.com/google-gemini/gemini-cli/pull/20041)
- fix(core): prevent state corruption in McpClientManager during collis by @h30s
in [#19782](https://github.com/google-gemini/gemini-cli/pull/19782)
- fix(bundling): copy devtools package to bundle for runtime resolution by
@SandyTao520 in
[#18876](https://github.com/google-gemini/gemini-cli/pull/18876)
- chore: cleanup unused and add unlisted dependencies in packages/a2a-server by
@adamfweidman in
[#18916](https://github.com/google-gemini/gemini-cli/pull/18916)
- fix(plan): isolate plan files per session by @Adib234 in
[#18757](https://github.com/google-gemini/gemini-cli/pull/18757)
- fix: character truncation in raw markdown mode by @jackwotherspoon in
[#18938](https://github.com/google-gemini/gemini-cli/pull/18938)
- feat(cli): prototype clean UI toggle and minimal-mode bleed-through by
@LyalinDotCom in
[#18683](https://github.com/google-gemini/gemini-cli/pull/18683)
- ui(polish) blend background color with theme by @jacob314 in
[#18802](https://github.com/google-gemini/gemini-cli/pull/18802)
- Add generic searchable list to back settings and extensions by @chrstnb in
[#18838](https://github.com/google-gemini/gemini-cli/pull/18838)
- feat(ui): align `AskUser` color scheme with UX spec by @jerop in
[#18943](https://github.com/google-gemini/gemini-cli/pull/18943)
- Hide AskUser tool validation errors from UI (agent self-corrects) by @jerop in
[#18954](https://github.com/google-gemini/gemini-cli/pull/18954)
- bug(cli) fix flicker due to AppContainer continuous initialization by
@jacob314 in [#18958](https://github.com/google-gemini/gemini-cli/pull/18958)
- feat(admin): Add admin controls documentation by @skeshive in
[#18644](https://github.com/google-gemini/gemini-cli/pull/18644)
- feat(cli): disable ctrl-s shortcut outside of alternate buffer mode by
@jacob314 in [#18887](https://github.com/google-gemini/gemini-cli/pull/18887)
- fix(vim): vim support that feels (more) complete by @ppgranger in
[#18755](https://github.com/google-gemini/gemini-cli/pull/18755)
- feat(policy): add --policy flag for user defined policies by @allenhutchison
in [#18500](https://github.com/google-gemini/gemini-cli/pull/18500)
- Update installation guide by @g-samroberts in
[#18823](https://github.com/google-gemini/gemini-cli/pull/18823)
- refactor(core): centralize tool definitions (Group 1: replace, search, grep)
by @aishaneeshah in
[#18944](https://github.com/google-gemini/gemini-cli/pull/18944)
- refactor(cli): finalize event-driven transition and remove interaction bridge
by @abhipatel12 in
[#18569](https://github.com/google-gemini/gemini-cli/pull/18569)
- Fix drag and drop escaping by @scidomino in
[#18965](https://github.com/google-gemini/gemini-cli/pull/18965)
- feat(sdk): initial package bootstrap for SDK by @mbleigh in
[#18861](https://github.com/google-gemini/gemini-cli/pull/18861)
- feat(sdk): implements SessionContext for SDK tool calls by @mbleigh in
[#18862](https://github.com/google-gemini/gemini-cli/pull/18862)
- fix(plan): make question type required in AskUser tool by @Adib234 in
[#18959](https://github.com/google-gemini/gemini-cli/pull/18959)
- fix(core): ensure --yolo does not force headless mode by @NTaylorMullen in
[#18976](https://github.com/google-gemini/gemini-cli/pull/18976)
- refactor(core): adopt `CoreToolCallStatus` enum for type safety by @jerop in
[#18998](https://github.com/google-gemini/gemini-cli/pull/18998)
- Enable in-CLI extension management commands for team by @chrstnb in
[#18957](https://github.com/google-gemini/gemini-cli/pull/18957)
- Adjust lint rules to avoid unnecessary warning. by @scidomino in
[#18970](https://github.com/google-gemini/gemini-cli/pull/18970)
- fix(vscode): resolve unsafe type assertion lint errors by @ehedlund in
[#19006](https://github.com/google-gemini/gemini-cli/pull/19006)
- Remove unnecessary eslint config file by @scidomino in
[#19015](https://github.com/google-gemini/gemini-cli/pull/19015)
- fix(core): Prevent loop detection false positives on lists with long shared
prefixes by @SandyTao520 in
[#18975](https://github.com/google-gemini/gemini-cli/pull/18975)
- feat(core): fallback to chat-base when using unrecognized models for chat by
@SandyTao520 in
[#19016](https://github.com/google-gemini/gemini-cli/pull/19016)
- docs: fix inconsistent commandRegex example in policy engine by @NTaylorMullen
in [#19027](https://github.com/google-gemini/gemini-cli/pull/19027)
- fix(plan): persist the approval mode in UI even when agent is thinking by
@Adib234 in [#18955](https://github.com/google-gemini/gemini-cli/pull/18955)
- feat(sdk): Implement dynamic system instructions by @mbleigh in
[#18863](https://github.com/google-gemini/gemini-cli/pull/18863)
- Docs: Refresh docs to organize and standardize reference materials. by
@jkcinouye in [#18403](https://github.com/google-gemini/gemini-cli/pull/18403)
- fix windows escaping (and broken tests) by @scidomino in
[#19011](https://github.com/google-gemini/gemini-cli/pull/19011)
- refactor: use `CoreToolCallStatus` in the the history data model by @jerop in
[#19033](https://github.com/google-gemini/gemini-cli/pull/19033)
- feat(cleanup): enable 30-day session retention by default by @skeshive in
[#18854](https://github.com/google-gemini/gemini-cli/pull/18854)
- feat(plan): hide plan write and edit operations on plans in Plan Mode by
@jerop in [#19012](https://github.com/google-gemini/gemini-cli/pull/19012)
- bug(ui) fix flicker refreshing background color by @jacob314 in
[#19041](https://github.com/google-gemini/gemini-cli/pull/19041)
- chore: fix dep vulnerabilities by @scidomino in
[#19036](https://github.com/google-gemini/gemini-cli/pull/19036)
- Revamp automated changelog skill by @g-samroberts in
[#18974](https://github.com/google-gemini/gemini-cli/pull/18974)
- feat(sdk): implement support for custom skills by @mbleigh in
[#19031](https://github.com/google-gemini/gemini-cli/pull/19031)
- refactor(core): complete centralization of core tool definitions by
[#19766](https://github.com/google-gemini/gemini-cli/pull/19766)
- feat(policy): Support MCP Server Wildcards in Policy Engine by @jerop in
[#20024](https://github.com/google-gemini/gemini-cli/pull/20024)
- docs(CONTRIBUTING): update React DevTools version to 6 by @mmgok in
[#20014](https://github.com/google-gemini/gemini-cli/pull/20014)
- feat(core): optimize tool descriptions and schemas for Gemini 3 by
@aishaneeshah in
[#18991](https://github.com/google-gemini/gemini-cli/pull/18991)
- feat: add /commands reload to refresh custom TOML commands by @korade-krushna
in [#19078](https://github.com/google-gemini/gemini-cli/pull/19078)
- fix(cli): wrap terminal capability queries in hidden sequence by @srithreepo
in [#19080](https://github.com/google-gemini/gemini-cli/pull/19080)
- fix(workflows): fix GitHub App token permissions for maintainer detection by
@bdmorgan in [#19139](https://github.com/google-gemini/gemini-cli/pull/19139)
- test: fix hook integration test flakiness on Windows CI by @NTaylorMullen in
[#18665](https://github.com/google-gemini/gemini-cli/pull/18665)
- fix(core): Encourage non-interactive flags for scaffolding commands by
@NTaylorMullen in
[#18804](https://github.com/google-gemini/gemini-cli/pull/18804)
- fix(core): propagate User-Agent header to setup-phase CodeAssist API calls by
@gsquared94 in
[#19182](https://github.com/google-gemini/gemini-cli/pull/19182)
- docs: document .agents/skills alias and discovery precedence by @kevmoo in
[#19166](https://github.com/google-gemini/gemini-cli/pull/19166)
- feat(cli): add loading state to new agents notification by @sehoon38 in
[#19190](https://github.com/google-gemini/gemini-cli/pull/19190)
- Add base branch to workflow. by @g-samroberts in
[#19189](https://github.com/google-gemini/gemini-cli/pull/19189)
- feat(cli): handle invalid model names in useQuotaAndFallback by @sehoon38 in
[#19222](https://github.com/google-gemini/gemini-cli/pull/19222)
- docs: custom themes in extensions by @jackwotherspoon in
[#19219](https://github.com/google-gemini/gemini-cli/pull/19219)
- Disable workspace settings when starting GCLI in the home directory. by
@kevinjwang1 in
[#19034](https://github.com/google-gemini/gemini-cli/pull/19034)
- feat(cli): refactor model command to support set and manage subcommands by
@sehoon38 in [#19221](https://github.com/google-gemini/gemini-cli/pull/19221)
- Add refresh/reload aliases to slash command subcommands by @korade-krushna in
[#19218](https://github.com/google-gemini/gemini-cli/pull/19218)
- refactor: consolidate development rules and add cli guidelines by @jacob314 in
[#19214](https://github.com/google-gemini/gemini-cli/pull/19214)
- chore(ui): remove outdated tip about model routing by @sehoon38 in
[#19226](https://github.com/google-gemini/gemini-cli/pull/19226)
- feat(core): support custom reasoning models by default by @NTaylorMullen in
[#19227](https://github.com/google-gemini/gemini-cli/pull/19227)
- Add Solarized Dark and Solarized Light themes by @rmedranollamas in
[#19064](https://github.com/google-gemini/gemini-cli/pull/19064)
- fix(telemetry): replace JSON.stringify with safeJsonStringify in file
exporters by @gsquared94 in
[#19244](https://github.com/google-gemini/gemini-cli/pull/19244)
- feat(telemetry): add keychain availability and token storage metrics by
@abhipatel12 in
[#18971](https://github.com/google-gemini/gemini-cli/pull/18971)
- feat(cli): update approval mode cycle order by @jerop in
[#19254](https://github.com/google-gemini/gemini-cli/pull/19254)
- refactor(cli): code review cleanup fix for tab+tab by @jacob314 in
[#18967](https://github.com/google-gemini/gemini-cli/pull/18967)
- feat(plan): support project exploration without planning when in plan mode by
@Adib234 in [#18992](https://github.com/google-gemini/gemini-cli/pull/18992)
- feat: add role-specific statistics to telemetry and UI (cont. #15234) by
@yunaseoul in [#18824](https://github.com/google-gemini/gemini-cli/pull/18824)
- feat(cli): remove Plan Mode from rotation when actively working by @jerop in
[#19262](https://github.com/google-gemini/gemini-cli/pull/19262)
- Fix side breakage where anchors don't work in slugs. by @g-samroberts in
[#19261](https://github.com/google-gemini/gemini-cli/pull/19261)
- feat(config): add setting to make directory tree context configurable by
@kevin-ramdass in
[#19053](https://github.com/google-gemini/gemini-cli/pull/19053)
- fix(acp): Wait for mcp initialization in acp (#18893) by @Mervap in
[#18894](https://github.com/google-gemini/gemini-cli/pull/18894)
- docs: format UTC times in releases doc by @pavan-sh in
[#18169](https://github.com/google-gemini/gemini-cli/pull/18169)
- Docs: Clarify extensions documentation. by @jkcinouye in
[#19277](https://github.com/google-gemini/gemini-cli/pull/19277)
- refactor(core): modularize tool definitions by model family by @aishaneeshah
in [#19269](https://github.com/google-gemini/gemini-cli/pull/19269)
- fix(paths): Add cross-platform path normalization by @spencer426 in
[#18939](https://github.com/google-gemini/gemini-cli/pull/18939)
- feat(core): experimental in-progress steering hints (1 of 3) by @joshualitt in
[#19008](https://github.com/google-gemini/gemini-cli/pull/19008)
- fix(patch): cherry-pick 261788c to release/v0.30.0-preview.0-pr-19453 to patch
version v0.30.0-preview.0 and create version 0.30.0-preview.1 by
[#19643](https://github.com/google-gemini/gemini-cli/pull/19643)
- feat(core): implement experimental direct web fetch by @mbleigh in
[#19557](https://github.com/google-gemini/gemini-cli/pull/19557)
- feat(core): replace expected_replacements with allow_multiple in replace tool
by @SandyTao520 in
[#20033](https://github.com/google-gemini/gemini-cli/pull/20033)
- fix(sandbox): harden image packaging integrity checks by @aviralgarg05 in
[#19552](https://github.com/google-gemini/gemini-cli/pull/19552)
- fix(core): allow environment variable expansion and explicit overrides for MCP
servers by @galz10 in
[#18837](https://github.com/google-gemini/gemini-cli/pull/18837)
- feat(policy): Implement Tool Annotation Matching in Policy Engine by @jerop in
[#20029](https://github.com/google-gemini/gemini-cli/pull/20029)
- fix(core): prevent utility calls from changing session active model by
@adamfweidman in
[#20035](https://github.com/google-gemini/gemini-cli/pull/20035)
- fix(cli): skip workspace policy loading when in home directory by
@Abhijit-2592 in
[#20054](https://github.com/google-gemini/gemini-cli/pull/20054)
- fix(scripts): Add Windows (win32/x64) support to lint.js by @ZafeerMahmood in
[#16193](https://github.com/google-gemini/gemini-cli/pull/16193)
- fix(a2a-server): Remove unsafe type assertions in agent by @Nixxx19 in
[#19723](https://github.com/google-gemini/gemini-cli/pull/19723)
- Fix: Handle corrupted token file gracefully when switching auth types (#19845)
by @Nixxx19 in
[#19850](https://github.com/google-gemini/gemini-cli/pull/19850)
- fix critical dep vulnerability by @scidomino in
[#20087](https://github.com/google-gemini/gemini-cli/pull/20087)
- Add new setting to configure maxRetries by @kevinjwang1 in
[#20064](https://github.com/google-gemini/gemini-cli/pull/20064)
- Stabilize tests. by @gundermanc in
[#20095](https://github.com/google-gemini/gemini-cli/pull/20095)
- make windows tests mandatory by @scidomino in
[#20096](https://github.com/google-gemini/gemini-cli/pull/20096)
- Add 3.1 pro preview to behavioral evals. by @gundermanc in
[#20088](https://github.com/google-gemini/gemini-cli/pull/20088)
- feat:PR-rate-limit by @JagjeevanAK in
[#19804](https://github.com/google-gemini/gemini-cli/pull/19804)
- feat(cli): allow expanding full details of MCP tool on approval by @y-okt in
[#19916](https://github.com/google-gemini/gemini-cli/pull/19916)
- feat(security): Introduce Conseca framework by @shrishabh in
[#13193](https://github.com/google-gemini/gemini-cli/pull/13193)
- fix(cli): Remove unsafe type assertions in activityLogger #19713 by @Nixxx19
in [#19745](https://github.com/google-gemini/gemini-cli/pull/19745)
- feat: implement AfterTool tail tool calls by @googlestrobe in
[#18486](https://github.com/google-gemini/gemini-cli/pull/18486)
- ci(actions): fix PR rate limiter excluding maintainers by @scidomino in
[#20117](https://github.com/google-gemini/gemini-cli/pull/20117)
- Shortcuts: Move SectionHeader title below top line and refine styling by
@keithguerin in
[#18721](https://github.com/google-gemini/gemini-cli/pull/18721)
- refactor(ui): Update and simplify use of gray colors in themes by @keithguerin
in [#20141](https://github.com/google-gemini/gemini-cli/pull/20141)
- fix punycode2 by @jacob314 in
[#20154](https://github.com/google-gemini/gemini-cli/pull/20154)
- feat(ide): add GEMINI_CLI_IDE_PID env var to override IDE process detection by
@kiryltech in [#15842](https://github.com/google-gemini/gemini-cli/pull/15842)
- feat(policy): Propagate Tool Annotations for MCP Servers by @jerop in
[#20083](https://github.com/google-gemini/gemini-cli/pull/20083)
- fix(a2a-server): pass allowedTools settings to core Config by @reyyanxahmed in
[#19680](https://github.com/google-gemini/gemini-cli/pull/19680)
- feat(mcp): add progress bar, throttling, and input validation for MCP tool
progress by @jasmeetsb in
[#19772](https://github.com/google-gemini/gemini-cli/pull/19772)
- feat(policy): centralize plan mode tool visibility in policy engine by @jerop
in [#20178](https://github.com/google-gemini/gemini-cli/pull/20178)
- feat(browser): implement experimental browser agent by @gsquared94 in
[#19284](https://github.com/google-gemini/gemini-cli/pull/19284)
- feat(plan): summarize work after executing a plan by @jerop in
[#19432](https://github.com/google-gemini/gemini-cli/pull/19432)
- fix(core): create new McpClient on restart to apply updated config by @h30s in
[#20126](https://github.com/google-gemini/gemini-cli/pull/20126)
- Changelog for v0.30.0-preview.5 by @gemini-cli-robot in
[#20107](https://github.com/google-gemini/gemini-cli/pull/20107)
- Update packages. by @jacob314 in
[#20152](https://github.com/google-gemini/gemini-cli/pull/20152)
- Fix extension env dir loading issue by @chrstnb in
[#20198](https://github.com/google-gemini/gemini-cli/pull/20198)
- restrict /assign to help-wanted issues by @scidomino in
[#20207](https://github.com/google-gemini/gemini-cli/pull/20207)
- feat(plan): inject message when user manually exits Plan mode by @jerop in
[#20203](https://github.com/google-gemini/gemini-cli/pull/20203)
- feat(extensions): enforce folder trust for local extension install by @galz10
in [#19703](https://github.com/google-gemini/gemini-cli/pull/19703)
- feat(hooks): adds support for RuntimeHook functions. by @mbleigh in
[#19598](https://github.com/google-gemini/gemini-cli/pull/19598)
- Docs: Update UI links. by @jkcinouye in
[#20224](https://github.com/google-gemini/gemini-cli/pull/20224)
- feat: prompt users to run /terminal-setup with yes/no by @ishaanxgupta in
[#16235](https://github.com/google-gemini/gemini-cli/pull/16235)
- fix: additional high vulnerabilities (minimatch, cross-spawn) by @adamfweidman
in [#20221](https://github.com/google-gemini/gemini-cli/pull/20221)
- feat(telemetry): Add context breakdown to API response event by @SandyTao520
in [#19699](https://github.com/google-gemini/gemini-cli/pull/19699)
- Docs: Add nested sub-folders for related topics by @g-samroberts in
[#20235](https://github.com/google-gemini/gemini-cli/pull/20235)
- feat(plan): support automatic model switching for Plan Mode by @jerop in
[#20240](https://github.com/google-gemini/gemini-cli/pull/20240)
- fix(patch): cherry-pick 58df1c6 to release/v0.31.0-preview.0-pr-20374 to patch
version v0.31.0-preview.0 and create version 0.31.0-preview.1 by
@gemini-cli-robot in
[#19490](https://github.com/google-gemini/gemini-cli/pull/19490)
- fix(patch): cherry-pick c43500c to release/v0.30.0-preview.1-pr-19502 to patch
version v0.30.0-preview.1 and create version 0.30.0-preview.2 by
[#20568](https://github.com/google-gemini/gemini-cli/pull/20568)
- fix(patch): cherry-pick ea48bd9 to release/v0.31.0-preview.1-pr-20577
[CONFLICTS] by @gemini-cli-robot in
[#20592](https://github.com/google-gemini/gemini-cli/pull/20592)
- fix(patch): cherry-pick 32e777f to release/v0.31.0-preview.2-pr-20531 to patch
version v0.31.0-preview.2 and create version 0.31.0-preview.3 by
@gemini-cli-robot in
[#19521](https://github.com/google-gemini/gemini-cli/pull/19521)
- fix(patch): cherry-pick aa9163d to release/v0.30.0-preview.3-pr-19991 to patch
version v0.30.0-preview.3 and create version 0.30.0-preview.4 by
@gemini-cli-robot in
[#20040](https://github.com/google-gemini/gemini-cli/pull/20040)
- fix(patch): cherry-pick 2c1d6f8 to release/v0.30.0-preview.4-pr-19369 to patch
version v0.30.0-preview.4 and create version 0.30.0-preview.5 by
@gemini-cli-robot in
[#20086](https://github.com/google-gemini/gemini-cli/pull/20086)
- fix(patch): cherry-pick d96bd05 to release/v0.30.0-preview.5-pr-19867 to patch
version v0.30.0-preview.5 and create version 0.30.0-preview.6 by
@gemini-cli-robot in
[#20112](https://github.com/google-gemini/gemini-cli/pull/20112)
[#20607](https://github.com/google-gemini/gemini-cli/pull/20607)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.29.7...v0.30.1
https://github.com/google-gemini/gemini-cli/compare/v0.30.1...v0.31.0
+187 -395
View File
@@ -1,4 +1,4 @@
# Preview release: v0.31.0-preview.1
# Preview release: v0.32.0-preview.0
Released: February 27, 2026
@@ -13,404 +13,196 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Plan Mode Enhancements**: Numerous additions including automatic model
switching, custom storage directory configuration, message injection upon
manual exit, enforcement of read-only constraints, and centralized tool
visibility in the policy engine.
- **Policy Engine Updates**: Project-level policy support added, alongside MCP
server wildcard support, tool annotation propagation and matching, and
workspace-level "Always Allow" persistence.
- **MCP Integration Improvements**: Better integration through support for MCP
progress updates with input validation and throttling, environment variable
expansion for servers, and full details expansion on tool approval.
- **CLI & Core UX Enhancements**: Several UI and quality-of-life updates such as
Alt+D for forward word deletion, macOS run-event notifications, enhanced
folder trust configurations with security warnings, improved startup warnings,
and a new experimental browser agent.
- **Security & Stability**: Introduced the Conseca framework, deceptive URL and
Unicode character detection, stricter access checks, rate limits on web fetch,
and resolved multiple dependency vulnerabilities.
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including
support for modifying plans in external editors, adaptive workflows based on
task complexity, and new integration tests.
- **Agent and Core Engine Updates**: Enabled the generalist agent, introduced
`Kind.Agent` for sub-agent classification, implemented task tracking
foundation, and improved Agent-to-Agent (A2A) streaming and content
extraction.
- **CLI & User Experience**: Introduced interactive shell autocompletion, added
a new verbosity mode for cleaner error reporting, enabled parallel loading of
extensions, and improved UI hints and shortcut handling.
- **Billing and Security**: Implemented G1 AI credits overage flow with enhanced
billing telemetry, updated the authentication handshake to specification, and
added support for a policy engine in extensions.
- **Stability and Bug Fixes**: Addressed numerous issues including 100% CPU
consumption by orphaned processes, enhanced retry logic for Code Assist,
reduced intrusive MCP errors, and merged duplicate imports across packages.
## What's Changed
- fix(patch): cherry-pick 58df1c6 to release/v0.31.0-preview.0-pr-20374 to patch
version v0.31.0-preview.0 and create version 0.31.0-preview.1 by
@gemini-cli-robot in
[#20568](https://github.com/google-gemini/gemini-cli/pull/20568)
- Use ranged reads and limited searches and fuzzy editing improvements by
@gundermanc in
[#19240](https://github.com/google-gemini/gemini-cli/pull/19240)
- Fix bottom border color by @jacob314 in
[#19266](https://github.com/google-gemini/gemini-cli/pull/19266)
- Release note generator fix by @g-samroberts in
[#19363](https://github.com/google-gemini/gemini-cli/pull/19363)
- test(evals): add behavioral tests for tool output masking by @NTaylorMullen in
[#19172](https://github.com/google-gemini/gemini-cli/pull/19172)
- docs: clarify preflight instructions in GEMINI.md by @NTaylorMullen in
[#19377](https://github.com/google-gemini/gemini-cli/pull/19377)
- feat(cli): add gemini --resume hint on exit by @Mag1ck in
[#16285](https://github.com/google-gemini/gemini-cli/pull/16285)
- fix: optimize height calculations for ask_user dialog by @jackwotherspoon in
[#19017](https://github.com/google-gemini/gemini-cli/pull/19017)
- feat(cli): add Alt+D for forward word deletion by @scidomino in
[#19300](https://github.com/google-gemini/gemini-cli/pull/19300)
- Disable failing eval test by @chrstnb in
[#19455](https://github.com/google-gemini/gemini-cli/pull/19455)
- fix(cli): support legacy onConfirm callback in ToolActionsContext by
- feat(plan): add integration tests for plan mode by @Adib234 in
[#20214](https://github.com/google-gemini/gemini-cli/pull/20214)
- fix(acp): update auth handshake to spec by @skeshive in
[#19725](https://github.com/google-gemini/gemini-cli/pull/19725)
- feat(core): implement robust A2A streaming reassembly and fix task continuity
by @adamfweidman in
[#20091](https://github.com/google-gemini/gemini-cli/pull/20091)
- feat(cli): load extensions in parallel by @scidomino in
[#20229](https://github.com/google-gemini/gemini-cli/pull/20229)
- Plumb the maxAttempts setting through Config args by @kevinjwang1 in
[#20239](https://github.com/google-gemini/gemini-cli/pull/20239)
- fix(cli): skip 404 errors in setup-github file downloads by @h30s in
[#20287](https://github.com/google-gemini/gemini-cli/pull/20287)
- fix(cli): expose model.name setting in settings dialog for persistence by
@achaljhawar in
[#19605](https://github.com/google-gemini/gemini-cli/pull/19605)
- docs: remove legacy cmd examples in favor of powershell by @scidomino in
[#20323](https://github.com/google-gemini/gemini-cli/pull/20323)
- feat(core): Enable model steering in workspace. by @joshualitt in
[#20343](https://github.com/google-gemini/gemini-cli/pull/20343)
- fix: remove trailing comma in issue triage workflow settings json by @Nixxx19
in [#20265](https://github.com/google-gemini/gemini-cli/pull/20265)
- feat(core): implement task tracker foundation and service by @anj-s in
[#19464](https://github.com/google-gemini/gemini-cli/pull/19464)
- test: support tests that include color information by @jacob314 in
[#20220](https://github.com/google-gemini/gemini-cli/pull/20220)
- feat(core): introduce Kind.Agent for sub-agent classification by @abhipatel12
in [#20369](https://github.com/google-gemini/gemini-cli/pull/20369)
- Changelog for v0.30.0 by @gemini-cli-robot in
[#20252](https://github.com/google-gemini/gemini-cli/pull/20252)
- Update changelog workflow to reject nightly builds by @g-samroberts in
[#20248](https://github.com/google-gemini/gemini-cli/pull/20248)
- Changelog for v0.31.0-preview.0 by @gemini-cli-robot in
[#20249](https://github.com/google-gemini/gemini-cli/pull/20249)
- feat(cli): hide workspace policy update dialog and auto-accept by default by
@Abhijit-2592 in
[#20351](https://github.com/google-gemini/gemini-cli/pull/20351)
- feat(core): rename grep_search include parameter to include_pattern by
@SandyTao520 in
[#19369](https://github.com/google-gemini/gemini-cli/pull/19369)
- chore(deps): bump tar from 7.5.7 to 7.5.8 by dependabot[bot] in
[#19367](https://github.com/google-gemini/gemini-cli/pull/19367)
- fix(plan): allow safe fallback when experiment setting for plan is not enabled
but approval mode at startup is plan by @Adib234 in
[#19439](https://github.com/google-gemini/gemini-cli/pull/19439)
- Add explicit color-convert dependency by @chrstnb in
[#19460](https://github.com/google-gemini/gemini-cli/pull/19460)
- feat(devtools): migrate devtools package into monorepo by @SandyTao520 in
[#18936](https://github.com/google-gemini/gemini-cli/pull/18936)
- fix(core): clarify plan mode constraints and exit mechanism by @jerop in
[#19438](https://github.com/google-gemini/gemini-cli/pull/19438)
- feat(cli): add macOS run-event notifications (interactive only) by
@LyalinDotCom in
[#19056](https://github.com/google-gemini/gemini-cli/pull/19056)
- Changelog for v0.29.0 by @gemini-cli-robot in
[#19361](https://github.com/google-gemini/gemini-cli/pull/19361)
- fix(ui): preventing empty history items from being added by @devr0306 in
[#19014](https://github.com/google-gemini/gemini-cli/pull/19014)
- Changelog for v0.30.0-preview.0 by @gemini-cli-robot in
[#19364](https://github.com/google-gemini/gemini-cli/pull/19364)
- feat(core): add support for MCP progress updates by @NTaylorMullen in
[#19046](https://github.com/google-gemini/gemini-cli/pull/19046)
- fix(core): ensure directory exists before writing conversation file by
@godwiniheuwa in
[#18429](https://github.com/google-gemini/gemini-cli/pull/18429)
- fix(ui): move margin from top to bottom in ToolGroupMessage by @imadraude in
[#17198](https://github.com/google-gemini/gemini-cli/pull/17198)
- fix(cli): treat unknown slash commands as regular input instead of showing
error by @skyvanguard in
[#17393](https://github.com/google-gemini/gemini-cli/pull/17393)
- feat(core): experimental in-progress steering hints (2 of 2) by @joshualitt in
[#19307](https://github.com/google-gemini/gemini-cli/pull/19307)
- docs(plan): add documentation for plan mode command by @Adib234 in
[#19467](https://github.com/google-gemini/gemini-cli/pull/19467)
- fix(core): ripgrep fails when pattern looks like ripgrep flag by @syvb in
[#18858](https://github.com/google-gemini/gemini-cli/pull/18858)
- fix(cli): disable auto-completion on Shift+Tab to preserve mode cycling by
@NTaylorMullen in
[#19451](https://github.com/google-gemini/gemini-cli/pull/19451)
- use issuer instead of authorization_endpoint for oauth discovery by
@garrettsparks in
[#17332](https://github.com/google-gemini/gemini-cli/pull/17332)
- feat(cli): include `/dir add` directories in @ autocomplete suggestions by
@jasmeetsb in [#19246](https://github.com/google-gemini/gemini-cli/pull/19246)
- feat(admin): Admin settings should only apply if adminControlsApplicable =
true and fetch errors should be fatal by @skeshive in
[#19453](https://github.com/google-gemini/gemini-cli/pull/19453)
- Format strict-development-rules command by @g-samroberts in
[#19484](https://github.com/google-gemini/gemini-cli/pull/19484)
- feat(core): centralize compatibility checks and add TrueColor detection by
[#20328](https://github.com/google-gemini/gemini-cli/pull/20328)
- feat(plan): support opening and modifying plan in external editor by @Adib234
in [#20348](https://github.com/google-gemini/gemini-cli/pull/20348)
- feat(cli): implement interactive shell autocompletion by @mrpmohiburrahman in
[#20082](https://github.com/google-gemini/gemini-cli/pull/20082)
- fix(core): allow /memory add to work in plan mode by @Jefftree in
[#20353](https://github.com/google-gemini/gemini-cli/pull/20353)
- feat(core): add HTTP 499 to retryable errors and map to RetryableQuotaError by
@bdmorgan in [#20432](https://github.com/google-gemini/gemini-cli/pull/20432)
- feat(core): Enable generalist agent by @joshualitt in
[#19665](https://github.com/google-gemini/gemini-cli/pull/19665)
- Updated tests in TableRenderer.test.tsx to use SVG snapshots by @devr0306 in
[#20450](https://github.com/google-gemini/gemini-cli/pull/20450)
- Refactor Github Action per b/485167538 by @google-admin in
[#19443](https://github.com/google-gemini/gemini-cli/pull/19443)
- fix(github): resolve actionlint and yamllint regressions from #19443 by @jerop
in [#20467](https://github.com/google-gemini/gemini-cli/pull/20467)
- fix: action var usage by @galz10 in
[#20492](https://github.com/google-gemini/gemini-cli/pull/20492)
- feat(core): improve A2A content extraction by @adamfweidman in
[#20487](https://github.com/google-gemini/gemini-cli/pull/20487)
- fix(cli): support quota error fallbacks for all authentication types by
@sehoon38 in [#20475](https://github.com/google-gemini/gemini-cli/pull/20475)
- fix(core): flush transcript for pure tool-call responses to ensure BeforeTool
hooks see complete state by @krishdef7 in
[#20419](https://github.com/google-gemini/gemini-cli/pull/20419)
- feat(plan): adapt planning workflow based on complexity of task by @jerop in
[#20465](https://github.com/google-gemini/gemini-cli/pull/20465)
- fix: prevent orphaned processes from consuming 100% CPU when terminal closes
by @yuvrajangadsingh in
[#16965](https://github.com/google-gemini/gemini-cli/pull/16965)
- feat(core): increase fetch timeout and fix [object Object] error
stringification by @bdmorgan in
[#20441](https://github.com/google-gemini/gemini-cli/pull/20441)
- [Gemma x Gemini CLI] Add an Experimental Gemma Router that uses a LiteRT-LM
shim into the Composite Model Classifier Strategy by @sidwan02 in
[#17231](https://github.com/google-gemini/gemini-cli/pull/17231)
- docs(plan): update documentation regarding supporting editing of plan files
during plan approval by @Adib234 in
[#20452](https://github.com/google-gemini/gemini-cli/pull/20452)
- test(cli): fix flaky ToolResultDisplay overflow test by @jwhelangoog in
[#20518](https://github.com/google-gemini/gemini-cli/pull/20518)
- ui(cli): reduce length of Ctrl+O hint by @jwhelangoog in
[#20490](https://github.com/google-gemini/gemini-cli/pull/20490)
- fix(ui): correct styled table width calculations by @devr0306 in
[#20042](https://github.com/google-gemini/gemini-cli/pull/20042)
- Avoid overaggressive unescaping by @scidomino in
[#20520](https://github.com/google-gemini/gemini-cli/pull/20520)
- feat(telemetry) Instrument traces with more attributes and make them available
to OTEL users by @heaventourist in
[#20237](https://github.com/google-gemini/gemini-cli/pull/20237)
- Add support for policy engine in extensions by @chrstnb in
[#20049](https://github.com/google-gemini/gemini-cli/pull/20049)
- Docs: Update to Terms of Service & FAQ by @jkcinouye in
[#20488](https://github.com/google-gemini/gemini-cli/pull/20488)
- Fix bottom border rendering for search and add a regression test. by @jacob314
in [#20517](https://github.com/google-gemini/gemini-cli/pull/20517)
- fix(core): apply retry logic to CodeAssistServer for all users by @bdmorgan in
[#20507](https://github.com/google-gemini/gemini-cli/pull/20507)
- Fix extension MCP server env var loading by @chrstnb in
[#20374](https://github.com/google-gemini/gemini-cli/pull/20374)
- feat(ui): add 'ctrl+o' hint to truncated content message by @jerop in
[#20529](https://github.com/google-gemini/gemini-cli/pull/20529)
- Fix flicker showing message to press ctrl-O again to collapse. by @jacob314 in
[#20414](https://github.com/google-gemini/gemini-cli/pull/20414)
- fix(cli): hide shortcuts hint while model is thinking or the user has typed a
prompt + add debounce to avoid flicker by @jacob314 in
[#19389](https://github.com/google-gemini/gemini-cli/pull/19389)
- feat(plan): update planning workflow to encourage multi-select with
descriptions of options by @Adib234 in
[#20491](https://github.com/google-gemini/gemini-cli/pull/20491)
- refactor(core,cli): useAlternateBuffer read from config by @psinha40898 in
[#20346](https://github.com/google-gemini/gemini-cli/pull/20346)
- fix(cli): ensure dialogs stay scrolled to bottom in alternate buffer mode by
@jacob314 in [#20527](https://github.com/google-gemini/gemini-cli/pull/20527)
- fix(core): revert auto-save of policies to user space by @Abhijit-2592 in
[#20531](https://github.com/google-gemini/gemini-cli/pull/20531)
- Demote unreliable test. by @gundermanc in
[#20571](https://github.com/google-gemini/gemini-cli/pull/20571)
- fix(core): handle optional response fields from code assist API by @sehoon38
in [#20345](https://github.com/google-gemini/gemini-cli/pull/20345)
- fix(cli): keep thought summary when loading phrases are off by @LyalinDotCom
in [#20497](https://github.com/google-gemini/gemini-cli/pull/20497)
- feat(cli): add temporary flag to disable workspace policies by @Abhijit-2592
in [#20523](https://github.com/google-gemini/gemini-cli/pull/20523)
- Disable expensive and scheduled workflows on personal forks by @dewitt in
[#20449](https://github.com/google-gemini/gemini-cli/pull/20449)
- Moved markdown parsing logic to a separate util file by @devr0306 in
[#20526](https://github.com/google-gemini/gemini-cli/pull/20526)
- fix(plan): prevent agent from using ask_user for shell command confirmation by
@Adib234 in [#20504](https://github.com/google-gemini/gemini-cli/pull/20504)
- fix(core): disable retries for code assist streaming requests by @sehoon38 in
[#20561](https://github.com/google-gemini/gemini-cli/pull/20561)
- feat(billing): implement G1 AI credits overage flow with billing telemetry by
@gsquared94 in
[#18590](https://github.com/google-gemini/gemini-cli/pull/18590)
- feat: better error messages by @gsquared94 in
[#20577](https://github.com/google-gemini/gemini-cli/pull/20577)
- fix(ui): persist expansion in AskUser dialog when navigating options by @jerop
in [#20559](https://github.com/google-gemini/gemini-cli/pull/20559)
- fix(cli): prevent sub-agent tool calls from leaking into UI by @abhipatel12 in
[#20580](https://github.com/google-gemini/gemini-cli/pull/20580)
- fix(cli): Shell autocomplete polish by @jacob314 in
[#20411](https://github.com/google-gemini/gemini-cli/pull/20411)
- Changelog for v0.31.0-preview.1 by @gemini-cli-robot in
[#20590](https://github.com/google-gemini/gemini-cli/pull/20590)
- Add slash command for promoting behavioral evals to CI blocking by @gundermanc
in [#20575](https://github.com/google-gemini/gemini-cli/pull/20575)
- Changelog for v0.30.1 by @gemini-cli-robot in
[#20589](https://github.com/google-gemini/gemini-cli/pull/20589)
- Add low/full CLI error verbosity mode for cleaner UI by @LyalinDotCom in
[#20399](https://github.com/google-gemini/gemini-cli/pull/20399)
- Disable Gemini PR reviews on draft PRs. by @gundermanc in
[#20362](https://github.com/google-gemini/gemini-cli/pull/20362)
- Docs: FAQ update by @jkcinouye in
[#20585](https://github.com/google-gemini/gemini-cli/pull/20585)
- fix(core): reduce intrusive MCP errors and deduplicate diagnostics by
@spencer426 in
[#19478](https://github.com/google-gemini/gemini-cli/pull/19478)
- Remove unused files and update index and sidebar. by @g-samroberts in
[#19479](https://github.com/google-gemini/gemini-cli/pull/19479)
- Migrate core render util to use xterm.js as part of the rendering loop. by
@jacob314 in [#19044](https://github.com/google-gemini/gemini-cli/pull/19044)
- Changelog for v0.30.0-preview.1 by @gemini-cli-robot in
[#19496](https://github.com/google-gemini/gemini-cli/pull/19496)
- build: replace deprecated built-in punycode with userland package by @jacob314
in [#19502](https://github.com/google-gemini/gemini-cli/pull/19502)
- Speculative fixes to try to fix react error. by @jacob314 in
[#19508](https://github.com/google-gemini/gemini-cli/pull/19508)
- fix spacing by @jacob314 in
[#19494](https://github.com/google-gemini/gemini-cli/pull/19494)
- fix(core): ensure user rejections update tool outcome for telemetry by
@abhiasap in [#18982](https://github.com/google-gemini/gemini-cli/pull/18982)
- fix(acp): Initialize config (#18897) by @Mervap in
[#18898](https://github.com/google-gemini/gemini-cli/pull/18898)
- fix(core): add error logging for IDE fetch failures by @yuvrajangadsingh in
[#17981](https://github.com/google-gemini/gemini-cli/pull/17981)
- feat(acp): support set_mode interface (#18890) by @Mervap in
[#18891](https://github.com/google-gemini/gemini-cli/pull/18891)
- fix(core): robust workspace-based IDE connection discovery by @ehedlund in
[#18443](https://github.com/google-gemini/gemini-cli/pull/18443)
- Deflake windows tests. by @jacob314 in
[#19511](https://github.com/google-gemini/gemini-cli/pull/19511)
- Fix: Avoid tool confirmation timeout when no UI listeners are present by
@pdHaku0 in [#17955](https://github.com/google-gemini/gemini-cli/pull/17955)
- format md file by @scidomino in
[#19474](https://github.com/google-gemini/gemini-cli/pull/19474)
- feat(cli): add experimental.useOSC52Copy setting by @scidomino in
[#19488](https://github.com/google-gemini/gemini-cli/pull/19488)
- feat(cli): replace loading phrases boolean with enum setting by @LyalinDotCom
in [#19347](https://github.com/google-gemini/gemini-cli/pull/19347)
- Update skill to adjust for generated results. by @g-samroberts in
[#19500](https://github.com/google-gemini/gemini-cli/pull/19500)
- Fix message too large issue. by @gundermanc in
[#19499](https://github.com/google-gemini/gemini-cli/pull/19499)
- fix(core): prevent duplicate tool approval entries in auto-saved.toml by
@Abhijit-2592 in
[#19487](https://github.com/google-gemini/gemini-cli/pull/19487)
- fix(core): resolve crash in ClearcutLogger when os.cpus() is empty by @Adib234
in [#19555](https://github.com/google-gemini/gemini-cli/pull/19555)
- chore(core): improve encapsulation and remove unused exports by @adamfweidman
in [#19556](https://github.com/google-gemini/gemini-cli/pull/19556)
- Revert "Add generic searchable list to back settings and extensions (… by
@chrstnb in [#19434](https://github.com/google-gemini/gemini-cli/pull/19434)
- fix(core): improve error type extraction for telemetry by @yunaseoul in
[#19565](https://github.com/google-gemini/gemini-cli/pull/19565)
- fix: remove extra padding in Composer by @jackwotherspoon in
[#19529](https://github.com/google-gemini/gemini-cli/pull/19529)
- feat(plan): support configuring custom plans storage directory by @jerop in
[#19577](https://github.com/google-gemini/gemini-cli/pull/19577)
- Migrate files to resource or references folder. by @g-samroberts in
[#19503](https://github.com/google-gemini/gemini-cli/pull/19503)
- feat(policy): implement project-level policy support by @Abhijit-2592 in
[#18682](https://github.com/google-gemini/gemini-cli/pull/18682)
- feat(core): Implement parallel FC for read only tools. by @joshualitt in
[#18791](https://github.com/google-gemini/gemini-cli/pull/18791)
- chore(skills): adds pr-address-comments skill to work on PR feedback by
@mbleigh in [#19576](https://github.com/google-gemini/gemini-cli/pull/19576)
- refactor(sdk): introduce session-based architecture by @mbleigh in
[#19180](https://github.com/google-gemini/gemini-cli/pull/19180)
- fix(ci): add fallback JSON extraction to issue triage workflow by @bdmorgan in
[#19593](https://github.com/google-gemini/gemini-cli/pull/19593)
- feat(core): refine Edit and WriteFile tool schemas for Gemini 3 by
@SandyTao520 in
[#19476](https://github.com/google-gemini/gemini-cli/pull/19476)
- Changelog for v0.30.0-preview.3 by @gemini-cli-robot in
[#19585](https://github.com/google-gemini/gemini-cli/pull/19585)
- fix(plan): exclude EnterPlanMode tool from YOLO mode by @Adib234 in
[#19570](https://github.com/google-gemini/gemini-cli/pull/19570)
- chore: resolve build warnings and update dependencies by @mattKorwel in
[#18880](https://github.com/google-gemini/gemini-cli/pull/18880)
- feat(ui): add source indicators to slash commands by @ehedlund in
[#18839](https://github.com/google-gemini/gemini-cli/pull/18839)
- docs: refine Plan Mode documentation structure and workflow by @jerop in
[#19644](https://github.com/google-gemini/gemini-cli/pull/19644)
- Docs: Update release information regarding Gemini 3.1 by @jkcinouye in
[#19568](https://github.com/google-gemini/gemini-cli/pull/19568)
- fix(security): rate limit web_fetch tool to mitigate DDoS via prompt injection
by @mattKorwel in
[#19567](https://github.com/google-gemini/gemini-cli/pull/19567)
- Add initial implementation of /extensions explore command by @chrstnb in
[#19029](https://github.com/google-gemini/gemini-cli/pull/19029)
- fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) by
@maximus12793 in
[#19038](https://github.com/google-gemini/gemini-cli/pull/19038)
- Search updates by @alisa-alisa in
[#19482](https://github.com/google-gemini/gemini-cli/pull/19482)
- feat(cli): add support for numpad SS3 sequences by @scidomino in
[#19659](https://github.com/google-gemini/gemini-cli/pull/19659)
- feat(cli): enhance folder trust with configuration discovery and security
warnings by @galz10 in
[#19492](https://github.com/google-gemini/gemini-cli/pull/19492)
- feat(ui): improve startup warnings UX with dismissal and show-count limits by
@spencer426 in
[#19584](https://github.com/google-gemini/gemini-cli/pull/19584)
- feat(a2a): Add API key authentication provider by @adamfweidman in
[#19548](https://github.com/google-gemini/gemini-cli/pull/19548)
- Send accepted/removed lines with ACCEPT_FILE telemetry. by @gundermanc in
[#19670](https://github.com/google-gemini/gemini-cli/pull/19670)
- feat(models): support Gemini 3.1 Pro Preview and fixes by @sehoon38 in
[#19676](https://github.com/google-gemini/gemini-cli/pull/19676)
- feat(plan): enforce read-only constraints in Plan Mode by @mattKorwel in
[#19433](https://github.com/google-gemini/gemini-cli/pull/19433)
- fix(cli): allow perfect match @scripts/test-windows-paths.js completions to
submit on Enter by @spencer426 in
[#19562](https://github.com/google-gemini/gemini-cli/pull/19562)
- fix(core): treat 503 Service Unavailable as retryable quota error by @sehoon38
in [#19642](https://github.com/google-gemini/gemini-cli/pull/19642)
- Update sidebar.json for to allow top nav tabs. by @g-samroberts in
[#19595](https://github.com/google-gemini/gemini-cli/pull/19595)
- security: strip deceptive Unicode characters from terminal output by @ehedlund
in [#19026](https://github.com/google-gemini/gemini-cli/pull/19026)
- Fixes 'input.on' is not a function error in Gemini CLI by @gundermanc in
[#19691](https://github.com/google-gemini/gemini-cli/pull/19691)
- Revert "feat(ui): add source indicators to slash commands" by @ehedlund in
[#19695](https://github.com/google-gemini/gemini-cli/pull/19695)
- security: implement deceptive URL detection and disclosure in tool
confirmations by @ehedlund in
[#19288](https://github.com/google-gemini/gemini-cli/pull/19288)
- fix(core): restore auth consent in headless mode and add unit tests by
@ehedlund in [#19689](https://github.com/google-gemini/gemini-cli/pull/19689)
- Fix unsafe assertions in code_assist folder. by @gundermanc in
[#19706](https://github.com/google-gemini/gemini-cli/pull/19706)
- feat(cli): make JetBrains warning more specific by @jacob314 in
[#19687](https://github.com/google-gemini/gemini-cli/pull/19687)
- fix(cli): extensions dialog UX polish by @jacob314 in
[#19685](https://github.com/google-gemini/gemini-cli/pull/19685)
- fix(cli): use getDisplayString for manual model selection in dialog by
@sehoon38 in [#19726](https://github.com/google-gemini/gemini-cli/pull/19726)
- feat(policy): repurpose "Always Allow" persistence to workspace level by
@Abhijit-2592 in
[#19707](https://github.com/google-gemini/gemini-cli/pull/19707)
- fix(cli): re-enable CLI banner by @sehoon38 in
[#19741](https://github.com/google-gemini/gemini-cli/pull/19741)
- Disallow and suppress unsafe assignment by @gundermanc in
[#19736](https://github.com/google-gemini/gemini-cli/pull/19736)
- feat(core): migrate read_file to 1-based start_line/end_line parameters by
@adamfweidman in
[#19526](https://github.com/google-gemini/gemini-cli/pull/19526)
- feat(cli): improve CTRL+O experience for both standard and alternate screen
buffer (ASB) modes by @jwhelangoog in
[#19010](https://github.com/google-gemini/gemini-cli/pull/19010)
- Utilize pipelining of grep_search -> read_file to eliminate turns by
@gundermanc in
[#19574](https://github.com/google-gemini/gemini-cli/pull/19574)
- refactor(core): remove unsafe type assertions in error utils (Phase 1.1) by
@mattKorwel in
[#19750](https://github.com/google-gemini/gemini-cli/pull/19750)
- Disallow unsafe returns. by @gundermanc in
[#19767](https://github.com/google-gemini/gemini-cli/pull/19767)
- fix(cli): filter subagent sessions from resume history by @abhipatel12 in
[#19698](https://github.com/google-gemini/gemini-cli/pull/19698)
- chore(lint): fix lint errors seen when running npm run lint by @abhipatel12 in
[#19844](https://github.com/google-gemini/gemini-cli/pull/19844)
- feat(core): remove unnecessary login verbiage from Code Assist auth by
@NTaylorMullen in
[#19861](https://github.com/google-gemini/gemini-cli/pull/19861)
- fix(plan): time share by approval mode dashboard reporting negative time
shares by @Adib234 in
[#19847](https://github.com/google-gemini/gemini-cli/pull/19847)
- fix(core): allow any preview model in quota access check by @bdmorgan in
[#19867](https://github.com/google-gemini/gemini-cli/pull/19867)
- fix(core): prevent omission placeholder deletions in replace/write_file by
@nsalerni in [#19870](https://github.com/google-gemini/gemini-cli/pull/19870)
- fix(core): add uniqueness guard to edit tool by @Shivangisharma4 in
[#19890](https://github.com/google-gemini/gemini-cli/pull/19890)
- refactor(config): remove enablePromptCompletion from settings by @sehoon38 in
[#19974](https://github.com/google-gemini/gemini-cli/pull/19974)
- refactor(core): move session conversion logic to core by @abhipatel12 in
[#19972](https://github.com/google-gemini/gemini-cli/pull/19972)
- Fix: Persist manual model selection on restart #19864 by @Nixxx19 in
[#19891](https://github.com/google-gemini/gemini-cli/pull/19891)
- fix(core): increase default retry attempts and add quota error backoff by
@sehoon38 in [#19949](https://github.com/google-gemini/gemini-cli/pull/19949)
- feat(core): add policy chain support for Gemini 3.1 by @sehoon38 in
[#19991](https://github.com/google-gemini/gemini-cli/pull/19991)
- Updates command reference and /stats command. by @g-samroberts in
[#19794](https://github.com/google-gemini/gemini-cli/pull/19794)
- Fix for silent failures in non-interactive mode by @owenofbrien in
[#19905](https://github.com/google-gemini/gemini-cli/pull/19905)
- fix(plan): allow plan mode writes on Windows and fix prompt paths by @Adib234
in [#19658](https://github.com/google-gemini/gemini-cli/pull/19658)
- fix(core): prevent OAuth server crash on unexpected requests by @reyyanxahmed
in [#19668](https://github.com/google-gemini/gemini-cli/pull/19668)
- feat: Map tool kinds to explicit ACP.ToolKind values and update test … by
@sripasg in [#19547](https://github.com/google-gemini/gemini-cli/pull/19547)
- chore: restrict gemini-automted-issue-triage to only allow echo by @galz10 in
[#20047](https://github.com/google-gemini/gemini-cli/pull/20047)
- Allow ask headers longer than 16 chars by @scidomino in
[#20041](https://github.com/google-gemini/gemini-cli/pull/20041)
- fix(core): prevent state corruption in McpClientManager during collis by @h30s
in [#19782](https://github.com/google-gemini/gemini-cli/pull/19782)
- fix(bundling): copy devtools package to bundle for runtime resolution by
@SandyTao520 in
[#19766](https://github.com/google-gemini/gemini-cli/pull/19766)
- feat(policy): Support MCP Server Wildcards in Policy Engine by @jerop in
[#20024](https://github.com/google-gemini/gemini-cli/pull/20024)
- docs(CONTRIBUTING): update React DevTools version to 6 by @mmgok in
[#20014](https://github.com/google-gemini/gemini-cli/pull/20014)
- feat(core): optimize tool descriptions and schemas for Gemini 3 by
@aishaneeshah in
[#19643](https://github.com/google-gemini/gemini-cli/pull/19643)
- feat(core): implement experimental direct web fetch by @mbleigh in
[#19557](https://github.com/google-gemini/gemini-cli/pull/19557)
- feat(core): replace expected_replacements with allow_multiple in replace tool
by @SandyTao520 in
[#20033](https://github.com/google-gemini/gemini-cli/pull/20033)
- fix(sandbox): harden image packaging integrity checks by @aviralgarg05 in
[#19552](https://github.com/google-gemini/gemini-cli/pull/19552)
- fix(core): allow environment variable expansion and explicit overrides for MCP
servers by @galz10 in
[#18837](https://github.com/google-gemini/gemini-cli/pull/18837)
- feat(policy): Implement Tool Annotation Matching in Policy Engine by @jerop in
[#20029](https://github.com/google-gemini/gemini-cli/pull/20029)
- fix(core): prevent utility calls from changing session active model by
@adamfweidman in
[#20035](https://github.com/google-gemini/gemini-cli/pull/20035)
- fix(cli): skip workspace policy loading when in home directory by
@Abhijit-2592 in
[#20054](https://github.com/google-gemini/gemini-cli/pull/20054)
- fix(scripts): Add Windows (win32/x64) support to lint.js by @ZafeerMahmood in
[#16193](https://github.com/google-gemini/gemini-cli/pull/16193)
- fix(a2a-server): Remove unsafe type assertions in agent by @Nixxx19 in
[#19723](https://github.com/google-gemini/gemini-cli/pull/19723)
- Fix: Handle corrupted token file gracefully when switching auth types (#19845)
by @Nixxx19 in
[#19850](https://github.com/google-gemini/gemini-cli/pull/19850)
- fix critical dep vulnerability by @scidomino in
[#20087](https://github.com/google-gemini/gemini-cli/pull/20087)
- Add new setting to configure maxRetries by @kevinjwang1 in
[#20064](https://github.com/google-gemini/gemini-cli/pull/20064)
- Stabilize tests. by @gundermanc in
[#20095](https://github.com/google-gemini/gemini-cli/pull/20095)
- make windows tests mandatory by @scidomino in
[#20096](https://github.com/google-gemini/gemini-cli/pull/20096)
- Add 3.1 pro preview to behavioral evals. by @gundermanc in
[#20088](https://github.com/google-gemini/gemini-cli/pull/20088)
- feat:PR-rate-limit by @JagjeevanAK in
[#19804](https://github.com/google-gemini/gemini-cli/pull/19804)
- feat(cli): allow expanding full details of MCP tool on approval by @y-okt in
[#19916](https://github.com/google-gemini/gemini-cli/pull/19916)
- feat(security): Introduce Conseca framework by @shrishabh in
[#13193](https://github.com/google-gemini/gemini-cli/pull/13193)
- fix(cli): Remove unsafe type assertions in activityLogger #19713 by @Nixxx19
in [#19745](https://github.com/google-gemini/gemini-cli/pull/19745)
- feat: implement AfterTool tail tool calls by @googlestrobe in
[#18486](https://github.com/google-gemini/gemini-cli/pull/18486)
- ci(actions): fix PR rate limiter excluding maintainers by @scidomino in
[#20117](https://github.com/google-gemini/gemini-cli/pull/20117)
- Shortcuts: Move SectionHeader title below top line and refine styling by
@keithguerin in
[#18721](https://github.com/google-gemini/gemini-cli/pull/18721)
- refactor(ui): Update and simplify use of gray colors in themes by @keithguerin
in [#20141](https://github.com/google-gemini/gemini-cli/pull/20141)
- fix punycode2 by @jacob314 in
[#20154](https://github.com/google-gemini/gemini-cli/pull/20154)
- feat(ide): add GEMINI_CLI_IDE_PID env var to override IDE process detection by
@kiryltech in [#15842](https://github.com/google-gemini/gemini-cli/pull/15842)
- feat(policy): Propagate Tool Annotations for MCP Servers by @jerop in
[#20083](https://github.com/google-gemini/gemini-cli/pull/20083)
- fix(a2a-server): pass allowedTools settings to core Config by @reyyanxahmed in
[#19680](https://github.com/google-gemini/gemini-cli/pull/19680)
- feat(mcp): add progress bar, throttling, and input validation for MCP tool
progress by @jasmeetsb in
[#19772](https://github.com/google-gemini/gemini-cli/pull/19772)
- feat(policy): centralize plan mode tool visibility in policy engine by @jerop
in [#20178](https://github.com/google-gemini/gemini-cli/pull/20178)
- feat(browser): implement experimental browser agent by @gsquared94 in
[#19284](https://github.com/google-gemini/gemini-cli/pull/19284)
- feat(plan): summarize work after executing a plan by @jerop in
[#19432](https://github.com/google-gemini/gemini-cli/pull/19432)
- fix(core): create new McpClient on restart to apply updated config by @h30s in
[#20126](https://github.com/google-gemini/gemini-cli/pull/20126)
- Changelog for v0.30.0-preview.5 by @gemini-cli-robot in
[#20107](https://github.com/google-gemini/gemini-cli/pull/20107)
- Update packages. by @jacob314 in
[#20152](https://github.com/google-gemini/gemini-cli/pull/20152)
- Fix extension env dir loading issue by @chrstnb in
[#20198](https://github.com/google-gemini/gemini-cli/pull/20198)
- restrict /assign to help-wanted issues by @scidomino in
[#20207](https://github.com/google-gemini/gemini-cli/pull/20207)
- feat(plan): inject message when user manually exits Plan mode by @jerop in
[#20203](https://github.com/google-gemini/gemini-cli/pull/20203)
- feat(extensions): enforce folder trust for local extension install by @galz10
in [#19703](https://github.com/google-gemini/gemini-cli/pull/19703)
- feat(hooks): adds support for RuntimeHook functions. by @mbleigh in
[#19598](https://github.com/google-gemini/gemini-cli/pull/19598)
- Docs: Update UI links. by @jkcinouye in
[#20224](https://github.com/google-gemini/gemini-cli/pull/20224)
- feat: prompt users to run /terminal-setup with yes/no by @ishaanxgupta in
[#16235](https://github.com/google-gemini/gemini-cli/pull/16235)
- fix: additional high vulnerabilities (minimatch, cross-spawn) by @adamfweidman
in [#20221](https://github.com/google-gemini/gemini-cli/pull/20221)
- feat(telemetry): Add context breakdown to API response event by @SandyTao520
in [#19699](https://github.com/google-gemini/gemini-cli/pull/19699)
- Docs: Add nested sub-folders for related topics by @g-samroberts in
[#20235](https://github.com/google-gemini/gemini-cli/pull/20235)
- feat(plan): support automatic model switching for Plan Mode by @jerop in
[#20240](https://github.com/google-gemini/gemini-cli/pull/20240)
[#20232](https://github.com/google-gemini/gemini-cli/pull/20232)
- docs: fix spelling typos in installation guide by @campox747 in
[#20579](https://github.com/google-gemini/gemini-cli/pull/20579)
- Promote stable tests to CI blocking. by @gundermanc in
[#20581](https://github.com/google-gemini/gemini-cli/pull/20581)
- feat(core): enable contiguous parallel admission for Kind.Agent tools by
@abhipatel12 in
[#20583](https://github.com/google-gemini/gemini-cli/pull/20583)
- Enforce import/no-duplicates as error by @Nixxx19 in
[#19797](https://github.com/google-gemini/gemini-cli/pull/19797)
- fix: merge duplicate imports in sdk and test-utils packages (1/4) by @Nixxx19
in [#19777](https://github.com/google-gemini/gemini-cli/pull/19777)
- fix: merge duplicate imports in a2a-server package (2/4) by @Nixxx19 in
[#19781](https://github.com/google-gemini/gemini-cli/pull/19781)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.30.0-preview.6...v0.31.0-preview.1
https://github.com/google-gemini/gemini-cli/compare/v0.31.0-preview.3...v0.32.0-preview.0
+12 -12
View File
@@ -5,18 +5,18 @@ and parameters.
## CLI commands
| Command | Description | Example |
| ---------------------------------- | ---------------------------------- | --------------------------------------------------- |
| `gemini` | Start interactive REPL | `gemini` |
| `gemini "query"` | Query non-interactively, then exit | `gemini "explain this project"` |
| `cat file \| gemini` | Process piped content | `cat logs.txt \| gemini` |
| `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) |
| Command | Description | Example |
| ---------------------------------- | ---------------------------------- | ------------------------------------------------------------ |
| `gemini` | Start interactive REPL | `gemini` |
| `gemini "query"` | Query non-interactively, then exit | `gemini "explain this project"` |
| `cat file \| gemini` | Process piped content | `cat logs.txt \| gemini`<br>`Get-Content logs.txt \| gemini` |
| `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
+9
View File
@@ -278,11 +278,20 @@ Let's create a global command that asks the model to refactor a piece of code.
First, ensure the user commands directory exists, then create a `refactor`
subdirectory for organization and the final TOML file.
**macOS/Linux**
```bash
mkdir -p ~/.gemini/commands/refactor
touch ~/.gemini/commands/refactor/pure.toml
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\commands\refactor"
New-Item -ItemType File -Force -Path "$env:USERPROFILE\.gemini\commands\refactor\pure.toml"
```
**2. Add the content to the file:**
Open `~/.gemini/commands/refactor/pure.toml` in your editor and add the
+19
View File
@@ -203,6 +203,15 @@ 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.
**PowerShell Profile (Windows alternative):**
On Windows, administrators can achieve similar results by adding the environment
variable to the system-wide or user-specific PowerShell profile:
```powershell
Add-Content -Path $PROFILE -Value '$env:GEMINI_CLI_SYSTEM_SETTINGS_PATH="C:\ProgramData\gemini-cli\settings.json"'
```
## User isolation in shared environments
In shared compute environments (like ML experiment runners or shared build
@@ -214,12 +223,22 @@ 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.
**macOS/Linux**
```bash
# Isolate state for a specific job
export GEMINI_CLI_HOME="/tmp/gemini-job-123"
gemini
```
**Windows (PowerShell)**
```powershell
# Isolate state for a specific job
$env:GEMINI_CLI_HOME="C:\temp\gemini-job-123"
gemini
```
## Restricting tool access
You can significantly enhance security by controlling which tools the Gemini
+3 -2
View File
@@ -109,8 +109,9 @@ structure, and consultation level are proportional to the task's complexity:
- **Iterate:** Provide feedback to refine the plan.
- **Refine manually:** Press **Ctrl + X** to open the plan file in your
[preferred external editor]. This allows you to manually refine the plan
steps before approval. The CLI will automatically refresh and show the
updated plan after you save and close the editor.
steps before approval. If you make any changes and save the file, the CLI
will automatically send the updated plan back to the agent for review and
iteration.
For more complex or specialized planning tasks, you can
[customize the planning workflow with skills](#customizing-planning-with-skills).
+42 -2
View File
@@ -55,12 +55,27 @@ from your organization's registry.
```bash
# Enable sandboxing with command flag
gemini -s -p "analyze the code structure"
```
# Use environment variable
**Use environment variable**
**macOS/Linux**
```bash
export GEMINI_SANDBOX=true
gemini -p "run the test suite"
```
# Configure in settings.json
**Windows (PowerShell)**
```powershell
$env:GEMINI_SANDBOX="true"
gemini -p "run the test suite"
```
**Configure in settings.json**
```json
{
"tools": {
"sandbox": "docker"
@@ -99,26 +114,51 @@ use cases.
To disable SELinux labeling for volume mounts, you can set the following:
**macOS/Linux**
```bash
export SANDBOX_FLAGS="--security-opt label=disable"
```
**Windows (PowerShell)**
```powershell
$env:SANDBOX_FLAGS="--security-opt label=disable"
```
Multiple flags can be provided as a space-separated string:
**macOS/Linux**
```bash
export SANDBOX_FLAGS="--flag1 --flag2=value"
```
**Windows (PowerShell)**
```powershell
$env:SANDBOX_FLAGS="--flag1 --flag2=value"
```
## Linux UID/GID handling
The sandbox automatically handles user permissions on Linux. Override these
permissions with:
**macOS/Linux**
```bash
export SANDBOX_SET_UID_GID=true # Force host UID/GID
export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping
```
**Windows (PowerShell)**
```powershell
$env:SANDBOX_SET_UID_GID="true" # Force host UID/GID
$env:SANDBOX_SET_UID_GID="false" # Disable UID/GID mapping
```
## Troubleshooting
### Common issues
+2 -2
View File
@@ -32,8 +32,8 @@ they appear in the UI.
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
### Output
+29
View File
@@ -103,23 +103,52 @@ Before using either method below, complete these steps:
1. Set your Google Cloud project ID:
- For telemetry in a separate project from inference:
**macOS/Linux**
```bash
export OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id"
```
**Windows (PowerShell)**
```powershell
$env:OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id"
```
- For telemetry in the same project as inference:
**macOS/Linux**
```bash
export GOOGLE_CLOUD_PROJECT="your-project-id"
```
**Windows (PowerShell)**
```powershell
$env:GOOGLE_CLOUD_PROJECT="your-project-id"
```
2. Authenticate with Google Cloud:
- If using a user account:
```bash
gcloud auth application-default login
```
- If using a service account:
**macOS/Linux**
```bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account.json"
```
**Windows (PowerShell)**
```powershell
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\service-account.json"
```
3. Make sure your account or service account has these IAM roles:
- Cloud Trace Agent
- Monitoring Metric Writer
+101 -5
View File
@@ -37,10 +37,18 @@ output.
Pipe a file:
**macOS/Linux**
```bash
cat error.log | gemini "Explain why this failed"
```
**Windows (PowerShell)**
```powershell
Get-Content error.log | gemini "Explain why this failed"
```
Pipe a command:
```bash
@@ -57,7 +65,10 @@ results to a file.
You have a folder of Python scripts and want to generate a `README.md` for each
one.
1. Save the following code as `generate_docs.sh`:
1. Save the following code as `generate_docs.sh` (or `generate_docs.ps1` for
Windows):
**macOS/Linux (`generate_docs.sh`)**
```bash
#!/bin/bash
@@ -72,13 +83,34 @@ one.
done
```
**Windows PowerShell (`generate_docs.ps1`)**
```powershell
# Loop through all Python files
Get-ChildItem -Filter *.py | ForEach-Object {
Write-Host "Generating docs for $($_.Name)..."
$newName = $_.Name -replace '\.py$', '.md'
# Ask Gemini CLI to generate the documentation and print it to stdout
gemini "Generate a Markdown documentation summary for @$($_.Name). Print the result to standard output." | Out-File -FilePath $newName -Encoding utf8
}
```
2. Make the script executable and run it in your directory:
**macOS/Linux**
```bash
chmod +x generate_docs.sh
./generate_docs.sh
```
**Windows (PowerShell)**
```powershell
.\generate_docs.ps1
```
This creates a corresponding Markdown file for every Python file in the
folder.
@@ -90,7 +122,10 @@ like `jq`. To get pure JSON data from the model, combine the
### Scenario: Extract and return structured data
1. Save the following script as `generate_json.sh`:
1. Save the following script as `generate_json.sh` (or `generate_json.ps1` for
Windows):
**macOS/Linux (`generate_json.sh`)**
```bash
#!/bin/bash
@@ -105,13 +140,35 @@ like `jq`. To get pure JSON data from the model, combine the
gemini --output-format json "Return a raw JSON object with keys 'version' and 'deps' from @package.json" | jq -r '.response' > data.json
```
2. Run `generate_json.sh`:
**Windows PowerShell (`generate_json.ps1`)**
```powershell
# Ensure we are in a project root
if (-not (Test-Path "package.json")) {
Write-Error "Error: package.json not found."
exit 1
}
# Extract data (requires jq installed, or you can use ConvertFrom-Json)
$output = gemini --output-format json "Return a raw JSON object with keys 'version' and 'deps' from @package.json" | ConvertFrom-Json
$output.response | Out-File -FilePath data.json -Encoding utf8
```
2. Run the script:
**macOS/Linux**
```bash
chmod +x generate_json.sh
./generate_json.sh
```
**Windows (PowerShell)**
```powershell
.\generate_json.ps1
```
3. Check `data.json`. The file should look like this:
```json
@@ -129,8 +186,10 @@ Use headless mode to perform custom, automated AI tasks.
### Scenario: Create a "Smart Commit" alias
You can add a function to your shell configuration (like `.zshrc` or `.bashrc`)
to create a `git commit` wrapper that writes the message for you.
You can add a function to your shell configuration to create a `git commit`
wrapper that writes the message for you.
**macOS/Linux (Bash/Zsh)**
1. Open your `.zshrc` file (or `.bashrc` if you use Bash) in your preferred
text editor.
@@ -170,6 +229,43 @@ to create a `git commit` wrapper that writes the message for you.
source ~/.zshrc
```
**Windows (PowerShell)**
1. Open your PowerShell profile in your preferred text editor.
```powershell
notepad $PROFILE
```
2. Scroll to the very bottom of the file and paste this code:
```powershell
function gcommit {
# Get the diff of staged changes
$diff = git diff --staged
if (-not $diff) {
Write-Host "No staged changes to commit."
return
}
# Ask Gemini to write the message
Write-Host "Generating commit message..."
$msg = $diff | gemini "Write a concise Conventional Commit message for this diff. Output ONLY the message."
# Commit with the generated message
git commit -m "$msg"
}
```
Save your file and exit.
3. Run this command to make the function available immediately:
```powershell
. $PROFILE
```
4. Use your new command:
```bash
+8
View File
@@ -20,10 +20,18 @@ Most MCP servers require authentication. For GitHub, you need a PAT.
**Read/Write** access to **Issues** and **Pull Requests**.
3. Store it in your environment:
**macOS/Linux**
```bash
export GITHUB_PERSONAL_ACCESS_TOKEN="github_pat_..."
```
**Windows (PowerShell)**
```powershell
$env:GITHUB_PERSONAL_ACCESS_TOKEN="github_pat_..."
```
## How to configure Gemini CLI
You tell Gemini about new servers by editing your `settings.json`.
@@ -14,10 +14,18 @@ responding correctly.
1. Run the following command to create the folders:
**macOS/Linux**
```bash
mkdir -p .gemini/skills/api-auditor/scripts
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path ".gemini\skills\api-auditor\scripts"
```
### Create the definition
1. Create a file at `.gemini/skills/api-auditor/SKILL.md`. This tells the agent
+16
View File
@@ -189,10 +189,18 @@ Custom commands create shortcuts for complex prompts.
1. Create a `commands` directory and a subdirectory for your command group:
**macOS/Linux**
```bash
mkdir -p commands/fs
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path "commands\fs"
```
2. Create a file named `commands/fs/grep-code.toml`:
```toml
@@ -252,10 +260,18 @@ Skills are activated only when needed, which saves context tokens.
1. Create a `skills` directory and a subdirectory for your skill:
**macOS/Linux**
```bash
mkdir -p skills/security-audit
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path "skills\security-audit"
```
2. Create a `skills/security-audit/SKILL.md` file:
```markdown
+86 -5
View File
@@ -78,11 +78,20 @@ To authenticate and use Gemini CLI with a Gemini API key:
2. Set the `GEMINI_API_KEY` environment variable to your key. For example:
**macOS/Linux**
```bash
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
**Windows (PowerShell)**
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -114,12 +123,22 @@ or the location where you want to run your jobs.
For example:
**macOS/Linux**
```bash
# Replace with your project ID and desired location (e.g., us-central1)
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
**Windows (PowerShell)**
```powershell
# Replace with your project ID and desired location (e.g., us-central1)
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
To make any Vertex AI environment variable settings persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -130,9 +149,17 @@ Consider this authentication method if you have Google Cloud CLI installed.
> **Note:** If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you
> must unset them to use ADC:
>
> **macOS/Linux**
>
> ```bash
> unset GOOGLE_API_KEY GEMINI_API_KEY
> ```
>
> **Windows (PowerShell)**
>
> ```powershell
> Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
> ```
1. Verify you have a Google Cloud project and Vertex AI API is enabled.
@@ -160,9 +187,17 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
> **Note:** If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you
> must unset them:
>
> **macOS/Linux**
>
> ```bash
> unset GOOGLE_API_KEY GEMINI_API_KEY
> ```
>
> **Windows (PowerShell)**
>
> ```powershell
> Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
> ```
1. [Create a service account and key](https://cloud.google.com/iam/docs/keys-create-delete)
and download the provided JSON file. Assign the "Vertex AI User" role to the
@@ -171,11 +206,20 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
2. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the JSON
file's absolute path. For example:
**macOS/Linux**
```bash
# Replace /path/to/your/keyfile.json with the actual path
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
```
**Windows (PowerShell)**
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
3. [Configure your Google Cloud Project](#set-gcp).
4. Start the CLI:
@@ -195,11 +239,20 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
2. Set the `GOOGLE_API_KEY` environment variable:
**macOS/Linux**
```bash
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
**Windows (PowerShell)**
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
> **Note:** If you see errors like
> `"API keys are not supported by this API..."`, your organization might
> restrict API key usage for this service. Try the other Vertex AI
@@ -243,11 +296,20 @@ To configure Gemini CLI to use a Google Cloud project, do the following:
For example, to set the `GOOGLE_CLOUD_PROJECT_ID` variable:
**macOS/Linux**
```bash
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
**Windows (PowerShell)**
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -257,16 +319,22 @@ To avoid setting environment variables for every terminal session, you can
persist them with the following methods:
1. **Add your environment variables to your shell configuration file:** Append
the `export ...` commands to your shell's startup file (e.g., `~/.bashrc`,
`~/.zshrc`, or `~/.profile`) and reload your shell (e.g.,
`source ~/.bashrc`).
the environment variable commands to your shell's startup file.
**macOS/Linux** (e.g., `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
```bash
# Example for .bashrc
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
**Windows (PowerShell)** (e.g., `$PROFILE`):
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
. $PROFILE
```
> **Warning:** Be aware that when you export API keys or service account
> paths in your shell configuration file, any process launched from that
> shell can read them.
@@ -274,10 +342,13 @@ persist them with the following methods:
2. **Use a `.env` file:** Create a `.gemini/.env` file in your project
directory or home directory. Gemini CLI automatically loads variables from
the first `.env` file it finds, searching up from the current directory,
then in `~/.gemini/.env` or `~/.env`. `.gemini/.env` is recommended.
then in your home directory's `.gemini/.env` (e.g., `~/.gemini/.env` or
`%USERPROFILE%\.gemini\.env`).
Example for user-wide settings:
**macOS/Linux**
```bash
mkdir -p ~/.gemini
cat >> ~/.gemini/.env <<'EOF'
@@ -286,6 +357,16 @@ persist them with the following methods:
EOF
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
@"
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
```
Variables are loaded from the first file found, not merged.
## Running in Google Cloud environments <a id="cloud-env"></a>
+1 -1
View File
@@ -13,7 +13,7 @@ installation methods, and release types.
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
- **Runtime:** Node.js 20.0.0+
- **Shell:** Bash or Zsh
- **Shell:** Bash, Zsh, or PowerShell
- **Location:**
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- **Internet connection required**
+33 -1
View File
@@ -167,6 +167,8 @@ try {
Run hook scripts manually with sample JSON input to verify they behave as
expected before hooking them up to the CLI.
**macOS/Linux**
```bash
# Create test input
cat > test-input.json << 'EOF'
@@ -187,7 +189,30 @@ cat test-input.json | .gemini/hooks/my-hook.sh
# Check exit code
echo "Exit code: $?"
```
**Windows (PowerShell)**
```powershell
# Create test input
@"
{
"session_id": "test-123",
"cwd": "C:\\temp\\test",
"hook_event_name": "BeforeTool",
"tool_name": "write_file",
"tool_input": {
"file_path": "test.txt",
"content": "Test content"
}
}
"@ | Out-File -FilePath test-input.json -Encoding utf8
# Test the hook
Get-Content test-input.json | .\.gemini\hooks\my-hook.ps1
# Check exit code
Write-Host "Exit code: $LASTEXITCODE"
```
### Check exit codes
@@ -333,7 +358,7 @@ tool_name=$(echo "$input" | jq -r '.tool_name')
### Make scripts executable
Always make hook scripts executable:
Always make hook scripts executable on macOS/Linux:
```bash
chmod +x .gemini/hooks/*.sh
@@ -341,6 +366,10 @@ chmod +x .gemini/hooks/*.js
```
**Windows Note**: On Windows, PowerShell scripts (`.ps1`) don't use `chmod`, but
you may need to ensure your execution policy allows them to run (e.g.,
`Set-ExecutionPolicy RemoteSigned -Scope CurrentUser`).
### Version control
Commit hooks to share with your team:
@@ -481,6 +510,9 @@ ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh
```
**Windows Note**: On Windows, ensure your execution policy allows running
scripts (e.g., `Get-ExecutionPolicy`).
**Verify script path:** Ensure the path in `settings.json` resolves correctly.
```bash
+24
View File
@@ -28,6 +28,8 @@ Create a directory for hooks and a simple logging script.
> This example uses `jq` to parse JSON. If you don't have it installed, you can
> perform similar logic using Node.js or Python.
**macOS/Linux**
```bash
mkdir -p .gemini/hooks
cat > .gemini/hooks/log-tools.sh << 'EOF'
@@ -52,6 +54,28 @@ EOF
chmod +x .gemini/hooks/log-tools.sh
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path ".gemini\hooks"
@"
# Read hook input from stdin
`$inputJson = `$input | Out-String | ConvertFrom-Json
# Extract tool name
`$toolName = `$inputJson.tool_name
# Log to stderr (visible in terminal if hook fails, or captured in logs)
[Console]::Error.WriteLine("Logging tool: `$toolName")
# Log to file
"[`$(Get-Date -Format 'o')] Tool executed: `$toolName" | Out-File -FilePath ".gemini\tool-log.txt" -Append -Encoding utf8
# Return success with empty JSON
"{}"
"@ | Out-File -FilePath ".gemini\hooks\log-tools.ps1" -Encoding utf8
```
## Exit Code Strategies
There are two ways to control or block an action in Gemini CLI:
+8
View File
@@ -177,10 +177,18 @@ standalone terminal and want to manually associate it with a specific IDE
instance, you can set the `GEMINI_CLI_IDE_PID` environment variable to the
process ID (PID) of your IDE.
**macOS/Linux**
```bash
export GEMINI_CLI_IDE_PID=12345
```
**Windows (PowerShell)**
```powershell
$env:GEMINI_CLI_IDE_PID=12345
```
When this variable is set, Gemini CLI will skip automatic detection and attempt
to connect using the provided PID.
+18 -14
View File
@@ -159,12 +159,12 @@ their corresponding top-level category object in your `settings.json` file.
- **`general.sessionRetention.enabled`** (boolean):
- **Description:** Enable automatic session cleanup
- **Default:** `false`
- **Default:** `true`
- **`general.sessionRetention.maxAge`** (string):
- **Description:** Automatically delete chats older than this time period
(e.g., "30d", "7d", "24h", "1w")
- **Default:** `undefined`
- **Default:** `"30d"`
- **`general.sessionRetention.maxCount`** (number):
- **Description:** Alternative: Maximum number of sessions to keep (most
@@ -175,11 +175,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Minimum retention period (safety limit, defaults to "1d")
- **Default:** `"1d"`
- **`general.sessionRetention.warningAcknowledged`** (boolean):
- **Description:** INTERNAL: Whether the user has acknowledged the session
retention warning
- **Default:** `false`
#### `output`
- **`output.format`** (enum):
@@ -1332,7 +1327,8 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_MODEL`**:
- Specifies the default Gemini model to use.
- Overrides the hardcoded default
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"`
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"` (Windows PowerShell:
`$env:GEMINI_MODEL="gemini-3-flash-preview"`)
- **`GEMINI_CLI_IDE_PID`**:
- Manually specifies the PID of the IDE process to use for integration. This
is useful when running Gemini CLI in a standalone terminal while still
@@ -1344,12 +1340,14 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- 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"`
- Example: `export GEMINI_CLI_HOME="/path/to/user/config"` (Windows
PowerShell: `$env:GEMINI_CLI_HOME="C:\path\to\user\config"`)
- **`GOOGLE_API_KEY`**:
- Your Google Cloud API key.
- Required for using Vertex AI in express mode.
- Ensure you have the necessary permissions.
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`.
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"` (Windows PowerShell:
`$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`).
- **`GOOGLE_CLOUD_PROJECT`**:
- Your Google Cloud Project ID.
- Required for using Code Assist or Vertex AI.
@@ -1360,18 +1358,23 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
you have `GOOGLE_CLOUD_PROJECT` set in your global environment in Cloud
Shell, it will be overridden by this default. To use a different project in
Cloud Shell, you must define `GOOGLE_CLOUD_PROJECT` in a `.env` file.
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"` (Windows
PowerShell: `$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`).
- **`GOOGLE_APPLICATION_CREDENTIALS`** (string):
- **Description:** The path to your Google Application Credentials JSON file.
- **Example:**
`export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
(Windows PowerShell:
`$env:GOOGLE_APPLICATION_CREDENTIALS="C:\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"`
- Example: `export GOOGLE_GENAI_API_VERSION="v1"` (Windows PowerShell:
`$env: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"`.
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"` (Windows
PowerShell: `$env:OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`).
- **`GEMINI_TELEMETRY_ENABLED`**:
- Set to `true` or `1` to enable telemetry. Any other value is treated as
disabling it.
@@ -1399,7 +1402,8 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GOOGLE_CLOUD_LOCATION`**:
- Your Google Cloud Project Location (e.g., us-central1).
- Required for using Vertex AI in non-express mode.
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"` (Windows
PowerShell: `$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`).
- **`GEMINI_SANDBOX`**:
- Alternative to the `sandbox` setting in `settings.json`.
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
+10
View File
@@ -152,3 +152,13 @@ available combinations.
inline when the cursor is over the placeholder.
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
view full content inline. Double-click again to collapse.
## Limitations
- On [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal):
- `shift+enter` is not supported.
- `shift+tab`
[is not supported](https://github.com/google-gemini/gemini-cli/issues/20314)
on Node 20 and earlier versions of Node 22.
- On macOS's [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>):
- `shift+enter` is not supported.
+10
View File
@@ -10,9 +10,19 @@ confirmation.
To create your first policy:
1. **Create the policy directory** if it doesn't exist:
**macOS/Linux**
```bash
mkdir -p ~/.gemini/policies
```
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\policies"
```
2. **Create a new policy file** (e.g., `~/.gemini/policies/my-rules.toml`). You
can use any filename ending in `.toml`; all such files in this directory
will be loaded and combined:
+8
View File
@@ -88,10 +88,18 @@ You can configure your Google Cloud Project ID using an environment variable.
Set the `GOOGLE_CLOUD_PROJECT` environment variable in your shell:
**macOS/Linux**
```bash
export GOOGLE_CLOUD_PROJECT="your-project-id"
```
**Windows (PowerShell)**
```powershell
$env:GOOGLE_CLOUD_PROJECT="your-project-id"
```
To make this setting permanent, add this line to your shell's startup file
(e.g., `~/.bashrc`, `~/.zshrc`).
+4 -1
View File
@@ -55,10 +55,13 @@ topics on:
- Set the `NODE_USE_SYSTEM_CA=1` environment variable to tell Node.js to use
the operating system's native certificate store (where corporate
certificates are typically already installed).
- Example: `export NODE_USE_SYSTEM_CA=1`
- Example: `export NODE_USE_SYSTEM_CA=1` (Windows PowerShell:
`$env:NODE_USE_SYSTEM_CA=1`)
- Set the `NODE_EXTRA_CA_CERTS` environment variable to the absolute path of
your corporate root CA certificate file.
- Example: `export NODE_EXTRA_CA_CERTS=/path/to/your/corporate-ca.crt`
(Windows PowerShell:
`$env:NODE_EXTRA_CA_CERTS="C:\path\to\your\corporate-ca.crt"`)
## Common error messages and solutions
+3 -1
View File
@@ -81,7 +81,9 @@ describe('JSON output', () => {
const message = (thrown as Error).message;
// Use a regex to find the first complete JSON object in the string
const jsonMatch = message.match(/{[\s\S]*}/);
// We expect the JSON to start with a quote (e.g. {"error": ...}) to avoid
// matching random error objects printed to stderr (like ENOENT).
const jsonMatch = message.match(/{\s*"[\s\S]*}/);
// Fail if no JSON-like text was found
expect(
+87 -34
View File
@@ -4,8 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, checkModelOutputContent } from './test-helper.js';
import { TestRig, checkModelOutputContent, GEMINI_DIR } from './test-helper.js';
describe('Plan Mode', () => {
let rig: TestRig;
@@ -62,50 +64,98 @@ describe('Plan Mode', () => {
});
});
it.skip('should allow write_file only in the plans directory in plan mode', async () => {
await rig.setup(
'should allow write_file only in the plans directory in plan mode',
{
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
allowed: ['write_file'],
it('should allow write_file to the plans directory in plan mode', async () => {
const plansDir = '.gemini/tmp/foo/123/plans';
const testName =
'should allow write_file to the plans directory in plan mode';
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
},
general: { defaultApprovalMode: 'plan' },
},
},
);
// We ask the agent to create a plan for a feature, which should trigger a write_file in the plans directory.
// Verify that write_file outside of plan directory fails
await rig.run({
approvalMode: 'plan',
stdin:
'Create a file called plan.md in the plans directory. Then create a file called hello.txt in the current directory',
});
const toolLogs = rig.readToolLogs();
const writeLogs = toolLogs.filter(
(l) => l.toolRequest.name === 'write_file',
// Disable the interactive terminal setup prompt in tests
writeFileSync(
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
);
const planWrite = writeLogs.find(
const run = await rig.runInteractive({
approvalMode: 'plan',
});
await run.type('Create a file called plan.md in the plans directory.');
await run.type('\r');
await rig.expectToolCallSuccess(['write_file'], 30000, (args) =>
args.includes('plan.md'),
);
const toolLogs = rig.readToolLogs();
const planWrite = toolLogs.find(
(l) =>
l.toolRequest.name === 'write_file' &&
l.toolRequest.args.includes('plans') &&
l.toolRequest.args.includes('plan.md'),
);
expect(planWrite?.toolRequest.success).toBe(true);
});
const blockedWrite = writeLogs.find((l) =>
l.toolRequest.args.includes('hello.txt'),
it('should deny write_file to non-plans directory in plan mode', async () => {
const plansDir = '.gemini/tmp/foo/123/plans';
const testName =
'should deny write_file to non-plans directory in plan mode';
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
},
},
},
});
// Disable the interactive terminal setup prompt in tests
writeFileSync(
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
);
// Model is undeterministic, sometimes a blocked write appears in tool logs and sometimes it doesn't
if (blockedWrite) {
expect(blockedWrite?.toolRequest.success).toBe(false);
}
const run = await rig.runInteractive({
approvalMode: 'plan',
});
expect(planWrite?.toolRequest.success).toBe(true);
await run.type('Create a file called hello.txt in the current directory.');
await run.type('\r');
const toolLogs = rig.readToolLogs();
const writeLog = toolLogs.find(
(l) =>
l.toolRequest.name === 'write_file' &&
l.toolRequest.args.includes('hello.txt'),
);
// In Plan Mode, writes outside the plans directory should be blocked.
// Model is undeterministic, sometimes it doesn't even try, but if it does, it must fail.
if (writeLog) {
expect(writeLog.toolRequest.success).toBe(false);
}
});
it('should be able to enter plan mode from default mode', async () => {
@@ -119,6 +169,12 @@ describe('Plan Mode', () => {
},
});
// Disable the interactive terminal setup prompt in tests
writeFileSync(
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
);
// Start in default mode and ask to enter plan mode.
await rig.run({
approvalMode: 'default',
@@ -126,10 +182,7 @@ describe('Plan Mode', () => {
'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
});
const enterPlanCallFound = await rig.waitForToolCall(
'enter_plan_mode',
10000,
);
const enterPlanCallFound = await rig.waitForToolCall('enter_plan_mode');
expect(enterPlanCallFound, 'Expected enter_plan_mode to be called').toBe(
true,
);
+9 -9
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"workspaces": [
"packages/*"
],
@@ -17056,7 +17056,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
@@ -17114,7 +17114,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -17197,7 +17197,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
@@ -17462,7 +17462,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -17477,7 +17477,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17494,7 +17494,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17511,7 +17511,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"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.30.0-nightly.20260210.a2174751d"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.33.0-nightly.20260228.1ca5c05d0"
},
"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.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+5 -2
View File
@@ -27,7 +27,8 @@ import {
type ToolCallConfirmationDetails,
type Config,
type UserTierId,
type AnsiOutput,
type ToolLiveOutput,
isSubagentProgress,
EDIT_TOOL_NAMES,
processRestorableToolCalls,
} from '@google/gemini-cli-core';
@@ -336,11 +337,13 @@ export class Task {
private _schedulerOutputUpdate(
toolCallId: string,
outputChunk: string | AnsiOutput,
outputChunk: ToolLiveOutput,
): void {
let outputAsText: string;
if (typeof outputChunk === 'string') {
outputAsText = outputChunk;
} else if (isSubagentProgress(outputChunk)) {
outputAsText = JSON.stringify(outputChunk);
} else {
outputAsText = outputChunk
.map((line) => line.map((token) => token.text).join(''))
@@ -28,6 +28,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const mockConfig = {
...params,
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: vi.fn(),
getExperiments: vi.fn().mockReturnValue({
flags: {
@@ -94,6 +95,7 @@ describe('loadConfig', () => {
const mockConfig = {
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: vi.fn(),
getExperiments: vi.fn().mockReturnValue({
flags: {
+2
View File
@@ -166,6 +166,8 @@ export async function loadConfig(
// Needed to initialize ToolRegistry, and git checkpointing if enabled
await config.initialize();
await config.waitForMcpInit();
startupProfiler.flush(config);
await refreshAuthentication(config, adcFilePath, 'Config');
+44 -5
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import express from 'express';
import express, { type Request } from 'express';
import type { AgentCard, Message } from '@a2a-js/sdk';
import {
@@ -13,8 +13,9 @@ import {
InMemoryTaskStore,
DefaultExecutionEventBus,
type AgentExecutionEvent,
UnauthenticatedUser,
} from '@a2a-js/sdk/server';
import { A2AExpressApp } from '@a2a-js/sdk/server/express'; // Import server components
import { A2AExpressApp, type UserBuilder } from '@a2a-js/sdk/server/express'; // Import server components
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../utils/logger.js';
import type { AgentSettings } from '../types.js';
@@ -55,8 +56,17 @@ const coderAgentCard: AgentCard = {
pushNotifications: false,
stateTransitionHistory: true,
},
securitySchemes: undefined,
security: undefined,
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
basicAuth: {
type: 'http',
scheme: 'basic',
},
},
security: [{ bearerAuth: [] }, { basicAuth: [] }],
defaultInputModes: ['text'],
defaultOutputModes: ['text'],
skills: [
@@ -81,6 +91,35 @@ export function updateCoderAgentCardUrl(port: number) {
coderAgentCard.url = `http://localhost:${port}/`;
}
const customUserBuilder: UserBuilder = async (req: Request) => {
const auth = req.headers['authorization'];
if (auth) {
const scheme = auth.split(' ')[0];
logger.info(
`[customUserBuilder] Received Authorization header with scheme: ${scheme}`,
);
}
if (!auth) return new UnauthenticatedUser();
// 1. Bearer Auth
if (auth.startsWith('Bearer ')) {
const token = auth.substring(7);
if (token === 'valid-token') {
return { userName: 'bearer-user', isAuthenticated: true };
}
}
// 2. Basic Auth
if (auth.startsWith('Basic ')) {
const credentials = Buffer.from(auth.substring(6), 'base64').toString();
if (credentials === 'admin:password') {
return { userName: 'basic-user', isAuthenticated: true };
}
}
return new UnauthenticatedUser();
};
async function handleExecuteCommand(
req: express.Request,
res: express.Response,
@@ -204,7 +243,7 @@ export async function createApp() {
requestStorage.run({ req }, next);
});
const appBuilder = new A2AExpressApp(requestHandler);
const appBuilder = new A2AExpressApp(requestHandler, customUserBuilder);
expressApp = appBuilder.setupRoutes(expressApp, '');
expressApp.use(express.json());
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.30.0-nightly.20260210.a2174751d",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"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.30.0-nightly.20260210.a2174751d"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.33.0-nightly.20260228.1ca5c05d0"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
-3
View File
@@ -185,9 +185,6 @@ export interface SessionRetentionSettings {
/** Minimum retention period (safety limit, defaults to "1d") */
minRetention?: string;
/** INTERNAL: Whether the user has acknowledged the session retention warning */
warningAcknowledged?: boolean;
}
export interface SettingsError {
+2 -12
View File
@@ -339,7 +339,7 @@ const SETTINGS_SCHEMA = {
label: 'Enable Session Cleanup',
category: 'General',
requiresRestart: false,
default: false,
default: true as boolean,
description: 'Enable automatic session cleanup',
showInDialog: true,
},
@@ -348,7 +348,7 @@ const SETTINGS_SCHEMA = {
label: 'Keep chat history',
category: 'General',
requiresRestart: false,
default: undefined as string | undefined,
default: '30d' as string,
description:
'Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w")',
showInDialog: true,
@@ -372,16 +372,6 @@ const SETTINGS_SCHEMA = {
description: `Minimum retention period (safety limit, defaults to "${DEFAULT_MIN_RETENTION}")`,
showInDialog: false,
},
warningAcknowledged: {
type: 'boolean',
label: 'Warning Acknowledged',
category: 'General',
requiresRestart: false,
default: false,
showInDialog: false,
description:
'INTERNAL: Whether the user has acknowledged the session retention warning',
},
},
description: 'Settings for automatic session cleanup.',
},
+1 -1
View File
@@ -243,7 +243,7 @@ export async function startInteractiveUI(
<ScrollProvider>
<OverflowProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<VimModeProvider>
<AppContainer
config={config}
startupWarnings={startupWarnings}
+6 -2
View File
@@ -528,12 +528,13 @@ export const mockSettings = new LoadedSettings(
// A minimal mock UIState to satisfy the context provider.
// Tests that need specific UIState values should provide their own.
const baseMockUiState = {
history: [],
renderMarkdown: true,
streamingState: StreamingState.Idle,
terminalWidth: 100,
terminalHeight: 40,
currentModel: 'gemini-pro',
terminalBackgroundColor: 'black',
terminalBackgroundColor: 'black' as const,
cleanUiDetailsVisible: false,
allowPlanMode: true,
activePtyId: undefined,
@@ -552,6 +553,9 @@ const baseMockUiState = {
warningText: '',
},
bannerVisible: false,
nightly: false,
updateInfo: null,
pendingHistoryItems: [],
};
export const mockAppState: AppState = {
@@ -752,7 +756,7 @@ export const renderWithProviders = (
<ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider settings={finalSettings}>
<VimModeProvider>
<ShellFocusContext.Provider value={shellFocus}>
<SessionStatsProvider>
<StreamingContext.Provider
+3 -30
View File
@@ -146,7 +146,6 @@ import { requestConsentInteractive } from '../config/extensions/consent.js';
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
import { useSessionResume } from './hooks/useSessionResume.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
import { useSettings } from './contexts/SettingsContext.js';
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
@@ -1548,28 +1547,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog);
const handleAutoEnableRetention = useCallback(() => {
const userSettings = settings.forScope(SettingScope.User).settings;
const currentRetention = userSettings.general?.sessionRetention ?? {};
settings.setValue(SettingScope.User, 'general.sessionRetention', {
...currentRetention,
enabled: true,
maxAge: '30d',
warningAcknowledged: true,
});
}, [settings]);
const {
shouldShowWarning: shouldShowRetentionWarning,
checkComplete: retentionCheckComplete,
sessionsToDeleteCount,
} = useSessionRetentionCheck(
config,
settings.merged,
handleAutoEnableRetention,
);
const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
@@ -2015,7 +1992,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
const nightly = props.version.includes('nightly');
const dialogsVisible =
(shouldShowRetentionWarning && retentionCheckComplete) ||
shouldShowIdePrompt ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
isPolicyUpdateDialogOpen ||
@@ -2202,9 +2179,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
history: historyManager.history,
historyManager,
isThemeDialogOpen,
shouldShowRetentionWarning:
shouldShowRetentionWarning && retentionCheckComplete,
sessionsToDeleteCount: sessionsToDeleteCount ?? 0,
themeError,
isAuthenticating,
isConfigInitialized,
@@ -2334,9 +2309,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
}),
[
isThemeDialogOpen,
shouldShowRetentionWarning,
retentionCheckComplete,
sessionsToDeleteCount,
themeError,
isAuthenticating,
isConfigInitialized,
@@ -2,20 +2,20 @@
exports[`App > Snapshots > renders default layout correctly 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
@@ -47,34 +47,31 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = `
"Notifications
Footer
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
Composer
"
`;
exports[`App > Snapshots > renders with dialogs visible 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
@@ -110,20 +107,17 @@ DialogManager
exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
HistoryItemDisplay
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
@@ -146,6 +140,9 @@ HistoryItemDisplay
Notifications
Composer
"
@@ -21,6 +21,10 @@ import {
ConfigExtensionDialog,
type ConfigExtensionDialogProps,
} from '../components/ConfigExtensionDialog.js';
import {
ExtensionRegistryView,
type ExtensionRegistryViewProps,
} from '../components/views/ExtensionRegistryView.js';
import { type CommandContext, type SlashCommand } from './types.js';
import {
@@ -39,6 +43,8 @@ import {
} from '../../config/extension-manager.js';
import { SettingScope } from '../../config/settings.js';
import { stat } from 'node:fs/promises';
import { type RegistryExtension } from '../../config/extensionRegistryClient.js';
import { waitFor } from '../../test-utils/async.js';
vi.mock('../../config/extension-manager.js', async (importOriginal) => {
const actual =
@@ -167,6 +173,7 @@ describe('extensionsCommand', () => {
},
ui: {
dispatchExtensionStateUpdate: mockDispatchExtensionState,
removeComponent: vi.fn(),
},
});
});
@@ -429,6 +436,61 @@ describe('extensionsCommand', () => {
throw new Error('Explore action not found');
}
it('should return ExtensionRegistryView custom dialog when experimental.extensionRegistry is true', async () => {
mockContext.services.settings.merged.experimental.extensionRegistry = true;
const result = await exploreAction(mockContext, '');
expect(result).toBeDefined();
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const component =
result.component as ReactElement<ExtensionRegistryViewProps>;
expect(component.type).toBe(ExtensionRegistryView);
expect(component.props.extensionManager).toBe(mockExtensionLoader);
});
it('should handle onSelect and onClose in ExtensionRegistryView', async () => {
mockContext.services.settings.merged.experimental.extensionRegistry = true;
const result = await exploreAction(mockContext, '');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const component =
result.component as ReactElement<ExtensionRegistryViewProps>;
const extension = {
extensionName: 'test-ext',
url: 'https://github.com/test/ext.git',
} as RegistryExtension;
vi.mocked(inferInstallMetadata).mockResolvedValue({
source: extension.url,
type: 'git',
});
mockInstallExtension.mockResolvedValue({ name: extension.url });
// Call onSelect
component.props.onSelect?.(extension);
await waitFor(() => {
expect(inferInstallMetadata).toHaveBeenCalledWith(extension.url);
expect(mockInstallExtension).toHaveBeenCalledWith({
source: extension.url,
type: 'git',
});
});
expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(1);
// Call onClose
component.props.onClose?.();
expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(2);
});
it("should add an info message and call 'open' in a non-sandbox environment", async () => {
// Ensure no special environment variables that would affect behavior
vi.stubEnv('NODE_ENV', '');
@@ -280,7 +280,9 @@ async function exploreAction(
type: 'custom_dialog' as const,
component: React.createElement(ExtensionRegistryView, {
onSelect: (extension) => {
debugLogger.debug(`Selected extension: ${extension.extensionName}`);
debugLogger.log(`Selected extension: ${extension.extensionName}`);
void installAction(context, extension.url);
context.ui.removeComponent();
},
onClose: () => context.ui.removeComponent(),
extensionManager,
@@ -7,7 +7,6 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { hooksCommand } from './hooksCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
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';
@@ -127,13 +126,10 @@ describe('hooksCommand', () => {
createMockHook('test-hook', HookEventName.BeforeTool, true),
]);
await hooksCommand.action(mockContext, '');
const result = await hooksCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
}),
);
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
});
});
@@ -161,7 +157,7 @@ describe('hooksCommand', () => {
});
});
it('should display panel even when hook system is not enabled', async () => {
it('should return custom_dialog even when hook system is not enabled', async () => {
mockConfig.getHookSystem.mockReturnValue(null);
const panelCmd = hooksCommand.subCommands!.find(
@@ -171,17 +167,13 @@ describe('hooksCommand', () => {
throw new Error('panel command must have an action');
}
await panelCmd.action(mockContext, '');
const result = await panelCmd.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
hooks: [],
}),
);
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
});
it('should display panel when no hooks are configured', async () => {
it('should return custom_dialog when no hooks are configured', async () => {
mockHookSystem.getAllHooks.mockReturnValue([]);
(mockContext.services.settings.merged as Record<string, unknown>)[
'hooksConfig'
@@ -194,17 +186,13 @@ describe('hooksCommand', () => {
throw new Error('panel command must have an action');
}
await panelCmd.action(mockContext, '');
const result = await panelCmd.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
hooks: [],
}),
);
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
});
it('should display hooks list when hooks are configured', async () => {
it('should return custom_dialog when hooks are configured', async () => {
const mockHooks: HookRegistryEntry[] = [
createMockHook('echo-test', HookEventName.BeforeTool, true),
createMockHook('notify', HookEventName.AfterAgent, false),
@@ -222,14 +210,10 @@ describe('hooksCommand', () => {
throw new Error('panel command must have an action');
}
await panelCmd.action(mockContext, '');
const result = await panelCmd.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
hooks: mockHooks,
}),
);
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
});
});
+18 -11
View File
@@ -4,9 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { SlashCommand, CommandContext } from './types.js';
import { createElement } from 'react';
import type {
SlashCommand,
CommandContext,
OpenCustomDialogActionReturn,
} from './types.js';
import { CommandKind } from './types.js';
import { MessageType, type HistoryItemHooksList } from '../types.js';
import type {
HookRegistryEntry,
MessageActionReturn,
@@ -15,13 +19,14 @@ import { getErrorMessage } from '@google/gemini-cli-core';
import { SettingScope, isLoadableSettingScope } from '../../config/settings.js';
import { enableHook, disableHook } from '../../utils/hookSettings.js';
import { renderHookActionFeedback } from '../../utils/hookUtils.js';
import { HooksDialog } from '../components/HooksDialog.js';
/**
* Display a formatted list of hooks with their status
* Display a formatted list of hooks with their status in a dialog
*/
async function panelAction(
function panelAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
): MessageActionReturn | OpenCustomDialogActionReturn {
const { config } = context.services;
if (!config) {
return {
@@ -34,12 +39,13 @@ async function panelAction(
const hookSystem = config.getHookSystem();
const allHooks = hookSystem?.getAllHooks() || [];
const hooksListItem: HistoryItemHooksList = {
type: MessageType.HOOKS_LIST,
hooks: allHooks,
return {
type: 'custom_dialog',
component: createElement(HooksDialog, {
hooks: allHooks,
onClose: () => context.ui.removeComponent(),
}),
};
context.ui.addItem(hooksListItem);
}
/**
@@ -343,6 +349,7 @@ const panelCommand: SlashCommand = {
altNames: ['list', 'show'],
description: 'Display all registered hooks with their status',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: panelAction,
};
@@ -393,5 +400,5 @@ export const hooksCommand: SlashCommand = {
enableAllCommand,
disableAllCommand,
],
action: async (context: CommandContext) => panelCommand.action!(context, ''),
action: (context: CommandContext) => panelCommand.action!(context, ''),
};
@@ -19,6 +19,7 @@ import {
BaseSettingsDialog,
type SettingsDialogItem,
} from './shared/BaseSettingsDialog.js';
import { getNestedValue, isRecord } from '../../utils/settingsUtils.js';
/**
* Configuration field definition for agent settings
@@ -111,32 +112,12 @@ interface AgentConfigDialogProps {
onSave?: () => void;
}
/**
* Get a nested value from an object using a path array
*/
function getNestedValue(
obj: Record<string, unknown> | undefined,
path: string[],
): unknown {
if (!obj) return undefined;
let current: unknown = obj;
for (const key of path) {
if (current === null || current === undefined) return undefined;
if (typeof current !== 'object') return undefined;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current = (current as Record<string, unknown>)[key];
}
return current;
}
/**
* Set a nested value in an object using a path array, creating intermediate objects as needed
*/
function setNestedValue(
obj: Record<string, unknown>,
path: string[],
value: unknown,
): Record<string, unknown> {
function setNestedValue(obj: unknown, path: string[], value: unknown): unknown {
if (!isRecord(obj)) return obj;
const result = { ...obj };
let current = result;
@@ -144,12 +125,17 @@ function setNestedValue(
const key = path[i];
if (current[key] === undefined || current[key] === null) {
current[key] = {};
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current[key] = { ...(current[key] as Record<string, unknown>) };
} else if (isRecord(current[key])) {
current[key] = { ...current[key] };
}
const next = current[key];
if (isRecord(next)) {
current = next;
} else {
// Cannot traverse further through non-objects
return result;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current = current[key] as Record<string, unknown>;
}
const finalKey = path[path.length - 1];
@@ -267,11 +253,7 @@ export function AgentConfigDialog({
const items: SettingsDialogItem[] = useMemo(
() =>
AGENT_CONFIG_FIELDS.map((field) => {
const currentValue = getNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
);
const currentValue = getNestedValue(pendingOverride, field.path);
const defaultValue = getFieldDefaultFromDefinition(field, definition);
const effectiveValue =
currentValue !== undefined ? currentValue : defaultValue;
@@ -324,23 +306,18 @@ export function AgentConfigDialog({
const field = AGENT_CONFIG_FIELDS.find((f) => f.key === key);
if (!field || field.type !== 'boolean') return;
const currentValue = getNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
);
const currentValue = getNestedValue(pendingOverride, field.path);
const defaultValue = getFieldDefaultFromDefinition(field, definition);
const effectiveValue =
currentValue !== undefined ? currentValue : defaultValue;
const newValue = !effectiveValue;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newOverride = setNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
pendingOverride,
field.path,
newValue,
) as AgentOverride;
setPendingOverride(newOverride);
setModifiedFields((prev) => new Set(prev).add(key));
@@ -375,9 +352,9 @@ export function AgentConfigDialog({
}
// Update pending override locally
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newOverride = setNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
pendingOverride,
field.path,
parsed,
) as AgentOverride;
@@ -398,9 +375,9 @@ export function AgentConfigDialog({
if (!field) return;
// Remove the override (set to undefined)
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newOverride = setNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
pendingOverride,
field.path,
undefined,
) as AgentOverride;
@@ -213,6 +213,12 @@ describe('<AppHeader />', () => {
it('should NOT render Tips when tipsShown is 10 or more', async () => {
const mockConfig = makeFakeConfig();
const uiState = {
bannerData: {
defaultText: '',
warningText: '',
},
};
persistentStateMock.setData({ tipsShown: 10 });
@@ -220,6 +226,7 @@ describe('<AppHeader />', () => {
<AppHeader version="1.0.0" />,
{
config: mockConfig,
uiState,
},
);
await waitUntilReady();
+74 -19
View File
@@ -1,58 +1,113 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box } from 'ink';
import { Header } from './Header.js';
import { Tips } from './Tips.js';
import { Box, Text } from 'ink';
import { UserIdentity } from './UserIdentity.js';
import { Tips } from './Tips.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { Banner } from './Banner.js';
import { useBanner } from '../hooks/useBanner.js';
import { useTips } from '../hooks/useTips.js';
import { theme } from '../semantic-colors.js';
import { ThemedGradient } from './ThemedGradient.js';
import { CliSpinner } from './CliSpinner.js';
interface AppHeaderProps {
version: string;
showDetails?: boolean;
}
const ICON = `▝▜▄
`;
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState();
const { terminalWidth, bannerData, bannerVisible, updateInfo } = useUIState();
const { bannerText } = useBanner(bannerData);
const { showTips } = useTips();
const showHeader = !(
settings.merged.ui.hideBanner || config.getScreenReader()
);
if (!showDetails) {
return (
<Box flexDirection="column">
<Header version={version} nightly={false} />
{showHeader && (
<Box
flexDirection="row"
marginTop={1}
marginBottom={1}
paddingLeft={2}
>
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
<Box marginLeft={2} flexDirection="column">
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
</Box>
</Box>
</Box>
)}
</Box>
);
}
return (
<Box flexDirection="column">
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
<>
<Header version={version} nightly={nightly} />
{bannerVisible && bannerText && (
<Banner
width={terminalWidth}
bannerText={bannerText}
isWarning={bannerData.warningText !== ''}
/>
)}
</>
{showHeader && (
<Box flexDirection="row" marginTop={1} marginBottom={1} paddingLeft={2}>
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
<Box marginLeft={2} flexDirection="column">
{/* Line 1: Gemini CLI vVersion [Updating] */}
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
{updateInfo && (
<Box marginLeft={2}>
<Text color={theme.text.secondary}>
<CliSpinner /> Updating
</Text>
</Box>
)}
</Box>
{/* Line 2: Blank */}
<Box height={1} />
{/* Lines 3 & 4: User Identity info (Email /auth and Plan /upgrade) */}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
)}
</Box>
</Box>
)}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
{bannerVisible && bannerText && (
<Banner
width={terminalWidth}
bannerText={bannerText}
isWarning={bannerData.warningText !== ''}
/>
)}
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
showTips && <Tips config={config} />}
</Box>
@@ -37,9 +37,6 @@ import { AdminSettingsChangedDialog } from './AdminSettingsChangedDialog.js';
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
import { NewAgentsNotification } from './NewAgentsNotification.js';
import { AgentConfigDialog } from './AgentConfigDialog.js';
import { SessionRetentionWarningDialog } from './SessionRetentionWarningDialog.js';
import { useCallback } from 'react';
import { SettingScope } from '../../config/settings.js';
import { PolicyUpdateDialog } from './PolicyUpdateDialog.js';
interface DialogManagerProps {
@@ -62,56 +59,8 @@ export const DialogManager = ({
terminalHeight,
staticExtraHeight,
terminalWidth: uiTerminalWidth,
shouldShowRetentionWarning,
sessionsToDeleteCount,
} = uiState;
const handleKeep120Days = useCallback(() => {
settings.setValue(
SettingScope.User,
'general.sessionRetention.warningAcknowledged',
true,
);
settings.setValue(
SettingScope.User,
'general.sessionRetention.enabled',
true,
);
settings.setValue(
SettingScope.User,
'general.sessionRetention.maxAge',
'120d',
);
}, [settings]);
const handleKeep30Days = useCallback(() => {
settings.setValue(
SettingScope.User,
'general.sessionRetention.warningAcknowledged',
true,
);
settings.setValue(
SettingScope.User,
'general.sessionRetention.enabled',
true,
);
settings.setValue(
SettingScope.User,
'general.sessionRetention.maxAge',
'30d',
);
}, [settings]);
if (shouldShowRetentionWarning && sessionsToDeleteCount !== undefined) {
return (
<SessionRetentionWarningDialog
onKeep120Days={handleKeep120Days}
onKeep30Days={handleKeep30Days}
sessionsToDeleteCount={sessionsToDeleteCount ?? 0}
/>
);
}
if (uiState.adminSettingsChanged) {
return <AdminSettingsChangedDialog />;
}
@@ -281,14 +230,12 @@ export const DialogManager = ({
return (
<Box flexDirection="column">
<SettingsDialog
settings={settings}
onSelect={() => uiActions.closeSettingsDialog()}
onRestartRequest={async () => {
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
}}
availableTerminalHeight={terminalHeight - staticExtraHeight}
config={config}
/>
</Box>
);
@@ -11,7 +11,6 @@ import { waitFor } from '../../test-utils/async.js';
import { ExitPlanModeDialog } from './ExitPlanModeDialog.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import { openFileInEditor } from '../utils/editorUtils.js';
import {
ApprovalMode,
validatePlanContent,
@@ -41,10 +40,6 @@ vi.mock('node:fs', async (importOriginal) => {
...actual,
existsSync: vi.fn(),
realpathSync: vi.fn((p) => p),
promises: {
...actual.promises,
readFile: vi.fn(),
},
};
});
@@ -546,7 +541,7 @@ Implement a comprehensive authentication system with multiple providers.
expect(onFeedback).not.toHaveBeenCalled();
});
it('opens plan in external editor when Ctrl+X is pressed', async () => {
it('automatically submits feedback when Ctrl+X is used to edit the plan', async () => {
const { stdin, lastFrame } = renderDialog({ useAlternateBuffer });
await act(async () => {
@@ -557,27 +552,16 @@ Implement a comprehensive authentication system with multiple providers.
expect(lastFrame()).toContain('Add user authentication');
});
// Reset the mock to track the second call during refresh
vi.mocked(processSingleFileContent).mockClear();
// Press Ctrl+X
await act(async () => {
writeKey(stdin, '\x18'); // Ctrl+X
});
await waitFor(() => {
expect(openFileInEditor).toHaveBeenCalledWith(
mockPlanFullPath,
expect.anything(),
expect.anything(),
undefined,
expect(onFeedback).toHaveBeenCalledWith(
'I have edited the plan or annotated it with feedback. Review the edited plan, update if necessary, and present it again for approval.',
);
});
// Verify that content is refreshed (processSingleFileContent called again)
await waitFor(() => {
expect(processSingleFileContent).toHaveBeenCalled();
});
});
},
);
@@ -156,11 +156,15 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
const handleOpenEditor = useCallback(async () => {
try {
await openFileInEditor(planPath, stdin, setRawMode, getPreferredEditor());
onFeedback(
'I have edited the plan or annotated it with feedback. Review the edited plan, update if necessary, and present it again for approval.',
);
refresh();
} catch (err) {
debugLogger.error('Failed to open plan in editor:', err);
}
}, [planPath, stdin, setRawMode, getPreferredEditor, refresh]);
}, [planPath, stdin, setRawMode, getPreferredEditor, refresh, onFeedback]);
useKeypress(
(key) => {
@@ -32,7 +32,6 @@ import { SkillsList } from './views/SkillsList.js';
import { AgentsStatus } from './views/AgentsStatus.js';
import { McpStatus } from './views/McpStatus.js';
import { ChatList } from './views/ChatList.js';
import { HooksList } from './views/HooksList.js';
import { ModelMessage } from './messages/ModelMessage.js';
import { ThinkingMessage } from './messages/ThinkingMessage.js';
import { HintMessage } from './messages/HintMessage.js';
@@ -217,9 +216,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
{itemForDisplay.type === 'chat_list' && (
<ChatList chats={itemForDisplay.chats} />
)}
{itemForDisplay.type === 'hooks_list' && (
<HooksList hooks={itemForDisplay.hooks} />
)}
</Box>
);
};
@@ -0,0 +1,248 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../test-utils/render.js';
import { act } from 'react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { HooksDialog, type HookEntry } from './HooksDialog.js';
describe('HooksDialog', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockHook = (
name: string,
eventName: string,
enabled: boolean,
options?: Partial<HookEntry>,
): HookEntry => ({
config: {
name,
command: `run-${name}`,
type: 'command',
description: `Test hook: ${name}`,
...options?.config,
},
source: options?.source ?? '/mock/path/GEMINI.md',
eventName,
enabled,
...options,
});
describe('snapshots', () => {
it('renders empty hooks dialog', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={[]} onClose={vi.fn()} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders single hook with security warning, source, and tips', async () => {
const hooks = [createMockHook('test-hook', 'before-tool', true)];
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders hooks grouped by event name with enabled and disabled status', async () => {
const hooks = [
createMockHook('hook1', 'before-tool', true),
createMockHook('hook2', 'before-tool', false),
createMockHook('hook3', 'after-agent', true),
];
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders hook with all metadata (matcher, sequential, timeout)', async () => {
const hooks = [
createMockHook('my-hook', 'before-tool', true, {
matcher: 'shell_exec',
sequential: true,
config: {
name: 'my-hook',
type: 'command',
description: 'A hook with all metadata fields',
timeout: 30,
},
}),
];
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders hook using command as name when name is not provided', async () => {
const hooks: HookEntry[] = [
{
config: {
command: 'echo hello',
type: 'command',
},
source: '/mock/path',
eventName: 'before-tool',
enabled: true,
},
];
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
describe('keyboard interaction', () => {
it('should call onClose when escape key is pressed', async () => {
const onClose = vi.fn();
const { waitUntilReady, stdin, unmount } = renderWithProviders(
<HooksDialog hooks={[]} onClose={onClose} />,
);
await waitUntilReady();
act(() => {
stdin.write('\u001b[27u');
});
expect(onClose).toHaveBeenCalledTimes(1);
unmount();
});
});
describe('scrolling behavior', () => {
const createManyHooks = (count: number): HookEntry[] =>
Array.from({ length: count }, (_, i) =>
createMockHook(`hook-${i + 1}`, `event-${(i % 3) + 1}`, i % 2 === 0),
);
it('should not show scroll indicators when hooks fit within maxVisibleHooks', async () => {
const hooks = [
createMockHook('hook1', 'before-tool', true),
createMockHook('hook2', 'after-tool', false),
];
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={10} />,
);
await waitUntilReady();
expect(lastFrame()).not.toContain('▲');
expect(lastFrame()).not.toContain('▼');
unmount();
});
it('should show scroll down indicator when there are more hooks than maxVisibleHooks', async () => {
const hooks = createManyHooks(15);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('▼');
unmount();
});
it('should scroll down when down arrow is pressed', async () => {
const hooks = createManyHooks(15);
const { lastFrame, waitUntilReady, stdin, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
);
await waitUntilReady();
// Initially should not show up indicator
expect(lastFrame()).not.toContain('▲');
act(() => {
stdin.write('\u001b[B');
});
await waitUntilReady();
// Should now show up indicator after scrolling down
expect(lastFrame()).toContain('▲');
unmount();
});
it('should scroll up when up arrow is pressed after scrolling down', async () => {
const hooks = createManyHooks(15);
const { lastFrame, waitUntilReady, stdin, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
);
await waitUntilReady();
// Scroll down twice
act(() => {
stdin.write('\u001b[B');
stdin.write('\u001b[B');
});
await waitUntilReady();
expect(lastFrame()).toContain('▲');
// Scroll up once
act(() => {
stdin.write('\u001b[A');
});
await waitUntilReady();
// Should still show up indicator (scrolled down once)
expect(lastFrame()).toContain('▲');
unmount();
});
it('should not scroll beyond the end', async () => {
const hooks = createManyHooks(10);
const { lastFrame, waitUntilReady, stdin, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
);
await waitUntilReady();
// Scroll down many times past the end
act(() => {
for (let i = 0; i < 20; i++) {
stdin.write('\u001b[B');
}
});
await waitUntilReady();
const frame = lastFrame();
expect(frame).toContain('▲');
// At the end, down indicator should be hidden
expect(frame).not.toContain('▼');
unmount();
});
it('should not scroll above the beginning', async () => {
const hooks = createManyHooks(10);
const { lastFrame, waitUntilReady, stdin, unmount } = renderWithProviders(
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
);
await waitUntilReady();
// Try to scroll up when already at top
act(() => {
stdin.write('\u001b[A');
});
await waitUntilReady();
expect(lastFrame()).not.toContain('▲');
expect(lastFrame()).toContain('▼');
unmount();
});
});
});
@@ -0,0 +1,247 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useState, useMemo } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
/**
* Hook entry type matching HookRegistryEntry from core
*/
export interface HookEntry {
config: {
command?: string;
type: string;
name?: string;
description?: string;
timeout?: number;
};
source: string;
eventName: string;
matcher?: string;
sequential?: boolean;
enabled: boolean;
}
interface HooksDialogProps {
hooks: readonly HookEntry[];
onClose: () => void;
/** Maximum number of hooks to display at once before scrolling. Default: 8 */
maxVisibleHooks?: number;
}
/** Maximum hooks to show at once before scrolling is needed */
const DEFAULT_MAX_VISIBLE_HOOKS = 8;
/**
* Dialog component for displaying hooks in a styled box.
* Replaces inline chat history display with a modal-style dialog.
* Supports scrolling with up/down arrow keys when there are many hooks.
*/
export const HooksDialog: React.FC<HooksDialogProps> = ({
hooks,
onClose,
maxVisibleHooks = DEFAULT_MAX_VISIBLE_HOOKS,
}) => {
const [scrollOffset, setScrollOffset] = useState(0);
// Flatten hooks with their event names for easier scrolling
const flattenedHooks = useMemo(() => {
const result: Array<{
type: 'header' | 'hook';
eventName: string;
hook?: HookEntry;
}> = [];
// Group hooks by event name
const hooksByEvent = hooks.reduce(
(acc, hook) => {
if (!acc[hook.eventName]) {
acc[hook.eventName] = [];
}
acc[hook.eventName].push(hook);
return acc;
},
{} as Record<string, HookEntry[]>,
);
// Flatten into displayable items
Object.entries(hooksByEvent).forEach(([eventName, eventHooks]) => {
result.push({ type: 'header', eventName });
eventHooks.forEach((hook) => {
result.push({ type: 'hook', eventName, hook });
});
});
return result;
}, [hooks]);
const totalItems = flattenedHooks.length;
const needsScrolling = totalItems > maxVisibleHooks;
const maxScrollOffset = Math.max(0, totalItems - maxVisibleHooks);
// Handle keyboard navigation
useKeypress(
(key) => {
if (keyMatchers[Command.ESCAPE](key)) {
onClose();
return true;
}
// Scroll navigation
if (needsScrolling) {
if (keyMatchers[Command.DIALOG_NAVIGATION_UP](key)) {
setScrollOffset((prev) => Math.max(0, prev - 1));
return true;
}
if (keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key)) {
setScrollOffset((prev) => Math.min(maxScrollOffset, prev + 1));
return true;
}
}
return false;
},
{ isActive: true },
);
// Get visible items based on scroll offset
const visibleItems = needsScrolling
? flattenedHooks.slice(scrollOffset, scrollOffset + maxVisibleHooks)
: flattenedHooks;
const showScrollUp = needsScrolling && scrollOffset > 0;
const showScrollDown = needsScrolling && scrollOffset < maxScrollOffset;
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
padding={1}
marginY={1}
width="100%"
>
{hooks.length === 0 ? (
<>
<Text color={theme.text.primary}>No hooks configured.</Text>
</>
) : (
<>
{/* Security Warning */}
<Box marginBottom={1} flexDirection="column">
<Text color={theme.status.warning} bold underline>
Security Warning:
</Text>
<Text color={theme.status.warning} wrap="wrap">
Hooks can execute arbitrary commands on your system. Only use
hooks from sources you trust. Review hook scripts carefully.
</Text>
</Box>
{/* Learn more link */}
<Box marginBottom={1}>
<Text wrap="wrap">
Learn more:{' '}
<Text color={theme.text.link}>
https://geminicli.com/docs/hooks
</Text>
</Text>
</Box>
{/* Configured Hooks heading */}
<Box marginBottom={1}>
<Text bold color={theme.text.accent}>
Configured Hooks
</Text>
</Box>
{/* Scroll up indicator */}
{showScrollUp && (
<Box paddingLeft={2} minWidth={0}>
<Text color={theme.text.secondary}></Text>
</Box>
)}
{/* Visible hooks */}
<Box flexDirection="column" paddingLeft={2}>
{visibleItems.map((item, index) => {
if (item.type === 'header') {
return (
<Box
key={`header-${item.eventName}-${index}`}
marginBottom={1}
>
<Text bold color={theme.text.link}>
{item.eventName}
</Text>
</Box>
);
}
const hook = item.hook!;
const hookName =
hook.config.name || hook.config.command || 'unknown';
const hookKey = `${item.eventName}:${hook.source}:${hook.config.name ?? ''}:${hook.config.command ?? ''}`;
const statusColor = hook.enabled
? theme.status.success
: theme.text.secondary;
const statusText = hook.enabled ? 'enabled' : 'disabled';
return (
<Box key={hookKey} flexDirection="column" marginBottom={1}>
<Box flexDirection="row">
<Text color={theme.text.accent} bold>
{hookName}
</Text>
<Text color={statusColor}>{` [${statusText}]`}</Text>
</Box>
<Box paddingLeft={2} flexDirection="column">
{hook.config.description && (
<Text color={theme.text.primary} italic wrap="wrap">
{hook.config.description}
</Text>
)}
<Text color={theme.text.secondary} wrap="wrap">
Source: {hook.source}
{hook.config.name &&
hook.config.command &&
` | Command: ${hook.config.command}`}
{hook.matcher && ` | Matcher: ${hook.matcher}`}
{hook.sequential && ` | Sequential`}
{hook.config.timeout &&
` | Timeout: ${hook.config.timeout}s`}
</Text>
</Box>
</Box>
);
})}
</Box>
{/* Scroll down indicator */}
{showScrollDown && (
<Box paddingLeft={2} minWidth={0}>
<Text color={theme.text.secondary}></Text>
</Box>
)}
{/* Tips */}
<Box marginTop={1}>
<Text color={theme.text.secondary} wrap="wrap">
Tip: Use <Text bold>/hooks enable {'<hook-name>'}</Text> or{' '}
<Text bold>/hooks disable {'<hook-name>'}</Text> to toggle
individual hooks. Use <Text bold>/hooks enable-all</Text> or{' '}
<Text bold>/hooks disable-all</Text> to toggle all hooks at once.
</Text>
</Box>
</>
)}
</Box>
);
};
@@ -34,6 +34,7 @@ export const MainContent = () => {
const confirmingTool = useConfirmingTool();
const showConfirmationQueue = confirmingTool !== null;
const confirmingToolCallId = confirmingTool?.tool.callId;
const scrollableListRef = useRef<VirtualizedListRef<unknown>>(null);
@@ -41,7 +42,7 @@ export const MainContent = () => {
if (showConfirmationQueue) {
scrollableListRef.current?.scrollToEnd();
}
}, [showConfirmationQueue, confirmingTool]);
}, [showConfirmationQueue, confirmingToolCallId]);
const {
pendingHistoryItems,
@@ -1,119 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* @license
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { SessionRetentionWarningDialog } from './SessionRetentionWarningDialog.js';
import { waitFor } from '../../test-utils/async.js';
import { act } from 'react';
// Helper to write to stdin
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
act(() => {
stdin.write(key);
});
};
describe('SessionRetentionWarningDialog', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('renders correctly with warning message and session count', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<SessionRetentionWarningDialog
onKeep120Days={vi.fn()}
onKeep30Days={vi.fn()}
sessionsToDeleteCount={42}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Keep chat history');
expect(lastFrame()).toContain(
'introducing a limit on how long chat sessions are stored',
);
expect(lastFrame()).toContain('Keep for 30 days (Recommended)');
expect(lastFrame()).toContain('42 sessions will be deleted');
expect(lastFrame()).toContain('Keep for 120 days');
expect(lastFrame()).toContain('No sessions will be deleted at this time');
});
it('handles pluralization correctly for 1 session', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<SessionRetentionWarningDialog
onKeep120Days={vi.fn()}
onKeep30Days={vi.fn()}
sessionsToDeleteCount={1}
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('1 session will be deleted');
});
it('defaults to "Keep for 120 days" when there are sessions to delete', async () => {
const onKeep120Days = vi.fn();
const onKeep30Days = vi.fn();
const { stdin, waitUntilReady } = renderWithProviders(
<SessionRetentionWarningDialog
onKeep120Days={onKeep120Days}
onKeep30Days={onKeep30Days}
sessionsToDeleteCount={10}
/>,
);
await waitUntilReady();
// Initial selection should be "Keep for 120 days" (index 1) because count > 0
// Pressing Enter immediately should select it.
writeKey(stdin, '\r');
await waitFor(() => {
expect(onKeep120Days).toHaveBeenCalled();
expect(onKeep30Days).not.toHaveBeenCalled();
});
});
it('calls onKeep30Days when "Keep for 30 days" is explicitly selected (from 120 days default)', async () => {
const onKeep120Days = vi.fn();
const onKeep30Days = vi.fn();
const { stdin, waitUntilReady } = renderWithProviders(
<SessionRetentionWarningDialog
onKeep120Days={onKeep120Days}
onKeep30Days={onKeep30Days}
sessionsToDeleteCount={10}
/>,
);
await waitUntilReady();
// Default is index 1 (120 days). Move UP to index 0 (30 days).
writeKey(stdin, '\x1b[A'); // Up arrow
writeKey(stdin, '\r');
await waitFor(() => {
expect(onKeep30Days).toHaveBeenCalled();
expect(onKeep120Days).not.toHaveBeenCalled();
});
});
it('should match snapshot', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<SessionRetentionWarningDialog
onKeep120Days={vi.fn()}
onKeep30Days={vi.fn()}
sessionsToDeleteCount={123}
/>,
);
await waitUntilReady();
// Initial render
expect(lastFrame()).toMatchSnapshot();
});
});
@@ -1,78 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import {
RadioButtonSelect,
type RadioSelectItem,
} from './shared/RadioButtonSelect.js';
interface SessionRetentionWarningDialogProps {
onKeep120Days: () => void;
onKeep30Days: () => void;
sessionsToDeleteCount: number;
}
export const SessionRetentionWarningDialog = ({
onKeep120Days,
onKeep30Days,
sessionsToDeleteCount,
}: SessionRetentionWarningDialogProps) => {
const options: Array<RadioSelectItem<() => void>> = [
{
label: 'Keep for 30 days (Recommended)',
value: onKeep30Days,
key: '30days',
sublabel: `${sessionsToDeleteCount} session${
sessionsToDeleteCount === 1 ? '' : 's'
} will be deleted`,
},
{
label: 'Keep for 120 days',
value: onKeep120Days,
key: '120days',
sublabel: 'No sessions will be deleted at this time',
},
];
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
width="100%"
padding={1}
>
<Box marginBottom={1} justifyContent="center" width="100%">
<Text bold>Keep chat history</Text>
</Box>
<Box flexDirection="column" gap={1} marginBottom={1}>
<Text>
To keep your workspace clean, we are introducing a limit on how long
chat sessions are stored. Please choose a retention period for your
existing chats:
</Text>
</Box>
<Box marginTop={1}>
<RadioButtonSelect
items={options}
onSelect={(action) => action()}
initialIndex={1}
/>
</Box>
<Box marginTop={1}>
<Text color={theme.text.secondary}>
Set a custom limit <Text color={theme.text.primary}>/settings</Text>{' '}
and change &quot;Keep chat history&quot;.
</Text>
</Box>
</Box>
);
};
@@ -14,7 +14,6 @@
* - Focus section switching between settings and scope selector
* - Scope selection and settings persistence across scopes
* - Restart-required vs immediate settings behavior
* - VimModeContext integration
* - Complex user interaction workflows
* - Error handling and edge cases
* - Display values for inherited and overridden settings
@@ -25,12 +24,12 @@ import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SettingsDialog } from './SettingsDialog.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { VimModeProvider } from '../contexts/VimModeContext.js';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
import { saveModifiedSettings, TEST_ONLY } from '../../utils/settingsUtils.js';
import { TEST_ONLY } from '../../utils/settingsUtils.js';
import { SettingsContext } from '../contexts/SettingsContext.js';
import {
getSettingsSchema,
type SettingDefinition,
@@ -38,10 +37,6 @@ import {
} from '../../config/settingsSchema.js';
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
// Mock the VimModeContext
const mockToggleVimEnabled = vi.fn().mockResolvedValue(undefined);
const mockSetVimMode = vi.fn();
vi.mock('../contexts/UIStateContext.js', () => ({
useUIState: () => ({
terminalWidth: 100, // Fixed width for consistent snapshots
@@ -68,27 +63,6 @@ vi.mock('../../config/settingsSchema.js', async (importOriginal) => {
};
});
vi.mock('../contexts/VimModeContext.js', async () => {
const actual = await vi.importActual('../contexts/VimModeContext.js');
return {
...actual,
useVimMode: () => ({
vimEnabled: false,
vimMode: 'INSERT' as const,
toggleVimEnabled: mockToggleVimEnabled,
setVimMode: mockSetVimMode,
}),
};
});
vi.mock('../../utils/settingsUtils.js', async () => {
const actual = await vi.importActual('../../utils/settingsUtils.js');
return {
...actual,
saveModifiedSettings: vi.fn(),
};
});
// Shared test schemas
enum StringEnum {
FOO = 'foo',
@@ -131,6 +105,62 @@ const ENUM_FAKE_SCHEMA: SettingsSchemaType = {
},
} as unknown as SettingsSchemaType;
const ARRAY_FAKE_SCHEMA: SettingsSchemaType = {
context: {
type: 'object',
label: 'Context',
category: 'Context',
requiresRestart: false,
default: {},
description: 'Context settings.',
showInDialog: false,
properties: {
fileFiltering: {
type: 'object',
label: 'File Filtering',
category: 'Context',
requiresRestart: false,
default: {},
description: 'File filtering settings.',
showInDialog: false,
properties: {
customIgnoreFilePaths: {
type: 'array',
label: 'Custom Ignore File Paths',
category: 'Context',
requiresRestart: false,
default: [] as string[],
description: 'Additional ignore file paths.',
showInDialog: true,
items: { type: 'string' },
},
},
},
},
},
security: {
type: 'object',
label: 'Security',
category: 'Security',
requiresRestart: false,
default: {},
description: 'Security settings.',
showInDialog: false,
properties: {
allowedExtensions: {
type: 'array',
label: 'Extension Source Regex Allowlist',
category: 'Security',
requiresRestart: false,
default: [] as string[],
description: 'Allowed extension source regex patterns.',
showInDialog: true,
items: { type: 'string' },
},
},
},
} as unknown as SettingsSchemaType;
const TOOLS_SHELL_FAKE_SCHEMA: SettingsSchemaType = {
tools: {
type: 'object',
@@ -185,7 +215,7 @@ const TOOLS_SHELL_FAKE_SCHEMA: SettingsSchemaType = {
// Helper function to render SettingsDialog with standard wrapper
const renderDialog = (
settings: LoadedSettings,
settings: ReturnType<typeof createMockSettings>,
onSelect: ReturnType<typeof vi.fn>,
options?: {
onRestartRequest?: ReturnType<typeof vi.fn>;
@@ -193,14 +223,15 @@ const renderDialog = (
},
) =>
render(
<KeypressProvider>
<SettingsDialog
settings={settings}
onSelect={onSelect}
onRestartRequest={options?.onRestartRequest}
availableTerminalHeight={options?.availableTerminalHeight}
/>
</KeypressProvider>,
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog
onSelect={onSelect}
onRestartRequest={options?.onRestartRequest}
availableTerminalHeight={options?.availableTerminalHeight}
/>
</KeypressProvider>
</SettingsContext.Provider>,
);
describe('SettingsDialog', () => {
@@ -210,7 +241,6 @@ describe('SettingsDialog', () => {
terminalCapabilityManager,
'isKittyProtocolEnabled',
).mockReturnValue(true);
mockToggleVimEnabled.mockRejectedValue(undefined);
});
afterEach(() => {
@@ -394,9 +424,8 @@ describe('SettingsDialog', () => {
describe('Settings Toggling', () => {
it('should toggle setting with Enter key', async () => {
vi.mocked(saveModifiedSettings).mockClear();
const settings = createMockSettings();
const setValueSpy = vi.spyOn(settings, 'setValue');
const onSelect = vi.fn();
const { stdin, unmount, lastFrame, waitUntilReady } = renderDialog(
@@ -414,29 +443,16 @@ describe('SettingsDialog', () => {
await act(async () => {
stdin.write(TerminalKeys.ENTER as string);
});
await waitUntilReady();
// Wait for the setting change to be processed
// Wait for setValue to be called
await waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
expect(setValueSpy).toHaveBeenCalled();
});
// Wait for the mock to be called
await waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
});
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
new Set<string>(['general.vimMode']),
expect.objectContaining({
general: expect.objectContaining({
vimMode: true,
}),
}),
expect.any(LoadedSettings),
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'general.vimMode',
true,
);
unmount();
@@ -455,13 +471,13 @@ describe('SettingsDialog', () => {
expectedValue: StringEnum.FOO,
},
])('$name', async ({ initialValue, expectedValue }) => {
vi.mocked(saveModifiedSettings).mockClear();
vi.mocked(getSettingsSchema).mockReturnValue(ENUM_FAKE_SCHEMA);
const settings = createMockSettings();
if (initialValue !== undefined) {
settings.setValue(SettingScope.User, 'ui.theme', initialValue);
}
const setValueSpy = vi.spyOn(settings, 'setValue');
const onSelect = vi.fn();
@@ -482,20 +498,13 @@ describe('SettingsDialog', () => {
await waitUntilReady();
await waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui.theme',
expectedValue,
);
});
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
new Set<string>(['ui.theme']),
expect.objectContaining({
ui: expect.objectContaining({
theme: expectedValue,
}),
}),
expect.any(LoadedSettings),
SettingScope.User,
);
unmount();
});
});
@@ -692,30 +701,6 @@ describe('SettingsDialog', () => {
});
});
describe('Error Handling', () => {
it('should handle vim mode toggle errors gracefully', async () => {
mockToggleVimEnabled.mockRejectedValue(new Error('Toggle failed'));
const settings = createMockSettings();
const onSelect = vi.fn();
const { stdin, unmount, waitUntilReady } = renderDialog(
settings,
onSelect,
);
await waitUntilReady();
// Try to toggle a setting (this might trigger vim mode toggle)
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Enter
});
await waitUntilReady();
// Should not crash
unmount();
});
});
describe('Complex State Management', () => {
it('should track modified settings correctly', async () => {
const settings = createMockSettings();
@@ -767,31 +752,6 @@ describe('SettingsDialog', () => {
});
});
describe('VimMode Integration', () => {
it('should sync with VimModeContext when vim mode is toggled', async () => {
const settings = createMockSettings();
const onSelect = vi.fn();
const { stdin, unmount, waitUntilReady } = render(
<VimModeProvider settings={settings}>
<KeypressProvider>
<SettingsDialog settings={settings} onSelect={onSelect} />
</KeypressProvider>
</VimModeProvider>,
);
await waitUntilReady();
// Navigate to and toggle vim mode setting
// This would require knowing the exact position of vim mode setting
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Enter
});
await waitUntilReady();
unmount();
});
});
describe('Specific Settings Behavior', () => {
it('should show correct display values for settings with different states', async () => {
const settings = createMockSettings({
@@ -861,7 +821,7 @@ describe('SettingsDialog', () => {
// Should not show restart prompt initially
await waitFor(() => {
expect(lastFrame()).not.toContain(
'To see changes, Gemini CLI must be restarted',
'Changes that require a restart have been modified',
);
});
@@ -957,63 +917,41 @@ describe('SettingsDialog', () => {
pager: 'less',
},
},
])(
'should $name',
async ({ toggleCount, shellSettings, expectedSiblings }) => {
vi.mocked(saveModifiedSettings).mockClear();
])('should $name', async ({ toggleCount, shellSettings }) => {
vi.mocked(getSettingsSchema).mockReturnValue(TOOLS_SHELL_FAKE_SCHEMA);
vi.mocked(getSettingsSchema).mockReturnValue(TOOLS_SHELL_FAKE_SCHEMA);
const settings = createMockSettings({
tools: {
shell: shellSettings,
},
});
const setValueSpy = vi.spyOn(settings, 'setValue');
const settings = createMockSettings({
tools: {
shell: shellSettings,
},
const onSelect = vi.fn();
const { stdin, unmount } = renderDialog(settings, onSelect);
for (let i = 0; i < toggleCount; i++) {
act(() => {
stdin.write(TerminalKeys.ENTER as string);
});
}
const onSelect = vi.fn();
await waitFor(() => {
expect(setValueSpy).toHaveBeenCalled();
});
const { stdin, unmount, waitUntilReady } = renderDialog(
settings,
onSelect,
);
await waitUntilReady();
// With the store pattern, setValue is called atomically per key.
// Sibling preservation is handled by LoadedSettings internally.
const calls = setValueSpy.mock.calls;
expect(calls.length).toBeGreaterThan(0);
calls.forEach((call) => {
// Each call should target only 'tools.shell.showColor'
expect(call[1]).toBe('tools.shell.showColor');
});
for (let i = 0; i < toggleCount; i++) {
await act(async () => {
stdin.write(TerminalKeys.ENTER as string);
});
await waitUntilReady();
}
await waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
});
const calls = vi.mocked(saveModifiedSettings).mock.calls;
calls.forEach((call) => {
const [modifiedKeys, pendingSettings] = call;
if (modifiedKeys.has('tools.shell.showColor')) {
const shellSettings = pendingSettings.tools?.shell as
| Record<string, unknown>
| undefined;
Object.entries(expectedSiblings).forEach(([key, value]) => {
expect(shellSettings?.[key]).toBe(value);
expect(modifiedKeys.has(`tools.shell.${key}`)).toBe(false);
});
expect(modifiedKeys.size).toBe(1);
}
});
expect(calls.length).toBeGreaterThan(0);
unmount();
},
);
unmount();
});
});
describe('Keyboard Shortcuts Edge Cases', () => {
@@ -1319,7 +1257,7 @@ describe('SettingsDialog', () => {
await waitFor(() => {
expect(lastFrame()).toContain(
'To see changes, Gemini CLI must be restarted',
'Changes that require a restart have been modified',
);
});
@@ -1366,7 +1304,7 @@ describe('SettingsDialog', () => {
await waitFor(() => {
expect(lastFrame()).toContain(
'To see changes, Gemini CLI must be restarted',
'Changes that require a restart have been modified',
);
});
@@ -1385,9 +1323,11 @@ describe('SettingsDialog', () => {
const onSelect = vi.fn();
const { stdin, unmount, rerender, waitUntilReady } = render(
<KeypressProvider>
<SettingsDialog settings={settings} onSelect={onSelect} />
</KeypressProvider>,
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</SettingsContext.Provider>,
);
await waitUntilReady();
@@ -1424,14 +1364,13 @@ describe('SettingsDialog', () => {
path: '',
},
});
await act(async () => {
rerender(
rerender(
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog settings={settings} onSelect={onSelect} />
</KeypressProvider>,
);
});
await waitUntilReady();
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</SettingsContext.Provider>,
);
// Press Escape to exit
await act(async () => {
@@ -1447,6 +1386,74 @@ describe('SettingsDialog', () => {
});
});
describe('Array Settings Editing', () => {
const typeInput = async (
stdin: { write: (data: string) => void },
input: string,
) => {
for (const ch of input) {
await act(async () => {
stdin.write(ch);
});
}
};
it('should parse comma-separated input as string arrays', async () => {
vi.mocked(getSettingsSchema).mockReturnValue(ARRAY_FAKE_SCHEMA);
const settings = createMockSettings();
const setValueSpy = vi.spyOn(settings, 'setValue');
const { stdin, unmount } = renderDialog(settings, vi.fn());
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Start editing first array setting
});
await typeInput(stdin, 'first/path, second/path,third/path');
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Commit
});
await waitFor(() => {
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'context.fileFiltering.customIgnoreFilePaths',
['first/path', 'second/path', 'third/path'],
);
});
unmount();
});
it('should parse JSON array input for allowedExtensions', async () => {
vi.mocked(getSettingsSchema).mockReturnValue(ARRAY_FAKE_SCHEMA);
const settings = createMockSettings();
const setValueSpy = vi.spyOn(settings, 'setValue');
const { stdin, unmount } = renderDialog(settings, vi.fn());
await act(async () => {
stdin.write(TerminalKeys.DOWN_ARROW as string); // Move to second array setting
});
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Start editing
});
await typeInput(stdin, '["^github\\\\.com/.*$", "^gitlab\\\\.com/.*$"]');
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Commit
});
await waitFor(() => {
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'security.allowedExtensions',
['^github\\.com/.*$', '^gitlab\\.com/.*$'],
);
});
unmount();
});
});
describe('Search Functionality', () => {
it('should display text entered in search', async () => {
const settings = createMockSettings();
+120 -366
View File
@@ -5,40 +5,35 @@
*/
import type React from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { Text } from 'ink';
import { AsyncFzf } from 'fzf';
import type { Key } from '../hooks/useKeypress.js';
import { theme } from '../semantic-colors.js';
import type {
LoadableSettingScope,
LoadedSettings,
Settings,
} from '../../config/settings.js';
import type { LoadableSettingScope, Settings } from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js';
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
import {
getDialogSettingKeys,
setPendingSettingValue,
getDisplayValue,
hasRestartRequiredSettings,
saveModifiedSettings,
getSettingDefinition,
isDefaultValue,
requiresRestart,
getRestartRequiredFromModified,
getEffectiveDefaultValue,
setPendingSettingValueAny,
getDialogRestartRequiredSettings,
getEffectiveValue,
isInSettingsScope,
getEditValue,
parseEditedValue,
} from '../../utils/settingsUtils.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import {
useSettingsStore,
type SettingsState,
} from '../contexts/SettingsContext.js';
import { getCachedStringWidth } from '../utils/textUtils.js';
import {
type SettingsType,
type SettingsValue,
TOGGLE_TYPES,
} from '../../config/settingsSchema.js';
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import { debugLogger } from '@google/gemini-cli-core';
import { useSearchBuffer } from '../hooks/useSearchBuffer.js';
import {
@@ -55,31 +50,56 @@ interface FzfResult {
}
interface SettingsDialogProps {
settings: LoadedSettings;
onSelect: (settingName: string | undefined, scope: SettingScope) => void;
onRestartRequest?: () => void;
availableTerminalHeight?: number;
config?: Config;
}
const MAX_ITEMS_TO_SHOW = 8;
// Create a snapshot of the initial per-scope state of Restart Required Settings
// This creates a nested map of the form
// restartRequiredSetting -> Map { scopeName -> value }
function getActiveRestartRequiredSettings(
settings: SettingsState,
): Map<string, Map<string, string>> {
const snapshot = new Map<string, Map<string, string>>();
const scopes: Array<[string, Settings]> = [
['User', settings.user.settings],
['Workspace', settings.workspace.settings],
['System', settings.system.settings],
];
for (const key of getDialogRestartRequiredSettings()) {
const scopeMap = new Map<string, string>();
for (const [scopeName, scopeSettings] of scopes) {
// Raw per-scope value (undefined if not in file)
const value = isInSettingsScope(key, scopeSettings)
? getEffectiveValue(key, scopeSettings)
: undefined;
scopeMap.set(scopeName, JSON.stringify(value));
}
snapshot.set(key, scopeMap);
}
return snapshot;
}
export function SettingsDialog({
settings,
onSelect,
onRestartRequest,
availableTerminalHeight,
config,
}: SettingsDialogProps): React.JSX.Element {
// Get vim mode context to sync vim mode changes
const { vimEnabled, toggleVimEnabled } = useVimMode();
// Reactive settings from store (re-renders on any settings change)
const { settings, setSetting } = useSettingsStore();
// Scope selector state (User by default)
const [selectedScope, setSelectedScope] = useState<LoadableSettingScope>(
SettingScope.User,
);
const [showRestartPrompt, setShowRestartPrompt] = useState(false);
// Snapshot restart-required values at mount time for diff tracking
const [activeRestartRequiredSettings] = useState(() =>
getActiveRestartRequiredSettings(settings),
);
// Search state
const [searchQuery, setSearchQuery] = useState('');
@@ -136,52 +156,34 @@ export function SettingsDialog({
};
}, [searchQuery, fzfInstance, searchMap]);
// Local pending settings state for the selected scope
const [pendingSettings, setPendingSettings] = useState<Settings>(() =>
// Deep clone to avoid mutation
structuredClone(settings.forScope(selectedScope).settings),
);
// Track whether a restart is required to apply the changes in the Settings json file
// This does not care for inheritance
// It checks whether a proposed change from this UI to a settings.json file requires a restart to take effect in the app
const pendingRestartRequiredSettings = useMemo(() => {
const changed = new Set<string>();
const scopes: Array<[string, Settings]> = [
['User', settings.user.settings],
['Workspace', settings.workspace.settings],
['System', settings.system.settings],
];
// Track which settings have been modified by the user
const [modifiedSettings, setModifiedSettings] = useState<Set<string>>(
new Set(),
);
// Preserve pending changes across scope switches
type PendingValue = boolean | number | string;
const [globalPendingChanges, setGlobalPendingChanges] = useState<
Map<string, PendingValue>
>(new Map());
// Track restart-required settings across scope changes
const [_restartRequiredSettings, setRestartRequiredSettings] = useState<
Set<string>
>(new Set());
useEffect(() => {
// Base settings for selected scope
let updated = structuredClone(settings.forScope(selectedScope).settings);
// Overlay globally pending (unsaved) changes so user sees their modifications in any scope
const newModified = new Set<string>();
const newRestartRequired = new Set<string>();
for (const [key, value] of globalPendingChanges.entries()) {
const def = getSettingDefinition(key);
if (def?.type === 'boolean' && typeof value === 'boolean') {
updated = setPendingSettingValue(key, value, updated);
} else if (
(def?.type === 'number' && typeof value === 'number') ||
(def?.type === 'string' && typeof value === 'string')
) {
updated = setPendingSettingValueAny(key, value, updated);
// Iterate through the nested map snapshot in activeRestartRequiredSettings, diff with current settings
for (const [key, initialScopeMap] of activeRestartRequiredSettings) {
for (const [scopeName, scopeSettings] of scopes) {
const currentValue = isInSettingsScope(key, scopeSettings)
? getEffectiveValue(key, scopeSettings)
: undefined;
const initialJson = initialScopeMap.get(scopeName);
if (JSON.stringify(currentValue) !== initialJson) {
changed.add(key);
break; // one scope changed is enough
}
}
newModified.add(key);
if (requiresRestart(key)) newRestartRequired.add(key);
}
setPendingSettings(updated);
setModifiedSettings(newModified);
setRestartRequiredSettings(newRestartRequired);
setShowRestartPrompt(newRestartRequired.size > 0);
}, [selectedScope, settings, globalPendingChanges]);
return changed;
}, [settings, activeRestartRequiredSettings]);
const showRestartPrompt = pendingRestartRequiredSettings.size > 0;
// Calculate max width for the left column (Label/Description) to keep values aligned or close
const maxLabelOrDescriptionWidth = useMemo(() => {
@@ -222,16 +224,10 @@ export function SettingsDialog({
return settingKeys.map((key) => {
const definition = getSettingDefinition(key);
const type = definition?.type ?? 'string';
const type: SettingsType = definition?.type ?? 'string';
// Get the display value (with * indicator if modified)
const displayValue = getDisplayValue(
key,
scopeSettings,
mergedSettings,
modifiedSettings,
pendingSettings,
);
const displayValue = getDisplayValue(key, scopeSettings, mergedSettings);
// Get the scope message (e.g., "(Modified in Workspace)")
const scopeMessage = getScopeMessageForSetting(
@@ -240,28 +236,28 @@ export function SettingsDialog({
settings,
);
// Check if the value is at default (grey it out)
const isGreyedOut = isDefaultValue(key, scopeSettings);
// Grey out values that defer to defaults
const isGreyedOut = !isInSettingsScope(key, scopeSettings);
// Get raw value for edit mode initialization
const rawValue = getEffectiveValue(key, pendingSettings, {});
// Some settings can be edited by an inline editor
const rawValue = getEffectiveValue(key, scopeSettings);
// The inline editor needs a string but non primitive settings like Arrays and Objects exist
const editValue = getEditValue(type, rawValue);
return {
key,
label: definition?.label || key,
description: definition?.description,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type: type as 'boolean' | 'number' | 'string' | 'enum',
type,
displayValue,
isGreyedOut,
scopeMessage,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
rawValue: rawValue as string | number | boolean | undefined,
rawValue,
editValue,
};
});
}, [settingKeys, selectedScope, settings, modifiedSettings, pendingSettings]);
}, [settingKeys, selectedScope, settings]);
// Scope selection handler
const handleScopeChange = useCallback((scope: LoadableSettingScope) => {
setSelectedScope(scope);
}, []);
@@ -273,17 +269,21 @@ export function SettingsDialog({
if (!TOGGLE_TYPES.has(definition?.type)) {
return;
}
const currentValue = getEffectiveValue(key, pendingSettings, {});
const scopeSettings = settings.forScope(selectedScope).settings;
const currentValue = getEffectiveValue(key, scopeSettings);
let newValue: SettingsValue;
if (definition?.type === 'boolean') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
newValue = !(currentValue as boolean);
setPendingSettings((prev) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
setPendingSettingValue(key, newValue as boolean, prev),
);
if (typeof currentValue !== 'boolean') {
return;
}
newValue = !currentValue;
} else if (definition?.type === 'enum' && definition.options) {
const options = definition.options;
if (options.length === 0) {
return;
}
const currentIndex = options?.findIndex(
(opt) => opt.value === currentValue,
);
@@ -292,303 +292,58 @@ export function SettingsDialog({
} else {
newValue = options[0].value; // loop back to start.
}
setPendingSettings((prev) =>
setPendingSettingValueAny(key, newValue, prev),
);
}
if (!requiresRestart(key)) {
const immediateSettings = new Set([key]);
const currentScopeSettings = settings.forScope(selectedScope).settings;
const immediateSettingsObject = setPendingSettingValueAny(
key,
newValue,
currentScopeSettings,
);
debugLogger.log(
`[DEBUG SettingsDialog] Saving ${key} immediately with value:`,
newValue,
);
saveModifiedSettings(
immediateSettings,
immediateSettingsObject,
settings,
selectedScope,
);
// Special handling for vim mode to sync with VimModeContext
if (key === 'general.vimMode' && newValue !== vimEnabled) {
// Call toggleVimEnabled to sync the VimModeContext local state
toggleVimEnabled().catch((error) => {
coreEvents.emitFeedback(
'error',
'Failed to toggle vim mode:',
error,
);
});
}
// Remove from modifiedSettings since it's now saved
setModifiedSettings((prev) => {
const updated = new Set(prev);
updated.delete(key);
return updated;
});
// Also remove from restart-required settings if it was there
setRestartRequiredSettings((prev) => {
const updated = new Set(prev);
updated.delete(key);
return updated;
});
// Remove from global pending changes if present
setGlobalPendingChanges((prev) => {
if (!prev.has(key)) return prev;
const next = new Map(prev);
next.delete(key);
return next;
});
} else {
// For restart-required settings, track as modified
setModifiedSettings((prev) => {
const updated = new Set(prev).add(key);
const needsRestart = hasRestartRequiredSettings(updated);
debugLogger.log(
`[DEBUG SettingsDialog] Modified settings:`,
Array.from(updated),
'Needs restart:',
needsRestart,
);
if (needsRestart) {
setShowRestartPrompt(true);
setRestartRequiredSettings((prevRestart) =>
new Set(prevRestart).add(key),
);
}
return updated;
});
// Record pending change globally
setGlobalPendingChanges((prev) => {
const next = new Map(prev);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
next.set(key, newValue as PendingValue);
return next;
});
}
},
[pendingSettings, settings, selectedScope, vimEnabled, toggleVimEnabled],
);
// Edit commit handler
const handleEditCommit = useCallback(
(key: string, newValue: string, _item: SettingsDialogItem) => {
const definition = getSettingDefinition(key);
const type = definition?.type;
if (newValue.trim() === '' && type === 'number') {
// Nothing entered for a number; cancel edit
return;
}
let parsed: string | number;
if (type === 'number') {
const numParsed = Number(newValue.trim());
if (Number.isNaN(numParsed)) {
// Invalid number; cancel edit
return;
}
parsed = numParsed;
} else {
// For strings, use the buffer as is.
parsed = newValue;
}
// Update pending
setPendingSettings((prev) =>
setPendingSettingValueAny(key, parsed, prev),
debugLogger.log(
`[DEBUG SettingsDialog] Saving ${key} immediately with value:`,
newValue,
);
if (!requiresRestart(key)) {
const immediateSettings = new Set([key]);
const currentScopeSettings = settings.forScope(selectedScope).settings;
const immediateSettingsObject = setPendingSettingValueAny(
key,
parsed,
currentScopeSettings,
);
saveModifiedSettings(
immediateSettings,
immediateSettingsObject,
settings,
selectedScope,
);
// Remove from modified sets if present
setModifiedSettings((prev) => {
const updated = new Set(prev);
updated.delete(key);
return updated;
});
setRestartRequiredSettings((prev) => {
const updated = new Set(prev);
updated.delete(key);
return updated;
});
// Remove from global pending since it's immediately saved
setGlobalPendingChanges((prev) => {
if (!prev.has(key)) return prev;
const next = new Map(prev);
next.delete(key);
return next;
});
} else {
// Mark as modified and needing restart
setModifiedSettings((prev) => {
const updated = new Set(prev).add(key);
const needsRestart = hasRestartRequiredSettings(updated);
if (needsRestart) {
setShowRestartPrompt(true);
setRestartRequiredSettings((prevRestart) =>
new Set(prevRestart).add(key),
);
}
return updated;
});
// Record pending change globally for persistence across scopes
setGlobalPendingChanges((prev) => {
const next = new Map(prev);
next.set(key, parsed as PendingValue);
return next;
});
}
setSetting(selectedScope, key, newValue);
},
[settings, selectedScope],
[settings, selectedScope, setSetting],
);
// For inline editor
const handleEditCommit = useCallback(
(key: string, newValue: string, _item: SettingsDialogItem) => {
const definition = getSettingDefinition(key);
const type: SettingsType = definition?.type ?? 'string';
const parsed = parseEditedValue(type, newValue);
if (parsed === null) {
return;
}
setSetting(selectedScope, key, parsed);
},
[selectedScope, setSetting],
);
// Clear/reset handler - removes the value from settings.json so it falls back to default
const handleItemClear = useCallback(
(key: string, _item: SettingsDialogItem) => {
const defaultValue = getEffectiveDefaultValue(key, config);
// Update local pending state to show the default value
if (typeof defaultValue === 'boolean') {
setPendingSettings((prev) =>
setPendingSettingValue(key, defaultValue, prev),
);
} else if (
typeof defaultValue === 'number' ||
typeof defaultValue === 'string'
) {
setPendingSettings((prev) =>
setPendingSettingValueAny(key, defaultValue, prev),
);
}
// Clear the value from settings.json (set to undefined to remove the key)
if (!requiresRestart(key)) {
settings.setValue(selectedScope, key, undefined);
// Special handling for vim mode
if (key === 'general.vimMode') {
const booleanDefaultValue =
typeof defaultValue === 'boolean' ? defaultValue : false;
if (booleanDefaultValue !== vimEnabled) {
toggleVimEnabled().catch((error) => {
coreEvents.emitFeedback(
'error',
'Failed to toggle vim mode:',
error,
);
});
}
}
}
// Remove from modified sets
setModifiedSettings((prev) => {
const updated = new Set(prev);
updated.delete(key);
return updated;
});
setRestartRequiredSettings((prev) => {
const updated = new Set(prev);
updated.delete(key);
return updated;
});
setGlobalPendingChanges((prev) => {
if (!prev.has(key)) return prev;
const next = new Map(prev);
next.delete(key);
return next;
});
// Update restart prompt
setShowRestartPrompt((_prev) => {
const remaining = getRestartRequiredFromModified(modifiedSettings);
return remaining.filter((k) => k !== key).length > 0;
});
setSetting(selectedScope, key, undefined);
},
[
config,
settings,
selectedScope,
vimEnabled,
toggleVimEnabled,
modifiedSettings,
],
[selectedScope, setSetting],
);
const saveRestartRequiredSettings = useCallback(() => {
const restartRequiredSettings =
getRestartRequiredFromModified(modifiedSettings);
const restartRequiredSet = new Set(restartRequiredSettings);
if (restartRequiredSet.size > 0) {
saveModifiedSettings(
restartRequiredSet,
pendingSettings,
settings,
selectedScope,
);
// Remove saved keys from global pending changes
setGlobalPendingChanges((prev) => {
if (prev.size === 0) return prev;
const next = new Map(prev);
for (const key of restartRequiredSet) {
next.delete(key);
}
return next;
});
}
}, [modifiedSettings, pendingSettings, settings, selectedScope]);
// Close handler
const handleClose = useCallback(() => {
// Save any restart-required settings before closing
saveRestartRequiredSettings();
onSelect(undefined, selectedScope as SettingScope);
}, [saveRestartRequiredSettings, onSelect, selectedScope]);
}, [onSelect, selectedScope]);
// Custom key handler for restart key
const handleKeyPress = useCallback(
(key: Key, _currentItem: SettingsDialogItem | undefined): boolean => {
// 'r' key for restart
if (showRestartPrompt && key.sequence === 'r') {
saveRestartRequiredSettings();
setShowRestartPrompt(false);
setModifiedSettings(new Set());
setRestartRequiredSettings(new Set());
if (onRestartRequest) onRestartRequest();
return true;
}
return false;
},
[showRestartPrompt, onRestartRequest, saveRestartRequiredSettings],
[showRestartPrompt, onRestartRequest],
);
// Calculate effective max items and scope visibility based on terminal height
@@ -673,11 +428,10 @@ export function SettingsDialog({
showRestartPrompt,
]);
// Footer content for restart prompt
const footerContent = showRestartPrompt ? (
<Text color={theme.status.warning}>
To see changes, Gemini CLI must be restarted. Press r to exit and apply
changes now.
Changes that require a restart have been modified. Press r to exit and
apply changes now.
</Text>
) : null;
+14 -18
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -11,22 +11,18 @@ import type { Config } from '@google/gemini-cli-core';
describe('Tips', () => {
it.each([
[0, '3. Create GEMINI.md files'],
[5, '3. /help for more information'],
])(
'renders correct tips when file count is %i',
async (count, expectedText) => {
const config = {
getGeminiMdFileCount: vi.fn().mockReturnValue(count),
} as unknown as Config;
{ fileCount: 0, description: 'renders all tips including GEMINI.md tip' },
{ fileCount: 5, description: 'renders fewer tips when GEMINI.md exists' },
])('$description', async ({ fileCount }) => {
const config = {
getGeminiMdFileCount: vi.fn().mockReturnValue(fileCount),
} as unknown as Config;
const { lastFrame, waitUntilReady, unmount } = render(
<Tips config={config} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain(expectedText);
unmount();
},
);
const { lastFrame, waitUntilReady, unmount } = render(
<Tips config={config} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
+14 -18
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -15,30 +15,26 @@ interface TipsProps {
export const Tips: React.FC<TipsProps> = ({ config }) => {
const geminiMdFileCount = config.getGeminiMdFileCount();
return (
<Box flexDirection="column">
<Box flexDirection="column" marginTop={1}>
<Text color={theme.text.primary}>Tips for getting started:</Text>
<Text color={theme.text.primary}>
1. Ask questions, edit files, or run commands.
</Text>
<Text color={theme.text.primary}>
2. Be specific for the best results.
</Text>
{geminiMdFileCount === 0 && (
<Text color={theme.text.primary}>
3. Create{' '}
<Text bold color={theme.text.accent}>
GEMINI.md
</Text>{' '}
files to customize your interactions with Gemini.
1. Create <Text bold>GEMINI.md</Text> files to customize your
interactions
</Text>
)}
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '4.' : '3.'}{' '}
<Text bold color={theme.text.accent}>
/help
</Text>{' '}
for more information.
{geminiMdFileCount === 0 ? '2.' : '1.'}{' '}
<Text color={theme.text.secondary}>/help</Text> for more information
</Text>
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '3.' : '2.'} Ask coding questions, edit code
or run commands
</Text>
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '4.' : '3.'} Be specific for the best results
</Text>
</Box>
);
@@ -45,12 +45,12 @@ describe('<UserIdentity />', () => {
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Logged in with Google: test@example.com');
expect(output).toContain('test@example.com');
expect(output).toContain('/auth');
unmount();
});
it('should render login message without colon if email is missing', async () => {
it('should render login message if email is missing', async () => {
// Modify the mock for this specific test
vi.mocked(UserAccountManager).mockImplementationOnce(
() =>
@@ -73,12 +73,11 @@ describe('<UserIdentity />', () => {
const output = lastFrame();
expect(output).toContain('Logged in with Google');
expect(output).not.toContain('Logged in with Google:');
expect(output).toContain('/auth');
unmount();
});
it('should render plan name on a separate line if provided', async () => {
it('should render plan name and upgrade indicator', async () => {
const mockConfig = makeFakeConfig();
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
@@ -92,18 +91,10 @@ describe('<UserIdentity />', () => {
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Logged in with Google: test@example.com');
expect(output).toContain('test@example.com');
expect(output).toContain('/auth');
expect(output).toContain('Plan: Premium Plan');
// Check for two lines (or more if wrapped, but here it should be separate)
const lines = output?.split('\n').filter((line) => line.trim().length > 0);
expect(lines?.some((line) => line.includes('Logged in with Google'))).toBe(
true,
);
expect(lines?.some((line) => line.includes('Plan: Premium Plan'))).toBe(
true,
);
expect(output).toContain('Premium Plan');
expect(output).toContain('/upgrade');
unmount();
});
+24 -21
View File
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useMemo } from 'react';
import { useMemo, useEffect, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import {
@@ -20,42 +20,45 @@ interface UserIdentityProps {
export const UserIdentity: React.FC<UserIdentityProps> = ({ config }) => {
const authType = config.getContentGeneratorConfig()?.authType;
const [email, setEmail] = useState<string | undefined>();
const { email, tierName } = useMemo(() => {
if (!authType) {
return { email: undefined, tierName: undefined };
useEffect(() => {
if (authType) {
const userAccountManager = new UserAccountManager();
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
}
const userAccountManager = new UserAccountManager();
return {
email: userAccountManager.getCachedGoogleAccount(),
tierName: config.getUserTierName(),
};
}, [config, authType]);
}, [authType]);
const tierName = useMemo(
() => (authType ? config.getUserTierName() : undefined),
[config, authType],
);
if (!authType) {
return null;
}
return (
<Box marginTop={1} flexDirection="column">
<Box flexDirection="column">
{/* User Email /auth */}
<Box>
<Text color={theme.text.primary}>
<Text color={theme.text.primary} wrap="truncate-end">
{authType === AuthType.LOGIN_WITH_GOOGLE ? (
<Text>
<Text bold>Logged in with Google{email ? ':' : ''}</Text>
{email ? ` ${email}` : ''}
</Text>
<Text>{email ?? 'Logged in with Google'}</Text>
) : (
`Authenticated with ${authType}`
)}
</Text>
<Text color={theme.text.secondary}> /auth</Text>
</Box>
{tierName && (
<Text color={theme.text.primary}>
<Text bold>Plan:</Text> {tierName}
{/* Tier Name /upgrade */}
<Box>
<Text color={theme.text.primary} wrap="truncate-end">
{tierName ?? 'Gemini Code Assist for individuals'}
</Text>
)}
<Text color={theme.text.secondary}> /upgrade</Text>
</Box>
</Box>
);
};
@@ -2,20 +2,17 @@
exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
Action Required (was prompted):
@@ -25,20 +22,17 @@ Action Required (was prompted):
exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -52,39 +46,33 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`AlternateBufferQuittingDisplay > renders with history but no pending items > with_history_no_pending 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -98,39 +86,33 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello Gemini
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
@@ -2,82 +2,70 @@
exports[`<AppHeader /> > should not render the banner when no flags are set 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should not render the default banner if shown count is 5 or more 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should render the banner with default text 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ This is the default banner │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should render the banner with warning text 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ There are capacity issues │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
@@ -0,0 +1,124 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`HooksDialog > snapshots > renders empty hooks dialog 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ No hooks configured. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`HooksDialog > snapshots > renders hook using command as name when name is not provided 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Security Warning: │
│ Hooks can execute arbitrary commands on your system. Only use hooks from sources you trust. │
│ Review hook scripts carefully. │
│ │
│ Learn more: https://geminicli.com/docs/hooks │
│ │
│ Configured Hooks │
│ │
│ before-tool │
│ │
│ echo hello [enabled] │
│ Source: /mock/path │
│ │
│ │
│ Tip: Use /hooks enable <hook-name> or /hooks disable <hook-name> to toggle individual hooks. Use │
│ /hooks enable-all or /hooks disable-all to toggle all hooks at once. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`HooksDialog > snapshots > renders hook with all metadata (matcher, sequential, timeout) 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Security Warning: │
│ Hooks can execute arbitrary commands on your system. Only use hooks from sources you trust. │
│ Review hook scripts carefully. │
│ │
│ Learn more: https://geminicli.com/docs/hooks │
│ │
│ Configured Hooks │
│ │
│ before-tool │
│ │
│ my-hook [enabled] │
│ A hook with all metadata fields │
│ Source: /mock/path/GEMINI.md | Matcher: shell_exec | Sequential | Timeout: 30s │
│ │
│ │
│ Tip: Use /hooks enable <hook-name> or /hooks disable <hook-name> to toggle individual hooks. Use │
│ /hooks enable-all or /hooks disable-all to toggle all hooks at once. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`HooksDialog > snapshots > renders hooks grouped by event name with enabled and disabled status 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Security Warning: │
│ Hooks can execute arbitrary commands on your system. Only use hooks from sources you trust. │
│ Review hook scripts carefully. │
│ │
│ Learn more: https://geminicli.com/docs/hooks │
│ │
│ Configured Hooks │
│ │
│ before-tool │
│ │
│ hook1 [enabled] │
│ Test hook: hook1 │
│ Source: /mock/path/GEMINI.md | Command: run-hook1 │
│ │
│ hook2 [disabled] │
│ Test hook: hook2 │
│ Source: /mock/path/GEMINI.md | Command: run-hook2 │
│ │
│ after-agent │
│ │
│ hook3 [enabled] │
│ Test hook: hook3 │
│ Source: /mock/path/GEMINI.md | Command: run-hook3 │
│ │
│ │
│ Tip: Use /hooks enable <hook-name> or /hooks disable <hook-name> to toggle individual hooks. Use │
│ /hooks enable-all or /hooks disable-all to toggle all hooks at once. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`HooksDialog > snapshots > renders single hook with security warning, source, and tips 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Security Warning: │
│ Hooks can execute arbitrary commands on your system. Only use hooks from sources you trust. │
│ Review hook scripts carefully. │
│ │
│ Learn more: https://geminicli.com/docs/hooks │
│ │
│ Configured Hooks │
│ │
│ before-tool │
│ │
│ test-hook [enabled] │
│ Test hook: test-hook │
│ Source: /mock/path/GEMINI.md | Command: run-test-hook │
│ │
│ │
│ Tip: Use /hooks enable <hook-name> or /hooks disable <hook-name> to toggle individual hooks. Use │
│ /hooks enable-all or /hooks disable-all to toggle all hooks at once. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -1,21 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SessionRetentionWarningDialog > should match snapshot 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Keep chat history │
│ │
│ To keep your workspace clean, we are introducing a limit on how long chat sessions are stored. │
│ Please choose a retention period for your existing chats: │
│ │
│ │
│ 1. Keep for 30 days (Recommended) │
│ 123 sessions will be deleted │
│ ● 2. Keep for 120 days │
│ No sessions will be deleted at this time │
│ │
│ Set a custom limit /settings and change "Keep chat history". │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -0,0 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Tips > 'renders all tips including GEMINI.md …' 1`] = `
"
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`Tips > 'renders fewer tips when GEMINI.md exi…' 1`] = `
"
Tips for getting started:
1. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
"
`;
@@ -0,0 +1,193 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render, cleanup } from '../../../test-utils/render.js';
import { SubagentProgressDisplay } from './SubagentProgressDisplay.js';
import type { SubagentProgress } from '@google/gemini-cli-core';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { Text } from 'ink';
vi.mock('ink-spinner', () => ({
default: () => <Text></Text>,
}));
describe('<SubagentProgressDisplay />', () => {
afterEach(() => {
vi.restoreAllMocks();
cleanup();
});
it('renders correctly with description in args', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '1',
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello", "description": "Say hello"}',
status: 'running',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly with displayName and description from item', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '1',
type: 'tool_call',
content: 'run_shell_command',
displayName: 'RunShellCommand',
description: 'Executing echo hello',
args: '{"command": "echo hello"}',
status: 'running',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly with command fallback', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '2',
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello"}',
status: 'running',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly with file_path', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '3',
type: 'tool_call',
content: 'write_file',
args: '{"file_path": "/tmp/test.txt", "content": "foo"}',
status: 'completed',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('truncates long args', async () => {
const longDesc =
'This is a very long description that should definitely be truncated because it exceeds the limit of sixty characters.';
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '4',
type: 'tool_call',
content: 'run_shell_command',
args: JSON.stringify({ description: longDesc }),
status: 'running',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders thought bubbles correctly', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '5',
type: 'thought',
content: 'Thinking about life',
status: 'running',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders cancelled state correctly', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [],
state: 'cancelled',
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders "Request cancelled." with the info icon', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '6',
type: 'thought',
content: 'Request cancelled.',
status: 'error',
},
],
};
const { lastFrame, waitUntilReady } = render(
<SubagentProgressDisplay progress={progress} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
});
@@ -0,0 +1,151 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import Spinner from 'ink-spinner';
import type {
SubagentProgress,
SubagentActivityItem,
} from '@google/gemini-cli-core';
import { TOOL_STATUS } from '../../constants.js';
import { STATUS_INDICATOR_WIDTH } from './ToolShared.js';
export interface SubagentProgressDisplayProps {
progress: SubagentProgress;
}
const formatToolArgs = (args?: string): string => {
if (!args) return '';
try {
const parsed: unknown = JSON.parse(args);
if (typeof parsed !== 'object' || parsed === null) {
return args;
}
if (
'description' in parsed &&
typeof parsed.description === 'string' &&
parsed.description
) {
return parsed.description;
}
if ('command' in parsed && typeof parsed.command === 'string')
return parsed.command;
if ('file_path' in parsed && typeof parsed.file_path === 'string')
return parsed.file_path;
if ('dir_path' in parsed && typeof parsed.dir_path === 'string')
return parsed.dir_path;
if ('query' in parsed && typeof parsed.query === 'string')
return parsed.query;
if ('url' in parsed && typeof parsed.url === 'string') return parsed.url;
if ('target' in parsed && typeof parsed.target === 'string')
return parsed.target;
return args;
} catch {
return args;
}
};
export const SubagentProgressDisplay: React.FC<
SubagentProgressDisplayProps
> = ({ progress }) => {
let headerText: string | undefined;
let headerColor = theme.text.secondary;
if (progress.state === 'cancelled') {
headerText = `Subagent ${progress.agentName} was cancelled.`;
headerColor = theme.status.warning;
} else if (progress.state === 'error') {
headerText = `Subagent ${progress.agentName} failed.`;
headerColor = theme.status.error;
} else if (progress.state === 'completed') {
headerText = `Subagent ${progress.agentName} completed.`;
headerColor = theme.status.success;
}
return (
<Box flexDirection="column" paddingY={0}>
{headerText && (
<Box marginBottom={1}>
<Text color={headerColor} italic>
{headerText}
</Text>
</Box>
)}
<Box flexDirection="column" marginLeft={0} gap={0}>
{progress.recentActivity.map((item: SubagentActivityItem) => {
if (item.type === 'thought') {
const isCancellation = item.content === 'Request cancelled.';
const icon = isCancellation ? ' ' : '💭';
const color = isCancellation
? theme.status.warning
: theme.text.secondary;
return (
<Box key={item.id} flexDirection="row">
<Box minWidth={STATUS_INDICATOR_WIDTH}>
<Text color={color}>{icon}</Text>
</Box>
<Box flexGrow={1}>
<Text color={color}>{item.content}</Text>
</Box>
</Box>
);
} else if (item.type === 'tool_call') {
const statusSymbol =
item.status === 'running' ? (
<Spinner type="dots" />
) : item.status === 'completed' ? (
<Text color={theme.status.success}>{TOOL_STATUS.SUCCESS}</Text>
) : item.status === 'cancelled' ? (
<Text color={theme.status.warning} bold>
{TOOL_STATUS.CANCELED}
</Text>
) : (
<Text color={theme.status.error}>{TOOL_STATUS.ERROR}</Text>
);
const formattedArgs = item.description || formatToolArgs(item.args);
const displayArgs =
formattedArgs.length > 60
? formattedArgs.slice(0, 60) + '...'
: formattedArgs;
return (
<Box key={item.id} flexDirection="row">
<Box minWidth={STATUS_INDICATOR_WIDTH}>{statusSymbol}</Box>
<Box flexDirection="row" flexGrow={1} flexWrap="wrap">
<Text
bold
color={theme.text.primary}
strikethrough={item.status === 'cancelled'}
>
{item.displayName || item.content}
</Text>
{displayArgs && (
<Box marginLeft={1}>
<Text
color={theme.text.secondary}
wrap="truncate"
strikethrough={item.status === 'cancelled'}
>
{displayArgs}
</Text>
</Box>
)}
</Box>
</Box>
);
}
return null;
})}
</Box>
</Box>
);
};
@@ -75,6 +75,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
status: t.status,
approvalMode: t.approvalMode,
hasResultDisplay: !!t.resultDisplay,
parentCallId: t.parentCallId,
});
}),
[allToolCalls, isLowErrorVerbosity],
@@ -11,7 +11,11 @@ import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { theme } from '../../semantic-colors.js';
import type { AnsiOutput, AnsiLine } from '@google/gemini-cli-core';
import {
type AnsiOutput,
type AnsiLine,
isSubagentProgress,
} from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
import { tryParseJSON } from '../../../utils/jsonoutput.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
@@ -20,6 +24,7 @@ import { ScrollableList } from '../shared/ScrollableList.js';
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
import { calculateToolContentMaxLines } from '../../utils/toolLayoutUtils.js';
import { SubagentProgressDisplay } from './SubagentProgressDisplay.js';
// Large threshold to ensure we don't cause performance issues for very large
// outputs that will get truncated further MaxSizedBox anyway.
@@ -167,6 +172,8 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
{formattedJSON}
</Text>
);
} else if (isSubagentProgress(truncatedResultDisplay)) {
content = <SubagentProgressDisplay progress={truncatedResultDisplay} />;
} else if (
typeof truncatedResultDisplay === 'string' &&
renderOutputAsMarkdown
@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<SubagentProgressDisplay /> > renders "Request cancelled." with the info icon 1`] = `
" Request cancelled.
"
`;
exports[`<SubagentProgressDisplay /> > renders cancelled state correctly 1`] = `
"Subagent TestAgent was cancelled.
"
`;
exports[`<SubagentProgressDisplay /> > renders correctly with command fallback 1`] = `
"⠋ run_shell_command echo hello
"
`;
exports[`<SubagentProgressDisplay /> > renders correctly with description in args 1`] = `
"⠋ run_shell_command Say hello
"
`;
exports[`<SubagentProgressDisplay /> > renders correctly with displayName and description from item 1`] = `
"⠋ RunShellCommand Executing echo hello
"
`;
exports[`<SubagentProgressDisplay /> > renders correctly with file_path 1`] = `
"✓ write_file /tmp/test.txt
"
`;
exports[`<SubagentProgressDisplay /> > renders thought bubbles correctly 1`] = `
"💭 Thinking about life
"
`;
exports[`<SubagentProgressDisplay /> > truncates long args 1`] = `
"⠋ run_shell_command This is a very long description that should definitely be tr...
"
`;
@@ -531,6 +531,37 @@ describe('BaseSettingsDialog', () => {
});
describe('edit mode', () => {
it('should prioritize editValue over rawValue stringification', async () => {
const objectItem: SettingsDialogItem = {
key: 'object-setting',
label: 'Object Setting',
description: 'A complex object setting',
displayValue: '{"foo":"bar"}',
type: 'object',
rawValue: { foo: 'bar' },
editValue: '{"foo":"bar"}',
};
const { stdin } = await renderDialog({
items: [objectItem],
});
// Enter edit mode and immediately commit
await act(async () => {
stdin.write(TerminalKeys.ENTER);
});
await act(async () => {
stdin.write(TerminalKeys.ENTER);
});
await waitFor(() => {
expect(mockOnEditCommit).toHaveBeenCalledWith(
'object-setting',
'{"foo":"bar"}',
expect.objectContaining({ type: 'object' }),
);
});
});
it('should commit edit on Enter', async () => {
const items = createMockItems(4);
const stringItem = items.find((i) => i.type === 'string')!;
@@ -9,6 +9,10 @@ import { Box, Text } from 'ink';
import chalk from 'chalk';
import { theme } from '../../semantic-colors.js';
import type { LoadableSettingScope } from '../../../config/settings.js';
import type {
SettingsType,
SettingsValue,
} from '../../../config/settingsSchema.js';
import { getScopeItems } from '../../../utils/dialogScopeUtils.js';
import { RadioButtonSelect } from './RadioButtonSelect.js';
import { TextInput } from './TextInput.js';
@@ -33,7 +37,7 @@ export interface SettingsDialogItem {
/** Optional description below label */
description?: string;
/** Item type for determining interaction behavior */
type: 'boolean' | 'number' | 'string' | 'enum';
type: SettingsType;
/** Pre-formatted display value (with * if modified) */
displayValue: string;
/** Grey out value (at default) */
@@ -41,7 +45,9 @@ export interface SettingsDialogItem {
/** Scope message e.g., "(Modified in Workspace)" */
scopeMessage?: string;
/** Raw value for edit mode initialization */
rawValue?: string | number | boolean;
rawValue?: SettingsValue;
/** Optional pre-formatted edit buffer value for complex types */
editValue?: string;
}
/**
@@ -381,9 +387,11 @@ export function BaseSettingsDialog({
if (currentItem.type === 'boolean' || currentItem.type === 'enum') {
onItemToggle(currentItem.key, currentItem);
} else {
// Start editing for string/number
// Start editing for string/number/array/object
const rawVal = currentItem.rawValue;
const initialValue = rawVal !== undefined ? String(rawVal) : '';
const initialValue =
currentItem.editValue ??
(rawVal !== undefined ? String(rawVal) : '');
startEditing(currentItem.key, initialValue);
}
return true;
@@ -24,7 +24,7 @@ import { useRegistrySearch } from '../../hooks/useRegistrySearch.js';
import { useUIState } from '../../contexts/UIStateContext.js';
interface ExtensionRegistryViewProps {
export interface ExtensionRegistryViewProps {
onSelect?: (extension: RegistryExtension) => void;
onClose?: () => void;
extensionManager: ExtensionManager;
@@ -1,126 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
interface HooksListProps {
hooks: ReadonlyArray<{
config: {
command?: string;
type: string;
name?: string;
description?: string;
timeout?: number;
};
source: string;
eventName: string;
matcher?: string;
sequential?: boolean;
enabled: boolean;
}>;
}
export const HooksList: React.FC<HooksListProps> = ({ hooks }) => {
if (hooks.length === 0) {
return (
<Box flexDirection="column" marginBottom={1}>
<Text>No hooks configured.</Text>
</Box>
);
}
// Group hooks by event name for better organization
const hooksByEvent = hooks.reduce(
(acc, hook) => {
if (!acc[hook.eventName]) {
acc[hook.eventName] = [];
}
acc[hook.eventName].push(hook);
return acc;
},
{} as Record<string, Array<(typeof hooks)[number]>>,
);
return (
<Box flexDirection="column" marginBottom={1}>
<Box flexDirection="column">
<Text color={theme.status.warning} bold underline>
Security Warning:
</Text>
<Text color={theme.status.warning}>
Hooks can execute arbitrary commands on your system. Only use hooks
from sources you trust. Review hook scripts carefully.
</Text>
</Box>
<Box marginTop={1}>
<Text>
Learn more:{' '}
<Text color={theme.text.link}>https://geminicli.com/docs/hooks</Text>
</Text>
</Box>
<Box marginTop={1}>
<Text bold>Configured Hooks:</Text>
</Box>
<Box flexDirection="column" paddingLeft={2} marginTop={1}>
{Object.entries(hooksByEvent).map(([eventName, eventHooks]) => (
<Box key={eventName} flexDirection="column" marginBottom={1}>
<Text color={theme.text.accent} bold>
{eventName}:
</Text>
<Box flexDirection="column" paddingLeft={2}>
{eventHooks.map((hook, index) => {
const hookName =
hook.config.name || hook.config.command || 'unknown';
const statusColor = hook.enabled
? theme.status.success
: theme.text.secondary;
const statusText = hook.enabled ? 'enabled' : 'disabled';
return (
<Box key={`${eventName}-${index}`} flexDirection="column">
<Box>
<Text>
<Text color={theme.text.accent}>{hookName}</Text>
<Text color={statusColor}>{` [${statusText}]`}</Text>
</Text>
</Box>
<Box paddingLeft={2} flexDirection="column">
{hook.config.description && (
<Text italic>{hook.config.description}</Text>
)}
<Text dimColor>
Source: {hook.source}
{hook.config.name &&
hook.config.command &&
` | Command: ${hook.config.command}`}
{hook.matcher && ` | Matcher: ${hook.matcher}`}
{hook.sequential && ` | Sequential`}
{hook.config.timeout &&
` | Timeout: ${hook.config.timeout}s`}
</Text>
</Box>
</Box>
);
})}
</Box>
</Box>
))}
</Box>
<Box marginTop={1}>
<Text dimColor>
Tip: Use <Text bold>/hooks enable {'<hook-name>'}</Text> or{' '}
<Text bold>/hooks disable {'<hook-name>'}</Text> to toggle individual
hooks. Use <Text bold>/hooks enable-all</Text> or{' '}
<Text bold>/hooks disable-all</Text> to toggle all hooks at once.
</Text>
</Box>
</Box>
);
};
@@ -12,6 +12,7 @@ import type {
SettingsFile,
} from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js';
import { checkExhaustive } from '@google/gemini-cli-core';
export const SettingsContext = React.createContext<LoadedSettings | undefined>(
undefined,
@@ -66,7 +67,7 @@ export const useSettingsStore = (): SettingsStoreValue => {
case SettingScope.SystemDefaults:
return snapshot.systemDefaults;
default:
throw new Error(`Invalid scope: ${scope}`);
checkExhaustive(scope);
}
},
}),
@@ -107,8 +107,6 @@ export interface UIState {
history: HistoryItem[];
historyManager: UseHistoryManagerReturn;
isThemeDialogOpen: boolean;
shouldShowRetentionWarning: boolean;
sessionsToDeleteCount: number;
themeError: string | null;
isAuthenticating: boolean;
isConfigInitialized: boolean;
@@ -4,15 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import type { LoadedSettings } from '../../config/settings.js';
import { createContext, useCallback, useContext, useState } from 'react';
import { SettingScope } from '../../config/settings.js';
import { useSettingsStore } from './SettingsContext.js';
export type VimMode = 'NORMAL' | 'INSERT';
@@ -27,35 +21,22 @@ const VimModeContext = createContext<VimModeContextType | undefined>(undefined);
export const VimModeProvider = ({
children,
settings,
}: {
children: React.ReactNode;
settings: LoadedSettings;
}) => {
const initialVimEnabled = settings.merged.general.vimMode;
const [vimEnabled, setVimEnabled] = useState(initialVimEnabled);
const { settings, setSetting } = useSettingsStore();
const vimEnabled = settings.merged.general.vimMode;
const [vimMode, setVimMode] = useState<VimMode>('INSERT');
useEffect(() => {
// Initialize vimEnabled from settings on mount
const enabled = settings.merged.general.vimMode;
setVimEnabled(enabled);
// When vim mode is enabled, start in INSERT mode
if (enabled) {
setVimMode('INSERT');
}
}, [settings.merged.general.vimMode]);
const toggleVimEnabled = useCallback(async () => {
const newValue = !vimEnabled;
setVimEnabled(newValue);
// When enabling vim mode, start in INSERT mode
if (newValue) {
setVimMode('INSERT');
}
settings.setValue(SettingScope.User, 'general.vimMode', newValue);
setSetting(SettingScope.User, 'general.vimMode', newValue);
return newValue;
}, [vimEnabled, settings]);
}, [vimEnabled, setSetting]);
const value = {
vimEnabled,
+1
View File
@@ -48,6 +48,7 @@ export function mapToDisplay(
const baseDisplayProperties = {
callId: call.request.callId,
parentCallId: call.request.parentCallId,
name: displayName,
description,
renderOutputAsMarkdown,
@@ -1,217 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { useSessionRetentionCheck } from './useSessionRetentionCheck.js';
import { type Config } from '@google/gemini-cli-core';
import type { Settings } from '../../config/settingsSchema.js';
import { waitFor } from '../../test-utils/async.js';
// Mock utils
const mockGetAllSessionFiles = vi.fn();
const mockIdentifySessionsToDelete = vi.fn();
vi.mock('../../utils/sessionUtils.js', () => ({
getAllSessionFiles: () => mockGetAllSessionFiles(),
}));
vi.mock('../../utils/sessionCleanup.js', () => ({
identifySessionsToDelete: () => mockIdentifySessionsToDelete(),
DEFAULT_MIN_RETENTION: '30d',
}));
describe('useSessionRetentionCheck', () => {
const mockConfig = {
storage: {
getProjectTempDir: () => '/mock/project/temp/dir',
},
getSessionId: () => 'mock-session-id',
} as unknown as Config;
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should show warning if enabled is true but maxAge is undefined', async () => {
const settings = {
general: {
sessionRetention: {
enabled: true,
maxAge: undefined,
warningAcknowledged: false,
},
},
} as unknown as Settings;
mockGetAllSessionFiles.mockResolvedValue(['session1.json']);
mockIdentifySessionsToDelete.mockResolvedValue(['session1.json']);
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(true);
expect(mockGetAllSessionFiles).toHaveBeenCalled();
expect(mockIdentifySessionsToDelete).toHaveBeenCalled();
});
});
it('should not show warning if warningAcknowledged is true', async () => {
const settings = {
general: {
sessionRetention: {
warningAcknowledged: true,
},
},
} as unknown as Settings;
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(false);
expect(mockGetAllSessionFiles).not.toHaveBeenCalled();
expect(mockIdentifySessionsToDelete).not.toHaveBeenCalled();
});
});
it('should not show warning if retention is already enabled', async () => {
const settings = {
general: {
sessionRetention: {
enabled: true,
maxAge: '30d', // Explicitly enabled with non-default
},
},
} as unknown as Settings;
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(false);
expect(mockGetAllSessionFiles).not.toHaveBeenCalled();
expect(mockIdentifySessionsToDelete).not.toHaveBeenCalled();
});
});
it('should show warning if sessions to delete exist', async () => {
const settings = {
general: {
sessionRetention: {
enabled: false,
warningAcknowledged: false,
},
},
} as unknown as Settings;
mockGetAllSessionFiles.mockResolvedValue([
'session1.json',
'session2.json',
]);
mockIdentifySessionsToDelete.mockResolvedValue(['session1.json']); // 1 session to delete
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(true);
expect(result.current.sessionsToDeleteCount).toBe(1);
expect(mockGetAllSessionFiles).toHaveBeenCalled();
expect(mockIdentifySessionsToDelete).toHaveBeenCalled();
});
});
it('should call onAutoEnable if no sessions to delete and currently disabled', async () => {
const settings = {
general: {
sessionRetention: {
enabled: false,
warningAcknowledged: false,
},
},
} as unknown as Settings;
mockGetAllSessionFiles.mockResolvedValue(['session1.json']);
mockIdentifySessionsToDelete.mockResolvedValue([]); // 0 sessions to delete
const onAutoEnable = vi.fn();
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings, onAutoEnable),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(false);
expect(onAutoEnable).toHaveBeenCalled();
});
});
it('should not show warning if no sessions to delete', async () => {
const settings = {
general: {
sessionRetention: {
enabled: false,
warningAcknowledged: false,
},
},
} as unknown as Settings;
mockGetAllSessionFiles.mockResolvedValue([
'session1.json',
'session2.json',
]);
mockIdentifySessionsToDelete.mockResolvedValue([]); // 0 sessions to delete
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(false);
expect(result.current.sessionsToDeleteCount).toBe(0);
expect(mockGetAllSessionFiles).toHaveBeenCalled();
expect(mockIdentifySessionsToDelete).toHaveBeenCalled();
});
});
it('should handle errors gracefully (assume no warning)', async () => {
const settings = {
general: {
sessionRetention: {
enabled: false,
warningAcknowledged: false,
},
},
} as unknown as Settings;
mockGetAllSessionFiles.mockRejectedValue(new Error('FS Error'));
const { result } = renderHook(() =>
useSessionRetentionCheck(mockConfig, settings),
);
await waitFor(() => {
expect(result.current.checkComplete).toBe(true);
expect(result.current.shouldShowWarning).toBe(false);
});
});
});
@@ -1,70 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect } from 'react';
import { type Config } from '@google/gemini-cli-core';
import { type Settings } from '../../config/settings.js';
import { getAllSessionFiles } from '../../utils/sessionUtils.js';
import { identifySessionsToDelete } from '../../utils/sessionCleanup.js';
import path from 'node:path';
export function useSessionRetentionCheck(
config: Config,
settings: Settings,
onAutoEnable?: () => void,
) {
const [shouldShowWarning, setShouldShowWarning] = useState(false);
const [sessionsToDeleteCount, setSessionsToDeleteCount] = useState(0);
const [checkComplete, setCheckComplete] = useState(false);
useEffect(() => {
// If warning already acknowledged or retention already enabled, skip check
if (
settings.general?.sessionRetention?.warningAcknowledged ||
(settings.general?.sessionRetention?.enabled &&
settings.general?.sessionRetention?.maxAge !== undefined)
) {
setShouldShowWarning(false);
setCheckComplete(true);
return;
}
const checkSessions = async () => {
try {
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
const allFiles = await getAllSessionFiles(
chatsDir,
config.getSessionId(),
);
// Calculate how many sessions would be deleted if we applied a 30-day retention
const sessionsToDelete = await identifySessionsToDelete(allFiles, {
enabled: true,
maxAge: '30d',
});
if (sessionsToDelete.length > 0) {
setSessionsToDeleteCount(sessionsToDelete.length);
setShouldShowWarning(true);
} else {
setShouldShowWarning(false);
// If no sessions to delete, safe to auto-enable retention
onAutoEnable?.();
}
} catch {
// If we can't check sessions, default to not showing the warning to be safe
setShouldShowWarning(false);
} finally {
setCheckComplete(true);
}
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
checkSessions();
}, [config, settings.general?.sessionRetention, onAutoEnable]);
return { shouldShowWarning, checkComplete, sessionsToDeleteCount };
}
@@ -196,6 +196,36 @@ describe('useTerminalTheme', () => {
expect(mockHandleThemeSelect).not.toHaveBeenCalled();
});
it('should switch theme even if terminal background report is identical to previousColor if current theme is mismatched', () => {
// Background is dark at startup
config.setTerminalBackground('#000000');
vi.mocked(config.setTerminalBackground).mockClear();
// But theme is light
mockSettings.merged.ui.theme = 'default-light';
const refreshStatic = vi.fn();
const { unmount } = renderHook(() =>
useTerminalTheme(mockHandleThemeSelect, config, refreshStatic),
);
const handler = mockSubscribe.mock.calls[0][0];
// Terminal reports the same dark background
handler('rgb:0000/0000/0000');
expect(config.setTerminalBackground).not.toHaveBeenCalled();
expect(themeManager.setTerminalBackground).not.toHaveBeenCalled();
expect(refreshStatic).not.toHaveBeenCalled();
// But it SHOULD select the dark theme because of the mismatch!
expect(mockHandleThemeSelect).toHaveBeenCalledWith(
'default',
expect.anything(),
);
mockSettings.merged.ui.theme = 'default';
unmount();
});
it('should not switch theme if autoThemeSwitching is disabled', () => {
mockSettings.merged.ui.autoThemeSwitching = false;
const { unmount } = renderHook(() =>
+10 -8
View File
@@ -59,14 +59,6 @@ export function useTerminalTheme(
if (!hexColor) return;
const previousColor = config.getTerminalBackground();
if (previousColor === hexColor) {
return;
}
config.setTerminalBackground(hexColor);
themeManager.setTerminalBackground(hexColor);
const luminance = getLuminance(hexColor);
const currentThemeName = settings.merged.ui.theme;
@@ -77,6 +69,16 @@ export function useTerminalTheme(
DefaultLight.name,
);
if (previousColor === hexColor) {
if (newTheme) {
void handleThemeSelect(newTheme, SettingScope.User);
}
return;
}
config.setTerminalBackground(hexColor);
themeManager.setTerminalBackground(hexColor);
if (newTheme) {
void handleThemeSelect(newTheme, SettingScope.User);
} else {
@@ -20,6 +20,7 @@ import {
type AnyToolInvocation,
ROOT_SCHEDULER_ID,
CoreToolCallStatus,
type WaitingToolCall,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
@@ -32,6 +33,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
Scheduler: vi.fn().mockImplementation(() => ({
schedule: vi.fn().mockResolvedValue([]),
cancelAll: vi.fn(),
dispose: vi.fn(),
})),
};
});
@@ -341,7 +343,9 @@ describe('useToolScheduler', () => {
const callSub = {
...callRoot,
request: { ...callRoot.request, callId: 'call-sub' },
status: CoreToolCallStatus.AwaitingApproval as const, // Must be awaiting approval to be tracked
schedulerId: 'subagent-1',
confirmationDetails: { type: 'info', title: 'Confirm', prompt: 'Yes?' },
};
// 1. Populate state with multiple schedulers
@@ -360,9 +364,13 @@ describe('useToolScheduler', () => {
});
const [toolCalls] = result.current;
expect(toolCalls).toHaveLength(1);
expect(toolCalls[0].request.callId).toBe('call-root');
expect(toolCalls[0].schedulerId).toBe(ROOT_SCHEDULER_ID);
expect(toolCalls).toHaveLength(2);
expect(
toolCalls.find((t) => t.request.callId === 'call-root'),
).toBeDefined();
expect(
toolCalls.find((t) => t.request.callId === 'call-sub'),
).toBeDefined();
// 2. Call setToolCallsForDisplay (e.g., simulate a manual update or clear)
act(() => {
@@ -374,12 +382,11 @@ describe('useToolScheduler', () => {
// 3. Verify that tools are still present and maintain their scheduler IDs
const [toolCalls2] = result.current;
expect(toolCalls2).toHaveLength(1);
expect(toolCalls2[0].responseSubmittedToGemini).toBe(true);
expect(toolCalls2[0].schedulerId).toBe(ROOT_SCHEDULER_ID);
expect(toolCalls2).toHaveLength(2);
expect(toolCalls2.every((t) => t.responseSubmittedToGemini)).toBe(true);
});
it('ignores TOOL_CALLS_UPDATE from non-root schedulers', () => {
it('ignores TOOL_CALLS_UPDATE from non-root schedulers when no tools await approval', () => {
const { result } = renderHook(() =>
useToolScheduler(
vi.fn().mockResolvedValue(undefined),
@@ -410,8 +417,125 @@ describe('useToolScheduler', () => {
} as ToolCallsUpdateMessage);
});
expect(result.current[0]).toHaveLength(0);
});
it('allows TOOL_CALLS_UPDATE from non-root schedulers when tools are awaiting approval', () => {
const { result } = renderHook(() =>
useToolScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const subagentCall = {
status: CoreToolCallStatus.AwaitingApproval as const,
request: {
callId: 'call-sub',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
schedulerId: 'subagent-1',
confirmationDetails: { type: 'info', title: 'Confirm', prompt: 'Yes?' },
} as WaitingToolCall;
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [subagentCall],
schedulerId: 'subagent-1',
} as ToolCallsUpdateMessage);
});
const [toolCalls] = result.current;
expect(toolCalls).toHaveLength(0);
expect(toolCalls).toHaveLength(1);
expect(toolCalls[0].request.callId).toBe('call-sub');
expect(toolCalls[0].status).toBe(CoreToolCallStatus.AwaitingApproval);
});
it('preserves subagent tools in the UI after they have been approved', () => {
const { result } = renderHook(() =>
useToolScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const subagentCall = {
status: CoreToolCallStatus.AwaitingApproval as const,
request: {
callId: 'call-sub',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
schedulerId: 'subagent-1',
confirmationDetails: { type: 'info', title: 'Confirm', prompt: 'Yes?' },
} as WaitingToolCall;
// 1. Initial approval request
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [subagentCall],
schedulerId: 'subagent-1',
} as ToolCallsUpdateMessage);
});
expect(result.current[0]).toHaveLength(1);
// 2. Approved and executing
const approvedCall = {
...subagentCall,
status: CoreToolCallStatus.Executing as const,
} as unknown as ExecutingToolCall;
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [approvedCall],
schedulerId: 'subagent-1',
} as ToolCallsUpdateMessage);
});
expect(result.current[0]).toHaveLength(1);
expect(result.current[0][0].status).toBe(CoreToolCallStatus.Executing);
// 3. New turn with a background tool (should NOT be shown)
const backgroundTool = {
status: CoreToolCallStatus.Executing as const,
request: {
callId: 'call-background',
name: 'read_file',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
schedulerId: 'subagent-1',
} as ExecutingToolCall;
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [backgroundTool],
schedulerId: 'subagent-1',
} as ToolCallsUpdateMessage);
});
// The subagent list should now be empty because the previously approved tool
// is gone from the current list, and the new tool doesn't need approval.
expect(result.current[0]).toHaveLength(0);
});
it('adapts success/error status to executing when a tail call is present', () => {
+36 -17
View File
@@ -115,11 +115,42 @@ export function useToolScheduler(
useEffect(() => {
const handler = (event: ToolCallsUpdateMessage) => {
// Only process updates for the root scheduler.
// Subagent internal tools should not be displayed in the main tool list.
if (event.schedulerId !== ROOT_SCHEDULER_ID) {
return;
}
const isRoot = event.schedulerId === ROOT_SCHEDULER_ID;
setToolCallsMap((prev) => {
const prevCalls = prev[event.schedulerId] ?? [];
const prevCallIds = new Set(prevCalls.map((tc) => tc.request.callId));
// For non-root schedulers, we only show tool calls that:
// 1. Are currently awaiting approval.
// 2. Were previously shown (e.g., they are now executing or completed).
// This prevents "thinking" tools (reads/searches) from flickering in the UI
// unless they specifically required user interaction.
const filteredToolCalls = isRoot
? event.toolCalls
: event.toolCalls.filter(
(tc) =>
tc.status === CoreToolCallStatus.AwaitingApproval ||
prevCallIds.has(tc.request.callId),
);
// If this is a subagent and we have no tools to show and weren't showing any,
// we can skip the update entirely to avoid unnecessary re-renders.
if (
!isRoot &&
filteredToolCalls.length === 0 &&
prevCalls.length === 0
) {
return prev;
}
const adapted = internalAdaptToolCalls(filteredToolCalls, prevCalls);
return {
...prev,
[event.schedulerId]: adapted,
};
});
// Update output timer for UI spinners (Side Effect)
const hasExecuting = event.toolCalls.some(
@@ -134,18 +165,6 @@ export function useToolScheduler(
if (hasExecuting) {
setLastToolOutputTime(Date.now());
}
setToolCallsMap((prev) => {
const adapted = internalAdaptToolCalls(
event.toolCalls,
prev[event.schedulerId] ?? [],
);
return {
...prev,
[event.schedulerId]: adapted,
};
});
};
messageBus.subscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
@@ -56,7 +56,8 @@ const validCustomTheme: CustomTheme = {
describe('ThemeManager', () => {
beforeEach(() => {
// Reset themeManager state
// Reset themeManager state and inject mocks
themeManager.reinitialize({ fs, homedir: os.homedir });
themeManager.loadCustomThemes({});
themeManager.setActiveTheme(DEFAULT_THEME.name);
themeManager.setTerminalBackground(undefined);
+47 -7
View File
@@ -61,7 +61,13 @@ class ThemeManager {
private cachedSemanticColors: SemanticColors | undefined;
private lastCacheKey: string | undefined;
constructor() {
private fs: typeof fs;
private homedir: () => string;
constructor(dependencies?: { fs?: typeof fs; homedir?: () => string }) {
this.fs = dependencies?.fs ?? fs;
this.homedir = dependencies?.homedir ?? homedir;
this.availableThemes = [
AyuDark,
AyuLight,
@@ -242,10 +248,44 @@ class ThemeManager {
}
/**
* Sets the active theme.
* @param themeName The name of the theme to set as active.
* @returns True if the theme was successfully set, false otherwise.
* Clears all themes loaded from files.
* This is primarily for testing purposes to reset state between tests.
*/
clearFileThemes(): void {
this.fileThemes.clear();
}
/**
* Re-initializes the ThemeManager with new dependencies.
* This is primarily for testing to allow injecting mocks.
*/
reinitialize(dependencies: { fs?: typeof fs; homedir?: () => string }): void {
if (dependencies.fs) {
this.fs = dependencies.fs;
}
if (dependencies.homedir) {
this.homedir = dependencies.homedir;
}
}
/**
* Resets the ThemeManager state to defaults.
* This is for testing purposes to ensure test isolation.
*/
resetForTesting(dependencies?: {
fs?: typeof fs;
homedir?: () => string;
}): void {
if (dependencies) {
this.reinitialize(dependencies);
}
this.settingsThemes.clear();
this.extensionThemes.clear();
this.fileThemes.clear();
this.activeTheme = DEFAULT_THEME;
this.terminalBackground = undefined;
this.clearCache();
}
setActiveTheme(themeName: string | undefined): boolean {
const theme = this.findThemeByName(themeName);
if (!theme) {
@@ -505,7 +545,7 @@ class ThemeManager {
private loadThemeFromFile(themePath: string): Theme | undefined {
try {
// realpathSync resolves the path and throws if it doesn't exist.
const canonicalPath = fs.realpathSync(path.resolve(themePath));
const canonicalPath = this.fs.realpathSync(path.resolve(themePath));
// 1. Check cache using the canonical path.
if (this.fileThemes.has(canonicalPath)) {
@@ -513,7 +553,7 @@ class ThemeManager {
}
// 2. Perform security check.
const homeDir = path.resolve(homedir());
const homeDir = path.resolve(this.homedir());
if (!canonicalPath.startsWith(homeDir)) {
debugLogger.warn(
`Theme file at "${themePath}" is outside your home directory. ` +
@@ -523,7 +563,7 @@ class ThemeManager {
}
// 3. Read, parse, and validate the theme file.
const themeContent = fs.readFileSync(canonicalPath, 'utf-8');
const themeContent = this.fs.readFileSync(canonicalPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const customThemeConfig = JSON.parse(themeContent) as CustomTheme;
+2 -15
View File
@@ -98,6 +98,7 @@ export interface ToolCallEvent {
export interface IndividualToolCallDisplay {
callId: string;
parentCallId?: string;
name: string;
description: string;
resultDisplay: ToolResultDisplay | undefined;
@@ -348,18 +349,6 @@ export type HistoryItemMcpStatus = HistoryItemBase & {
showSchema: boolean;
};
export type HistoryItemHooksList = HistoryItemBase & {
type: 'hooks_list';
hooks: Array<{
config: { command?: string; type: string; timeout?: number };
source: string;
eventName: string;
matcher?: string;
sequential?: boolean;
enabled: boolean;
}>;
};
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
// 'tools' in historyItem.
@@ -388,8 +377,7 @@ export type HistoryItemWithoutId =
| HistoryItemMcpStatus
| HistoryItemChatList
| HistoryItemThinking
| HistoryItemHint
| HistoryItemHooksList;
| HistoryItemHint;
export type HistoryItem = HistoryItemWithoutId & { id: number };
@@ -413,7 +401,6 @@ export enum MessageType {
AGENTS_LIST = 'agents_list',
MCP_STATUS = 'mcp_status',
CHAT_LIST = 'chat_list',
HOOKS_LIST = 'hooks_list',
HINT = 'hint',
}
@@ -1,123 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="207" viewBox="0 0 920 207">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="275" fill="#000000" />
<rect width="920" height="207" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="19" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="19" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="19" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="19" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="19" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="19" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="19" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="19" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="19" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="36" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="36" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="36" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="36" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="36" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="36" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="36" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="36" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="36" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="36" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="36" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="36" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="36" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="198" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="53" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="53" fill="#6e84d6" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="53" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="53" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="53" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="53" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="53" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="70" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="70" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="70" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="70" fill="#6e84d6" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="72" y="70" fill="#7382d4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="70" fill="#797fd2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="70" fill="#7e7dd0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="70" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="70" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="70" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="87" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="87" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="87" fill="#6e84d6" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="72" y="87" fill="#7382d4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="87" fill="#7e7dd0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="87" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="87" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="87" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="87" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="87" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="87" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="87" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="198" y="87" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="104" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="104" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="104" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="104" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="104" fill="#7e7dd0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="104" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="104" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="104" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="104" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="104" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="104" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="104" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="104" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="104" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="121" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="121" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="121" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="121" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="121" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="121" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="121" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="121" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="121" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="121" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="121" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="121" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="121" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="121" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="138" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="138" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="138" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="138" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="138" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="138" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="138" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="138" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="138" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="138" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#f9e2af" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="189" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="189" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> ⊷ google_web_search </text>
<text x="855" y="189" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="206" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="223" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Searching... </text>
<text x="855" y="223" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#f9e2af" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="19" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Gemini CLI</text>
<text x="180" y="19" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#f9e2af" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="121" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="121" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> ⊷ google_web_search </text>
<text x="855" y="121" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="138" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="155" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Searching... </text>
<text x="855" y="155" fill="#f9e2af" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#f9e2af" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,123 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="207" viewBox="0 0 920 207">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="275" fill="#000000" />
<rect width="920" height="207" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="19" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="19" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="19" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="19" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="19" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="19" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="19" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="19" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="19" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="36" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="36" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="36" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="36" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="36" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="36" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="36" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="36" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="36" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="36" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="36" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="36" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="36" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="198" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="53" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="53" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="53" fill="#6e84d6" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="53" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="53" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="53" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="53" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="53" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="70" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="70" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="70" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="70" fill="#6e84d6" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="72" y="70" fill="#7382d4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="81" y="70" fill="#797fd2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="70" fill="#7e7dd0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="70" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="70" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="70" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="87" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="87" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="87" fill="#6e84d6" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="72" y="87" fill="#7382d4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="87" fill="#7e7dd0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="87" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="87" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="87" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="87" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="87" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="87" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="87" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="198" y="87" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="104" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="104" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="104" fill="#6389da" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="104" fill="#6887d8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="90" y="104" fill="#7e7dd0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="104" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="104" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="104" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="104" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="104" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="104" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="104" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="104" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="104" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="121" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="121" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="121" fill="#588ede" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="121" fill="#5d8cdc" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="99" y="121" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="121" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="121" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="121" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="121" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="121" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="121" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="121" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="121" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="121" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="189" y="121" fill="#bd6986" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="138" fill="#4d93e2" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#5291e0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="108" y="138" fill="#8a78c7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="117" y="138" fill="#8f77c0" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="126" y="138" fill="#9575b8" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="135" y="138" fill="#9b73b1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="144" y="138" fill="#a171aa" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="153" y="138" fill="#a670a3" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="162" y="138" fill="#ac6e9c" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="171" y="138" fill="#b26c95" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="180" y="138" fill="#b86a8d" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#6c7086" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="189" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="189" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> ⊷ run_shell_command </text>
<text x="855" y="189" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="206" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="223" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Running command... </text>
<text x="855" y="223" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#6c7086" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="63" y="19" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> Gemini CLI</text>
<text x="180" y="19" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#6c7086" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="121" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="121" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> ⊷ run_shell_command </text>
<text x="855" y="121" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="155" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Running command... </text>
<text x="855" y="155" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#6c7086" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

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