Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0f32ff7d6 | |||
| 1693afe027 | |||
| d179e3673b | |||
| e4d6a5cc7c | |||
| fc4fdc3cbe | |||
| bdbb996a2e | |||
| 2d05396dd2 | |||
| 7b4a822b0e | |||
| d44615ac2f | |||
| de656f01d7 | |||
| 1d2585dba6 | |||
| 97bc3f28c5 | |||
| 3038fdce2e | |||
| bb060d7a98 | |||
| 9a73aa4072 | |||
| d7d53981f3 | |||
| 19e0b1ff7d | |||
| 4d393f9dca | |||
| 5abc170b08 | |||
| ceb4c5f6a7 | |||
| b6beab9480 | |||
| c2691f44b6 | |||
| 663d9c0537 | |||
| 4863816b81 | |||
| 829c532703 | |||
| c68303c553 | |||
| 7242d71c01 | |||
| 391715c33c | |||
| 8a537d85e9 | |||
| cd7dced951 | |||
| 73c589f9e3 | |||
| e700a9220b | |||
| 4b76fe0061 | |||
| 867dc0fdda | |||
| 7506b00488 | |||
| 34709dc62d | |||
| 8432bcee75 | |||
| a38aaa47fb | |||
| 18e8dd768a | |||
| 45faf4d31b | |||
| 333475c41f | |||
| 41d4f59f5e | |||
| 10ab958378 | |||
| 949e85ca55 | |||
| f090736ebc | |||
| 35bf746e62 | |||
| 3da1563c30 | |||
| 7380424782 | |||
| 90b53f9a82 | |||
| f368e80baf | |||
| 4a6d1fad9d | |||
| 1a7f50661a | |||
| 8bfa5b5054 | |||
| e3b3b71c14 | |||
| 926dddf0bf | |||
| 352bbc36c0 | |||
| 3bf4f885d8 | |||
| 775bcbf3a6 | |||
| be16caece2 | |||
| e802776c96 | |||
| 45a4a7054e | |||
| b7578eba7d | |||
| b87718d1ff | |||
| 067e09a40b | |||
| 36ce2ba96e | |||
| 08e174a05c | |||
| df8b399bb4 | |||
| 41f1ea4672 | |||
| 92e0da3ecb | |||
| 6cc2f8d06e | |||
| 58557ba786 | |||
| 016d5d8eb6 | |||
| 6900fe5527 | |||
| 7e9e196793 | |||
| 50384ab3c9 | |||
| eaf6e8bbb1 | |||
| b804fe9662 | |||
| 99bbbc2170 | |||
| 3e3d04877b | |||
| 88638c14f7 | |||
| c2d38bac54 | |||
| f16f13fba8 | |||
| e54c450bc1 | |||
| 075e0b1a81 | |||
| f8ad3a200a | |||
| 8b09ccc288 | |||
| 5020d8fa57 | |||
| 3a0c9a0d39 | |||
| 20a226a5ab | |||
| daf3701194 | |||
| 657f19c1f3 | |||
| 524679d23c | |||
| 7c4570339e | |||
| e22d9917b7 | |||
| d63053cb59 | |||
| a9500d6a6c | |||
| acbd914d5e | |||
| 300929a32b | |||
| 519c75f410 | |||
| 3ff68a9e55 | |||
| 5b8ad9cd65 | |||
| 5caa192cfc | |||
| 9eae91a489 | |||
| 1b69637032 | |||
| e5615f47c4 | |||
| bc75a61982 | |||
| 5d213764fb | |||
| be67470432 | |||
| 00a39b3da9 | |||
| 077c1a1e2d | |||
| 0d60d68cf9 | |||
| b404fc02e7 | |||
| b00d7c88ad | |||
| 2dd037682c | |||
| 7aae5435fa | |||
| 13f78bd9eb | |||
| 4da0366eed | |||
| 18112c474e | |||
| 80cf2fe444 | |||
| 0b78de9601 | |||
| a220874281 | |||
| 49ea9b0457 | |||
| 556825f81c | |||
| b158c96465 | |||
| e91f86c248 | |||
| 47e4f6b13f | |||
| 94ab449e65 | |||
| 0486a1675a | |||
| f9fc83089c | |||
| 5dd2dab189 | |||
| a6b95897ad | |||
| 6ae6c810ba | |||
| 02d4451e77 | |||
| c25ff94608 | |||
| 14412c3a72 | |||
| ec7773eb7b | |||
| 4653b126f3 | |||
| 1e1e7e349d | |||
| 43eb74ac59 | |||
| 215f8f3f15 | |||
| b89944c3a3 | |||
| f88488d1f9 | |||
| 1fd42802be |
@@ -107,7 +107,7 @@ Gemini CLI project.
|
||||
set.
|
||||
- **Logging**: Use `debugLogger` for rethrown errors to avoid duplicate logging.
|
||||
- **Keyboard Shortcuts**: Define all new keyboard shortcuts in
|
||||
`packages/cli/src/config/keyBindings.ts` and document them in
|
||||
`packages/cli/src/ui/key/keyBindings.ts` and document them in
|
||||
`docs/cli/keyboard-shortcuts.md`. Be careful of keybindings that require the
|
||||
`Meta` key, as only certain meta key shortcuts are supported on Mac. Avoid
|
||||
function keys and shortcuts commonly bound in VSCode.
|
||||
|
||||
@@ -193,7 +193,7 @@ runs:
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
|
||||
- name: '📦 Prepare bundled CLI for npm release'
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
|
||||
@@ -347,6 +347,36 @@ async function run() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove status/need-triage from maintainer-only issues since they
|
||||
// don't need community triage. We always attempt removal rather than
|
||||
// checking the (potentially stale) label snapshot, because the
|
||||
// issue-opened-labeler workflow runs concurrently and may add the
|
||||
// label after our snapshot was taken.
|
||||
if (isDryRun) {
|
||||
console.log(
|
||||
`[DRY RUN] Would remove status/need-triage from ${issueKey}`,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await octokit.rest.issues.removeLabel({
|
||||
owner: issueInfo.owner,
|
||||
repo: issueInfo.repo,
|
||||
issue_number: issueInfo.number,
|
||||
name: 'status/need-triage',
|
||||
});
|
||||
console.log(`Removed status/need-triage from ${issueKey}`);
|
||||
} catch (removeError) {
|
||||
// 404 means the label wasn't present — that's fine.
|
||||
if (removeError.status === 404) {
|
||||
console.log(
|
||||
`status/need-triage not present on ${issueKey}, skipping.`,
|
||||
);
|
||||
} else {
|
||||
throw removeError;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing label for ${issueKey}: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ jobs:
|
||||
This PR contains the auto-generated changelog for the ${{ steps.release_info.outputs.VERSION }} release.
|
||||
|
||||
Please review and merge.
|
||||
|
||||
Related to #18505
|
||||
branch: 'changelog-${{ steps.release_info.outputs.VERSION }}'
|
||||
base: 'main'
|
||||
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
|
||||
|
||||
@@ -120,6 +120,9 @@ jobs:
|
||||
if (recentRuns.length > 0) {
|
||||
core.setOutput('dispatched_run_urls', recentRuns.map(r => r.html_url).join(','));
|
||||
core.setOutput('dispatched_run_ids', recentRuns.map(r => r.id).join(','));
|
||||
|
||||
const markdownLinks = recentRuns.map(r => `- [View dispatched workflow run](${r.html_url})`).join('\n');
|
||||
core.setOutput('dispatched_run_links', markdownLinks);
|
||||
}
|
||||
|
||||
- name: 'Comment on Failure'
|
||||
@@ -138,16 +141,19 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
✅ **Patch workflow(s) dispatched successfully!**
|
||||
🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**
|
||||
|
||||
**📋 Details:**
|
||||
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
|
||||
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
|
||||
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
|
||||
|
||||
**⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the specific workflow links below and approve the runs.
|
||||
|
||||
**🔗 Track Progress:**
|
||||
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
${{ steps.dispatch_patch.outputs.dispatched_run_links }}
|
||||
- [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This trigger workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
- name: 'Final Status Comment - Dispatch Success (No URL)'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && !steps.dispatch_patch.outputs.dispatched_run_urls"
|
||||
@@ -156,16 +162,18 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
✅ **Patch workflow(s) dispatched successfully!**
|
||||
🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**
|
||||
|
||||
**📋 Details:**
|
||||
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
|
||||
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
|
||||
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
|
||||
|
||||
**⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the workflow history link below and approve the runs.
|
||||
|
||||
**🔗 Track Progress:**
|
||||
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
- [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This trigger workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
- name: 'Final Status Comment - Failure'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && (steps.dispatch_patch.outcome == 'failure' || steps.dispatch_patch.outcome == 'cancelled')"
|
||||
@@ -174,7 +182,7 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
❌ **Patch workflow dispatch failed!**
|
||||
❌ **[Step 1/4] Patch workflow dispatch failed!**
|
||||
|
||||
There was an error dispatching the patch creation workflow.
|
||||
|
||||
|
||||
@@ -62,3 +62,5 @@ gemini-debug.log
|
||||
.gemini-clipboard/
|
||||
.eslintcache
|
||||
evals/logs/
|
||||
|
||||
temp_agents/
|
||||
|
||||
@@ -77,6 +77,10 @@ You can run the review tool in two ways:
|
||||
./scripts/review.sh <PR_NUMBER> [model]
|
||||
```
|
||||
|
||||
**Warning:** If you run `scripts/review.sh`, you must have first verified
|
||||
that the code for the PR being reviewed is safe to run and does not contain
|
||||
data exfiltration attacks.
|
||||
|
||||
**Authors are strongly encouraged to run this script on their own PRs**
|
||||
immediately after creation. This allows you to catch and fix simple issues
|
||||
locally before a maintainer performs a full review.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
|
||||
[](https://codewiki.google/github.com/google-gemini/gemini-cli?utm_source=badge&utm_medium=github&utm_campaign=github.com/google-gemini/gemini-cli)
|
||||
|
||||

|
||||

|
||||
|
||||
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
|
||||
into your terminal. It provides lightweight access to Gemini, giving you the
|
||||
@@ -147,7 +147,7 @@ Integrate Gemini CLI directly into your GitHub workflows with
|
||||
|
||||
Choose the authentication method that best fits your needs:
|
||||
|
||||
### Option 1: Login with Google (OAuth login using your Google Account)
|
||||
### Option 1: Sign in with Google (OAuth login using your Google Account)
|
||||
|
||||
**✨ Best for:** Individual developers as well as anyone who has a Gemini Code
|
||||
Assist License. (see
|
||||
@@ -161,7 +161,7 @@ for details)
|
||||
- **No API key management** - just sign in with your Google account
|
||||
- **Automatic updates** to latest models
|
||||
|
||||
#### Start Gemini CLI, then choose _Login with Google_ and follow the browser authentication flow when prompted
|
||||
#### Start Gemini CLI, then choose _Sign in with Google_ and follow the browser authentication flow when prompted
|
||||
|
||||
```bash
|
||||
gemini
|
||||
|
||||
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 120 KiB |
@@ -18,6 +18,25 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.33.0 - 2026-03-11
|
||||
|
||||
- **Agent Architecture Enhancements:** Introduced HTTP authentication for A2A
|
||||
remote agents and authenticated A2A agent card discovery
|
||||
([#20510](https://github.com/google-gemini/gemini-cli/pull/20510) by
|
||||
@SandyTao520, [#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
|
||||
by @SandyTao520).
|
||||
- **Plan Mode Updates:** Expanded Plan Mode with built-in research subagents,
|
||||
annotation support for feedback, and a new `copy` subcommand
|
||||
([#20972](https://github.com/google-gemini/gemini-cli/pull/20972) by @Adib234,
|
||||
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988) by
|
||||
@ruomengz).
|
||||
- **CLI UX & Admin Controls:** Redesigned the header to be compact with an ASCII
|
||||
icon, inverted context window display to show usage, and enabled a 30-day
|
||||
default retention for chat history
|
||||
([#18713](https://github.com/google-gemini/gemini-cli/pull/18713) by
|
||||
@keithguerin, [#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
|
||||
by @skeshive).
|
||||
|
||||
## Announcements: v0.32.0 - 2026-03-03
|
||||
|
||||
- **Generalist Agent:** The generalist agent is now enabled to improve task
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.32.1
|
||||
# Latest stable release: v0.33.1
|
||||
|
||||
Released: March 4, 2026
|
||||
Released: March 12, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,198 +11,224 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including the
|
||||
ability to open and modify plans in an external editor, adaptations for
|
||||
complex tasks with multi-select options, and integration tests for plan mode.
|
||||
- **Agent and Steering Improvements**: The generalist agent has been enabled to
|
||||
enhance task delegation, model steering is now supported directly within the
|
||||
workspace, and contiguous parallel admission is enabled for `Kind.Agent`
|
||||
tools.
|
||||
- **Interactive Shell**: Interactive shell autocompletion has been introduced,
|
||||
significantly enhancing the user experience.
|
||||
- **Core Stability and Performance**: Extensions are now loaded in parallel,
|
||||
fetch timeouts have been increased, robust A2A streaming reassembly was
|
||||
implemented, and orphaned processes when terminal closes have been prevented.
|
||||
- **Billing and Quota Handling**: Implemented G1 AI credits overage flow with
|
||||
billing telemetry and added support for quota error fallbacks across all
|
||||
authentication types.
|
||||
- **Agent Architecture Enhancements:** Introduced HTTP authentication support
|
||||
for A2A remote agents, authenticated A2A agent card discovery, and directly
|
||||
indicated auth-required states.
|
||||
- **Plan Mode Updates:** Expanded Plan Mode capabilities with built-in research
|
||||
subagents, annotation support for feedback during iteration, and a new `copy`
|
||||
subcommand.
|
||||
- **CLI UX Improvements:** Redesigned the header to be compact with an ASCII
|
||||
icon, inverted the context window display to show usage, and allowed sub-agent
|
||||
confirmation requests in the UI while preventing background flicker.
|
||||
- **ACP & MCP Integrations:** Implemented slash command handling in ACP for
|
||||
`/memory`, `/init`, `/extensions`, and `/restore`, added an MCPOAuthProvider,
|
||||
and introduced a `set models` interface for ACP.
|
||||
- **Admin & Core Stability:** Enabled a 30-day default retention for chat
|
||||
history, added tool name validation in TOML policy files, and improved tool
|
||||
parameter extraction.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 0659ad1 to release/v0.32.0-pr-21042 to patch version
|
||||
v0.32.0 and create version 0.32.1 by @gemini-cli-robot in
|
||||
[#21048](https://github.com/google-gemini/gemini-cli/pull/21048)
|
||||
- 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
|
||||
- fix(patch): cherry-pick 8432bce to release/v0.33.0-pr-22069 to patch version
|
||||
v0.33.0 and create version 0.33.1 by @gemini-cli-robot in
|
||||
[#22206](https://github.com/google-gemini/gemini-cli/pull/22206)
|
||||
- Docs: Update model docs to remove Preview Features. by @jkcinouye in
|
||||
[#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
|
||||
- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
|
||||
[#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
|
||||
- docs: add Windows PowerShell equivalents for environments and scripting by
|
||||
@scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
|
||||
- fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
|
||||
[#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
|
||||
- chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
|
||||
in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
|
||||
- fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
|
||||
[#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
|
||||
- chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
|
||||
@gemini-cli-robot in
|
||||
[#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
|
||||
- Changelog for v0.31.0 by @gemini-cli-robot in
|
||||
[#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
|
||||
- fix: use full paths for ACP diff payloads by @JagjeevanAK in
|
||||
[#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
|
||||
- Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
|
||||
[#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
|
||||
- fix: acp/zed race condition between MCP initialisation and prompt by
|
||||
@kartikangiras in
|
||||
[#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
|
||||
- fix(cli): reset themeManager between tests to ensure isolation by
|
||||
@NTaylorMullen in
|
||||
[#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
|
||||
- refactor(core): Extract tool parameter names as constants by @SandyTao520 in
|
||||
[#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
|
||||
- fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
|
||||
mismatches by @sehoon38 in
|
||||
[#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
|
||||
- feat(skills): add github-issue-creator skill by @sehoon38 in
|
||||
[#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
|
||||
- fix(cli): allow sub-agent confirmation requests in UI while preventing
|
||||
background flicker by @abhipatel12 in
|
||||
[#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
|
||||
- Merge User and Agent Card Descriptions #20849 by @adamfweidman in
|
||||
[#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
|
||||
- fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
|
||||
[#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
|
||||
- fix(plan): deflake plan mode integration tests by @Adib234 in
|
||||
[#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
|
||||
- Add /unassign support by @scidomino in
|
||||
[#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
|
||||
- feat(core): implement HTTP authentication support for A2A remote agents by
|
||||
@SandyTao520 in
|
||||
[#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
|
||||
[#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)
|
||||
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
|
||||
- feat(core): centralize read_file limits and update gemini-3 description by
|
||||
@aishaneeshah in
|
||||
[#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
|
||||
- Do not block CI on evals by @gundermanc in
|
||||
[#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
|
||||
- document node limitation for shift+tab by @scidomino in
|
||||
[#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
|
||||
- Add install as an option when extension is selected. by @DavidAPierce in
|
||||
[#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
|
||||
- Update CODEOWNERS for README.md reviewers by @g-samroberts in
|
||||
[#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
|
||||
- feat(core): truncate large MCP tool output by @SandyTao520 in
|
||||
[#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
|
||||
- Subagent activity UX. by @gundermanc in
|
||||
[#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
|
||||
- style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
|
||||
[#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
|
||||
- feat: redesign header to be compact with ASCII icon by @keithguerin in
|
||||
[#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
|
||||
- fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
|
||||
[#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
|
||||
- feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
|
||||
[#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
|
||||
- refactor(cli): fully remove React anti patterns, improve type safety and fix
|
||||
UX oversights in SettingsDialog.tsx by @psinha40898 in
|
||||
[#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
|
||||
- Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
|
||||
@Nayana-Parameswarappa in
|
||||
[#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
|
||||
- feat(core): add tool name validation in TOML policy files by @allenhutchison
|
||||
in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
|
||||
- docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
|
||||
[#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
|
||||
- refactor(core): replace manual syncPlanModeTools with declarative policy rules
|
||||
by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
|
||||
- fix(core): increase default headers timeout to 5 minutes by @gundermanc in
|
||||
[#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
|
||||
- feat(admin): enable 30 day default retention for chat history & remove warning
|
||||
by @skeshive in
|
||||
[#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
|
||||
- feat(plan): support annotating plans with feedback for iteration by @Adib234
|
||||
in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
|
||||
- Add some dos and don'ts to behavioral evals README. by @gundermanc in
|
||||
[#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
|
||||
- fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
|
||||
[#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
|
||||
- fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
|
||||
models by @SandyTao520 in
|
||||
[#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
|
||||
- ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
|
||||
[#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
|
||||
- Build binary by @aswinashok44 in
|
||||
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
|
||||
- Code review fixes as a pr by @jacob314 in
|
||||
[#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
|
||||
- fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
|
||||
[#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
|
||||
- feat(cli): invert context window display to show usage by @keithguerin in
|
||||
[#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
|
||||
- fix(plan): clean up session directories and plans on deletion by @jerop in
|
||||
[#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
|
||||
- fix(core): enforce optionality for API response fields in code_assist by
|
||||
@sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
|
||||
- feat(extensions): add support for plan directory in extension manifest by
|
||||
@mahimashanware in
|
||||
[#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
|
||||
- feat(plan): enable built-in research subagents in plan mode by @Adib234 in
|
||||
[#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
|
||||
- feat(agents): directly indicate auth required state by @adamfweidman in
|
||||
[#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
|
||||
- fix(cli): wait for background auto-update before relaunching by @scidomino in
|
||||
[#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
|
||||
- fix: pre-load @scripts/copy_files.js references from external editor prompts
|
||||
by @kartikangiras in
|
||||
[#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
|
||||
- feat(evals): add behavioral evals for ask_user tool by @Adib234 in
|
||||
[#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
|
||||
- refactor common settings logic for skills,agents by @ishaanxgupta in
|
||||
[#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
|
||||
- Update docs-writer skill with new resource by @g-samroberts in
|
||||
[#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
|
||||
- fix(cli): pin clipboardy to ~5.2.x by @scidomino in
|
||||
[#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
|
||||
- feat: Implement slash command handling in ACP for
|
||||
`/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
|
||||
[#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
|
||||
- Docs/add hooks reference by @AadithyaAle in
|
||||
[#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
|
||||
- feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
|
||||
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
|
||||
- fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
|
||||
in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
|
||||
- Format the quota/limit style guide. by @g-samroberts in
|
||||
[#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
|
||||
- fix(core): send shell output to model on cancel by @devr0306 in
|
||||
[#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
|
||||
- remove hardcoded tiername when missing tier by @sehoon38 in
|
||||
[#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
|
||||
- feat(acp): add set models interface by @skeshive in
|
||||
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
|
||||
- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
|
||||
version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
|
||||
- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
|
||||
version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
|
||||
- fix(patch): cherry-pick 0135b03 to release/v0.33.0-preview.2-pr-21171
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21336](https://github.com/google-gemini/gemini-cli/pull/21336)
|
||||
- fix(patch): cherry-pick 7ec477d to release/v0.33.0-preview.3-pr-21305 to patch
|
||||
version v0.33.0-preview.3 and create version 0.33.0-preview.4 by
|
||||
@gemini-cli-robot in
|
||||
[#21349](https://github.com/google-gemini/gemini-cli/pull/21349)
|
||||
- fix(patch): cherry-pick 931e668 to release/v0.33.0-preview.4-pr-21425
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21478](https://github.com/google-gemini/gemini-cli/pull/21478)
|
||||
- fix(patch): cherry-pick 7837194 to release/v0.33.0-preview.5-pr-21487 to patch
|
||||
version v0.33.0-preview.5 and create version 0.33.0-preview.6 by
|
||||
@gemini-cli-robot in
|
||||
[#21720](https://github.com/google-gemini/gemini-cli/pull/21720)
|
||||
- fix(patch): cherry-pick 4f4431e to release/v0.33.0-preview.7-pr-21750 to patch
|
||||
version v0.33.0-preview.7 and create version 0.33.0-preview.8 by
|
||||
@gemini-cli-robot in
|
||||
[#21782](https://github.com/google-gemini/gemini-cli/pull/21782)
|
||||
- fix(patch): cherry-pick 9a74271 to release/v0.33.0-preview.8-pr-21236
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21788](https://github.com/google-gemini/gemini-cli/pull/21788)
|
||||
- fix(patch): cherry-pick 936f624 to release/v0.33.0-preview.9-pr-21702 to patch
|
||||
version v0.33.0-preview.9 and create version 0.33.0-preview.10 by
|
||||
@gemini-cli-robot in
|
||||
[#21800](https://github.com/google-gemini/gemini-cli/pull/21800)
|
||||
- fix(patch): cherry-pick 35ee2a8 to release/v0.33.0-preview.10-pr-21713 by
|
||||
@gemini-cli-robot in
|
||||
[#21859](https://github.com/google-gemini/gemini-cli/pull/21859)
|
||||
- fix(patch): cherry-pick 5dd2dab to release/v0.33.0-preview.11-pr-21871 by
|
||||
@gemini-cli-robot in
|
||||
[#21876](https://github.com/google-gemini/gemini-cli/pull/21876)
|
||||
- fix(patch): cherry-pick e5615f4 to release/v0.33.0-preview.12-pr-21037 to
|
||||
patch version v0.33.0-preview.12 and create version 0.33.0-preview.13 by
|
||||
@gemini-cli-robot in
|
||||
[#21922](https://github.com/google-gemini/gemini-cli/pull/21922)
|
||||
- fix(patch): cherry-pick 1b69637 to release/v0.33.0-preview.13-pr-21467
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21930](https://github.com/google-gemini/gemini-cli/pull/21930)
|
||||
- fix(patch): cherry-pick 3ff68a9 to release/v0.33.0-preview.14-pr-21884
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21952](https://github.com/google-gemini/gemini-cli/pull/21952)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.31.0...v0.32.1
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.32.1...v0.33.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.33.0-preview.4
|
||||
# Preview release: v0.34.0-preview.1
|
||||
|
||||
Released: March 06, 2026
|
||||
Released: March 12, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -13,189 +13,459 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Plan Mode Enhancements**: Added support for annotating plans with feedback
|
||||
for iteration, enabling built-in research subagents in plan mode, and a new
|
||||
`copy` subcommand.
|
||||
- **Agent and Skill Improvements**: Introduced the new `github-issue-creator`
|
||||
skill, implemented HTTP authentication support for A2A remote agents, and
|
||||
added support for authenticated A2A agent card discovery.
|
||||
- **CLI UX/UI Updates**: Redesigned the header to be compact with an ASCII icon,
|
||||
inverted the context window display to show usage, and directly indicate auth
|
||||
required state for agents.
|
||||
- **Core and ACP Enhancements**: Implemented slash command handling in ACP (for
|
||||
`/memory`, `/init`, `/extensions`, and `/restore`), added a set models
|
||||
interface to ACP, and centralized `read_file` limits while truncating large
|
||||
MCP tool output.
|
||||
- **Plan Mode Enabled by Default:** Plan Mode is now enabled out-of-the-box,
|
||||
providing a structured planning workflow and keeping approved plans during
|
||||
chat compression.
|
||||
- **Sandboxing Enhancements:** Added experimental LXC container sandbox support
|
||||
and native gVisor (`runsc`) sandboxing for improved security and isolation.
|
||||
- **Tracker Visualization and Tools:** Introduced CRUD tools and visualization
|
||||
for trackers, along with task tracker strategy improvements.
|
||||
- **Browser Agent Improvements:** Enhanced the browser agent with progress
|
||||
emission, a new automation overlay, and additional integration tests.
|
||||
- **CLI and UI Updates:** Standardized semantic focus colors, polished shell
|
||||
autocomplete rendering, unified keybinding infrastructure, and added custom
|
||||
footer configuration options.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 7ec477d to release/v0.33.0-preview.3-pr-21305 to patch
|
||||
version v0.33.0-preview.3 and create version 0.33.0-preview.4 by
|
||||
@gemini-cli-robot in
|
||||
[#21349](https://github.com/google-gemini/gemini-cli/pull/21349)
|
||||
- fix(patch): cherry-pick 0135b03 to release/v0.33.0-preview.2-pr-21171
|
||||
- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#21336](https://github.com/google-gemini/gemini-cli/pull/21336)
|
||||
- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
|
||||
version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
|
||||
[#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
|
||||
- feat(cli): add chat resume footer on session quit by @lordshashank in
|
||||
[#20667](https://github.com/google-gemini/gemini-cli/pull/20667)
|
||||
- Support bold and other styles in svg snapshots by @jacob314 in
|
||||
[#20937](https://github.com/google-gemini/gemini-cli/pull/20937)
|
||||
- fix(core): increase A2A agent timeout to 30 minutes by @adamfweidman in
|
||||
[#21028](https://github.com/google-gemini/gemini-cli/pull/21028)
|
||||
- Cleanup old branches. by @jacob314 in
|
||||
[#19354](https://github.com/google-gemini/gemini-cli/pull/19354)
|
||||
- chore(release): bump version to 0.34.0-nightly.20260303.34f0c1538 by
|
||||
@gemini-cli-robot in
|
||||
[#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
|
||||
- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
|
||||
version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
|
||||
[#21034](https://github.com/google-gemini/gemini-cli/pull/21034)
|
||||
- feat(ui): standardize semantic focus colors and enhance history visibility by
|
||||
@keithguerin in
|
||||
[#20745](https://github.com/google-gemini/gemini-cli/pull/20745)
|
||||
- fix: merge duplicate imports in packages/core (3/4) by @Nixxx19 in
|
||||
[#20928](https://github.com/google-gemini/gemini-cli/pull/20928)
|
||||
- Add extra safety checks for proto pollution by @jacob314 in
|
||||
[#20396](https://github.com/google-gemini/gemini-cli/pull/20396)
|
||||
- feat(core): Add tracker CRUD tools & visualization by @anj-s in
|
||||
[#19489](https://github.com/google-gemini/gemini-cli/pull/19489)
|
||||
- Revert "fix(ui): persist expansion in AskUser dialog when navigating options"
|
||||
by @jacob314 in
|
||||
[#21042](https://github.com/google-gemini/gemini-cli/pull/21042)
|
||||
- Changelog for v0.33.0-preview.0 by @gemini-cli-robot in
|
||||
[#21030](https://github.com/google-gemini/gemini-cli/pull/21030)
|
||||
- fix: model persistence for all scenarios by @sripasg in
|
||||
[#21051](https://github.com/google-gemini/gemini-cli/pull/21051)
|
||||
- chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 by
|
||||
@gemini-cli-robot in
|
||||
[#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
|
||||
- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
|
||||
version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
|
||||
- Docs: Update model docs to remove Preview Features. by @jkcinouye in
|
||||
[#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
|
||||
- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
|
||||
[#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
|
||||
- docs: add Windows PowerShell equivalents for environments and scripting by
|
||||
@scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
|
||||
- fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
|
||||
[#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
|
||||
- chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
|
||||
in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
|
||||
- fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
|
||||
[#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
|
||||
- chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
|
||||
@gemini-cli-robot in
|
||||
[#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
|
||||
- Changelog for v0.31.0 by @gemini-cli-robot in
|
||||
[#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
|
||||
- fix: use full paths for ACP diff payloads by @JagjeevanAK in
|
||||
[#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
|
||||
- Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
|
||||
[#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
|
||||
- fix: acp/zed race condition between MCP initialisation and prompt by
|
||||
@kartikangiras in
|
||||
[#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
|
||||
- fix(cli): reset themeManager between tests to ensure isolation by
|
||||
@NTaylorMullen in
|
||||
[#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
|
||||
- refactor(core): Extract tool parameter names as constants by @SandyTao520 in
|
||||
[#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
|
||||
- fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
|
||||
mismatches by @sehoon38 in
|
||||
[#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
|
||||
- feat(skills): add github-issue-creator skill by @sehoon38 in
|
||||
[#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
|
||||
- fix(cli): allow sub-agent confirmation requests in UI while preventing
|
||||
background flicker by @abhipatel12 in
|
||||
[#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
|
||||
- Merge User and Agent Card Descriptions #20849 by @adamfweidman in
|
||||
[#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
|
||||
- fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
|
||||
[#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
|
||||
- fix(plan): deflake plan mode integration tests by @Adib234 in
|
||||
[#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
|
||||
- Add /unassign support by @scidomino in
|
||||
[#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
|
||||
- feat(core): implement HTTP authentication support for A2A remote agents by
|
||||
@SandyTao520 in
|
||||
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
|
||||
- feat(core): centralize read_file limits and update gemini-3 description by
|
||||
[#21054](https://github.com/google-gemini/gemini-cli/pull/21054)
|
||||
- Consistently guard restarts against concurrent auto updates by @scidomino in
|
||||
[#21016](https://github.com/google-gemini/gemini-cli/pull/21016)
|
||||
- Defensive coding to reduce the risk of Maximum update depth errors by
|
||||
@jacob314 in [#20940](https://github.com/google-gemini/gemini-cli/pull/20940)
|
||||
- fix(cli): Polish shell autocomplete rendering to be a little more shell native
|
||||
feeling. by @jacob314 in
|
||||
[#20931](https://github.com/google-gemini/gemini-cli/pull/20931)
|
||||
- Docs: Update plan mode docs by @jkcinouye in
|
||||
[#19682](https://github.com/google-gemini/gemini-cli/pull/19682)
|
||||
- fix(mcp): Notifications/tools/list_changed support not working by @jacob314 in
|
||||
[#21050](https://github.com/google-gemini/gemini-cli/pull/21050)
|
||||
- fix(cli): register extension lifecycle events in DebugProfiler by
|
||||
@fayerman-source in
|
||||
[#20101](https://github.com/google-gemini/gemini-cli/pull/20101)
|
||||
- chore(dev): update vscode settings for typescriptreact by @rohit-4321 in
|
||||
[#19907](https://github.com/google-gemini/gemini-cli/pull/19907)
|
||||
- fix(cli): enable multi-arch docker builds for sandbox by @ru-aish in
|
||||
[#19821](https://github.com/google-gemini/gemini-cli/pull/19821)
|
||||
- Changelog for v0.32.0 by @gemini-cli-robot in
|
||||
[#21033](https://github.com/google-gemini/gemini-cli/pull/21033)
|
||||
- Changelog for v0.33.0-preview.1 by @gemini-cli-robot in
|
||||
[#21058](https://github.com/google-gemini/gemini-cli/pull/21058)
|
||||
- feat(core): improve @scripts/copy_files.js autocomplete to prioritize
|
||||
filenames by @sehoon38 in
|
||||
[#21064](https://github.com/google-gemini/gemini-cli/pull/21064)
|
||||
- feat(sandbox): add experimental LXC container sandbox support by @h30s in
|
||||
[#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
|
||||
- feat(evals): add overall pass rate row to eval nightly summary table by
|
||||
@gundermanc in
|
||||
[#20905](https://github.com/google-gemini/gemini-cli/pull/20905)
|
||||
- feat(telemetry): include language in telemetry and fix accepted lines
|
||||
computation by @gundermanc in
|
||||
[#21126](https://github.com/google-gemini/gemini-cli/pull/21126)
|
||||
- Changelog for v0.32.1 by @gemini-cli-robot in
|
||||
[#21055](https://github.com/google-gemini/gemini-cli/pull/21055)
|
||||
- feat(core): add robustness tests, logging, and metrics for CodeAssistServer
|
||||
SSE parsing by @yunaseoul in
|
||||
[#21013](https://github.com/google-gemini/gemini-cli/pull/21013)
|
||||
- feat: add issue assignee workflow by @kartikangiras in
|
||||
[#21003](https://github.com/google-gemini/gemini-cli/pull/21003)
|
||||
- fix: improve error message when OAuth succeeds but project ID is required by
|
||||
@Nixxx19 in [#21070](https://github.com/google-gemini/gemini-cli/pull/21070)
|
||||
- feat(loop-reduction): implement iterative loop detection and model feedback by
|
||||
@aishaneeshah in
|
||||
[#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
|
||||
- Do not block CI on evals by @gundermanc in
|
||||
[#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
|
||||
- document node limitation for shift+tab by @scidomino in
|
||||
[#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
|
||||
- Add install as an option when extension is selected. by @DavidAPierce in
|
||||
[#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
|
||||
- Update CODEOWNERS for README.md reviewers by @g-samroberts in
|
||||
[#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
|
||||
- feat(core): truncate large MCP tool output by @SandyTao520 in
|
||||
[#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
|
||||
- Subagent activity UX. by @gundermanc in
|
||||
[#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
|
||||
- style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
|
||||
[#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
|
||||
- feat: redesign header to be compact with ASCII icon by @keithguerin in
|
||||
[#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
|
||||
- fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
|
||||
[#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
|
||||
- feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
|
||||
[#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
|
||||
- refactor(cli): fully remove React anti patterns, improve type safety and fix
|
||||
UX oversights in SettingsDialog.tsx by @psinha40898 in
|
||||
[#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
|
||||
- Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
|
||||
@Nayana-Parameswarappa in
|
||||
[#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
|
||||
- feat(core): add tool name validation in TOML policy files by @allenhutchison
|
||||
in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
|
||||
- docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
|
||||
[#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
|
||||
- refactor(core): replace manual syncPlanModeTools with declarative policy rules
|
||||
by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
|
||||
- fix(core): increase default headers timeout to 5 minutes by @gundermanc in
|
||||
[#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
|
||||
- feat(admin): enable 30 day default retention for chat history & remove warning
|
||||
[#20763](https://github.com/google-gemini/gemini-cli/pull/20763)
|
||||
- chore(github): require prompt approvers for agent prompt files by @gundermanc
|
||||
in [#20896](https://github.com/google-gemini/gemini-cli/pull/20896)
|
||||
- Docs: Create tools reference by @jkcinouye in
|
||||
[#19470](https://github.com/google-gemini/gemini-cli/pull/19470)
|
||||
- fix(core, a2a-server): prevent hang during OAuth in non-interactive sessions
|
||||
by @spencer426 in
|
||||
[#21045](https://github.com/google-gemini/gemini-cli/pull/21045)
|
||||
- chore(cli): enable deprecated settings removal by default by @yashodipmore in
|
||||
[#20682](https://github.com/google-gemini/gemini-cli/pull/20682)
|
||||
- feat(core): Disable fast ack helper for hints. by @joshualitt in
|
||||
[#21011](https://github.com/google-gemini/gemini-cli/pull/21011)
|
||||
- fix(ui): suppress redundant failure note when tool error note is shown by
|
||||
@NTaylorMullen in
|
||||
[#21078](https://github.com/google-gemini/gemini-cli/pull/21078)
|
||||
- docs: document planning workflows with Conductor example by @jerop in
|
||||
[#21166](https://github.com/google-gemini/gemini-cli/pull/21166)
|
||||
- feat(release): ship esbuild bundle in npm package by @genneth in
|
||||
[#19171](https://github.com/google-gemini/gemini-cli/pull/19171)
|
||||
- fix(extensions): preserve symlinks in extension source path while enforcing
|
||||
folder trust by @galz10 in
|
||||
[#20867](https://github.com/google-gemini/gemini-cli/pull/20867)
|
||||
- fix(cli): defer tool exclusions to policy engine in non-interactive mode by
|
||||
@EricRahm in [#20639](https://github.com/google-gemini/gemini-cli/pull/20639)
|
||||
- fix(ui): removed double padding on rendered content by @devr0306 in
|
||||
[#21029](https://github.com/google-gemini/gemini-cli/pull/21029)
|
||||
- fix(core): truncate excessively long lines in grep search output by
|
||||
@gundermanc in
|
||||
[#21147](https://github.com/google-gemini/gemini-cli/pull/21147)
|
||||
- feat: add custom footer configuration via `/footer` by @jackwotherspoon in
|
||||
[#19001](https://github.com/google-gemini/gemini-cli/pull/19001)
|
||||
- perf(core): fix OOM crash in long-running sessions by @WizardsForgeGames in
|
||||
[#19608](https://github.com/google-gemini/gemini-cli/pull/19608)
|
||||
- refactor(cli): categorize built-in themes into dark/ and light/ directories by
|
||||
@JayadityaGit in
|
||||
[#18634](https://github.com/google-gemini/gemini-cli/pull/18634)
|
||||
- fix(core): explicitly allow codebase_investigator and cli_help in read-only
|
||||
mode by @Adib234 in
|
||||
[#21157](https://github.com/google-gemini/gemini-cli/pull/21157)
|
||||
- test: add browser agent integration tests by @kunal-10-cloud in
|
||||
[#21151](https://github.com/google-gemini/gemini-cli/pull/21151)
|
||||
- fix(cli): fix enabling kitty codes on Windows Terminal by @scidomino in
|
||||
[#21136](https://github.com/google-gemini/gemini-cli/pull/21136)
|
||||
- refactor(core): extract shared OAuth flow primitives from MCPOAuthProvider by
|
||||
@SandyTao520 in
|
||||
[#20895](https://github.com/google-gemini/gemini-cli/pull/20895)
|
||||
- fix(ui): add partial output to cancelled shell UI by @devr0306 in
|
||||
[#21178](https://github.com/google-gemini/gemini-cli/pull/21178)
|
||||
- fix(cli): replace hardcoded keybinding strings with dynamic formatters by
|
||||
@scidomino in [#21159](https://github.com/google-gemini/gemini-cli/pull/21159)
|
||||
- DOCS: Update quota and pricing page by @g-samroberts in
|
||||
[#21194](https://github.com/google-gemini/gemini-cli/pull/21194)
|
||||
- feat(telemetry): implement Clearcut logging for startup statistics by
|
||||
@yunaseoul in [#21172](https://github.com/google-gemini/gemini-cli/pull/21172)
|
||||
- feat(triage): add area/documentation to issue triage by @g-samroberts in
|
||||
[#21222](https://github.com/google-gemini/gemini-cli/pull/21222)
|
||||
- Fix so shell calls are formatted by @jacob314 in
|
||||
[#21237](https://github.com/google-gemini/gemini-cli/pull/21237)
|
||||
- feat(cli): add native gVisor (runsc) sandboxing support by @Zheyuan-Lin in
|
||||
[#21062](https://github.com/google-gemini/gemini-cli/pull/21062)
|
||||
- docs: use absolute paths for internal links in plan-mode.md by @jerop in
|
||||
[#21299](https://github.com/google-gemini/gemini-cli/pull/21299)
|
||||
- fix(core): prevent unhandled AbortError crash during stream loop detection by
|
||||
@7hokerz in [#21123](https://github.com/google-gemini/gemini-cli/pull/21123)
|
||||
- fix:reorder env var redaction checks to scan values first by @kartikangiras in
|
||||
[#21059](https://github.com/google-gemini/gemini-cli/pull/21059)
|
||||
- fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences
|
||||
by @skeshive in
|
||||
[#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
|
||||
- feat(plan): support annotating plans with feedback for iteration by @Adib234
|
||||
in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
|
||||
- Add some dos and don'ts to behavioral evals README. by @gundermanc in
|
||||
[#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
|
||||
- fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
|
||||
[#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
|
||||
- fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
|
||||
models by @SandyTao520 in
|
||||
[#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
|
||||
- ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
|
||||
[#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
|
||||
- Build binary by @aswinashok44 in
|
||||
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
|
||||
- Code review fixes as a pr by @jacob314 in
|
||||
[#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
|
||||
- fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
|
||||
[#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
|
||||
- feat(cli): invert context window display to show usage by @keithguerin in
|
||||
[#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
|
||||
- fix(plan): clean up session directories and plans on deletion by @jerop in
|
||||
[#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
|
||||
- fix(core): enforce optionality for API response fields in code_assist by
|
||||
@sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
|
||||
- feat(extensions): add support for plan directory in extension manifest by
|
||||
@mahimashanware in
|
||||
[#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
|
||||
- feat(plan): enable built-in research subagents in plan mode by @Adib234 in
|
||||
[#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
|
||||
- feat(agents): directly indicate auth required state by @adamfweidman in
|
||||
[#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
|
||||
- fix(cli): wait for background auto-update before relaunching by @scidomino in
|
||||
[#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
|
||||
- fix: pre-load @scripts/copy_files.js references from external editor prompts
|
||||
by @kartikangiras in
|
||||
[#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
|
||||
- feat(evals): add behavioral evals for ask_user tool by @Adib234 in
|
||||
[#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
|
||||
- refactor common settings logic for skills,agents by @ishaanxgupta in
|
||||
[#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
|
||||
- Update docs-writer skill with new resource by @g-samroberts in
|
||||
[#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
|
||||
- fix(cli): pin clipboardy to ~5.2.x by @scidomino in
|
||||
[#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
|
||||
- feat: Implement slash command handling in ACP for
|
||||
`/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
|
||||
[#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
|
||||
- Docs/add hooks reference by @AadithyaAle in
|
||||
[#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
|
||||
- feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
|
||||
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
|
||||
- fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
|
||||
in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
|
||||
- Format the quota/limit style guide. by @g-samroberts in
|
||||
[#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
|
||||
- fix(core): send shell output to model on cancel by @devr0306 in
|
||||
[#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
|
||||
- remove hardcoded tiername when missing tier by @sehoon38 in
|
||||
[#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
|
||||
- feat(acp): add set models interface by @skeshive in
|
||||
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
|
||||
[#21171](https://github.com/google-gemini/gemini-cli/pull/21171)
|
||||
- feat(core): fallback to 2.5 models with no access for toolcalls by @sehoon38
|
||||
in [#21283](https://github.com/google-gemini/gemini-cli/pull/21283)
|
||||
- test(core): improve testing for API request/response parsing by @sehoon38 in
|
||||
[#21227](https://github.com/google-gemini/gemini-cli/pull/21227)
|
||||
- docs(links): update docs-writer skill and fix broken link by @g-samroberts in
|
||||
[#21314](https://github.com/google-gemini/gemini-cli/pull/21314)
|
||||
- Fix code colorizer ansi escape bug. by @jacob314 in
|
||||
[#21321](https://github.com/google-gemini/gemini-cli/pull/21321)
|
||||
- remove wildcard behavior on keybindings by @scidomino in
|
||||
[#21315](https://github.com/google-gemini/gemini-cli/pull/21315)
|
||||
- feat(acp): Add support for AI Gateway auth by @skeshive in
|
||||
[#21305](https://github.com/google-gemini/gemini-cli/pull/21305)
|
||||
- fix(theme): improve theme color contrast for macOS Terminal.app by @clocky in
|
||||
[#21175](https://github.com/google-gemini/gemini-cli/pull/21175)
|
||||
- feat (core): Implement tracker related SI changes by @anj-s in
|
||||
[#19964](https://github.com/google-gemini/gemini-cli/pull/19964)
|
||||
- Changelog for v0.33.0-preview.2 by @gemini-cli-robot in
|
||||
[#21333](https://github.com/google-gemini/gemini-cli/pull/21333)
|
||||
- Changelog for v0.33.0-preview.3 by @gemini-cli-robot in
|
||||
[#21347](https://github.com/google-gemini/gemini-cli/pull/21347)
|
||||
- docs: format release times as HH:MM UTC by @pavan-sh in
|
||||
[#20726](https://github.com/google-gemini/gemini-cli/pull/20726)
|
||||
- fix(cli): implement --all flag for extensions uninstall by @sehoon38 in
|
||||
[#21319](https://github.com/google-gemini/gemini-cli/pull/21319)
|
||||
- docs: fix incorrect relative links to command reference by @kanywst in
|
||||
[#20964](https://github.com/google-gemini/gemini-cli/pull/20964)
|
||||
- documentiong ensures ripgrep by @Jatin24062005 in
|
||||
[#21298](https://github.com/google-gemini/gemini-cli/pull/21298)
|
||||
- fix(core): handle AbortError thrown during processTurn by @MumuTW in
|
||||
[#21296](https://github.com/google-gemini/gemini-cli/pull/21296)
|
||||
- docs(cli): clarify ! command output visibility in shell commands tutorial by
|
||||
@MohammedADev in
|
||||
[#21041](https://github.com/google-gemini/gemini-cli/pull/21041)
|
||||
- fix: logic for task tracker strategy and remove tracker tools by @anj-s in
|
||||
[#21355](https://github.com/google-gemini/gemini-cli/pull/21355)
|
||||
- fix(partUtils): display media type and size for inline data parts by @Aboudjem
|
||||
in [#21358](https://github.com/google-gemini/gemini-cli/pull/21358)
|
||||
- Fix(accessibility): add screen reader support to RewindViewer by @Famous077 in
|
||||
[#20750](https://github.com/google-gemini/gemini-cli/pull/20750)
|
||||
- fix(hooks): propagate stopHookActive in AfterAgent retry path (#20426) by
|
||||
@Aarchi-07 in [#20439](https://github.com/google-gemini/gemini-cli/pull/20439)
|
||||
- fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive
|
||||
filesystems (#19904) by @Nixxx19 in
|
||||
[#19915](https://github.com/google-gemini/gemini-cli/pull/19915)
|
||||
- feat(core): add concurrency safety guidance for subagent delegation (#17753)
|
||||
by @abhipatel12 in
|
||||
[#21278](https://github.com/google-gemini/gemini-cli/pull/21278)
|
||||
- feat(ui): dynamically generate all keybinding hints by @scidomino in
|
||||
[#21346](https://github.com/google-gemini/gemini-cli/pull/21346)
|
||||
- feat(core): implement unified KeychainService and migrate token storage by
|
||||
@ehedlund in [#21344](https://github.com/google-gemini/gemini-cli/pull/21344)
|
||||
- fix(cli): gracefully handle --resume when no sessions exist by @SandyTao520 in
|
||||
[#21429](https://github.com/google-gemini/gemini-cli/pull/21429)
|
||||
- fix(plan): keep approved plan during chat compression by @ruomengz in
|
||||
[#21284](https://github.com/google-gemini/gemini-cli/pull/21284)
|
||||
- feat(core): implement generic CacheService and optimize setupUser by @sehoon38
|
||||
in [#21374](https://github.com/google-gemini/gemini-cli/pull/21374)
|
||||
- Update quota and pricing documentation with subscription tiers by @srithreepo
|
||||
in [#21351](https://github.com/google-gemini/gemini-cli/pull/21351)
|
||||
- fix(core): append correct OTLP paths for HTTP exporters by
|
||||
@sebastien-prudhomme in
|
||||
[#16836](https://github.com/google-gemini/gemini-cli/pull/16836)
|
||||
- Changelog for v0.33.0-preview.4 by @gemini-cli-robot in
|
||||
[#21354](https://github.com/google-gemini/gemini-cli/pull/21354)
|
||||
- feat(cli): implement dot-prefixing for slash command conflicts by @ehedlund in
|
||||
[#20979](https://github.com/google-gemini/gemini-cli/pull/20979)
|
||||
- refactor(core): standardize MCP tool naming to mcp\_ FQN format by
|
||||
@abhipatel12 in
|
||||
[#21425](https://github.com/google-gemini/gemini-cli/pull/21425)
|
||||
- feat(cli): hide gemma settings from display and mark as experimental by
|
||||
@abhipatel12 in
|
||||
[#21471](https://github.com/google-gemini/gemini-cli/pull/21471)
|
||||
- feat(skills): refine string-reviewer guidelines and description by @clocky in
|
||||
[#20368](https://github.com/google-gemini/gemini-cli/pull/20368)
|
||||
- fix(core): whitelist TERM and COLORTERM in environment sanitization by
|
||||
@deadsmash07 in
|
||||
[#20514](https://github.com/google-gemini/gemini-cli/pull/20514)
|
||||
- fix(billing): fix overage strategy lifecycle and settings integration by
|
||||
@gsquared94 in
|
||||
[#21236](https://github.com/google-gemini/gemini-cli/pull/21236)
|
||||
- fix: expand paste placeholders in TextInput on submit by @Jefftree in
|
||||
[#19946](https://github.com/google-gemini/gemini-cli/pull/19946)
|
||||
- fix(core): add in-memory cache to ChatRecordingService to prevent OOM by
|
||||
@SandyTao520 in
|
||||
[#21502](https://github.com/google-gemini/gemini-cli/pull/21502)
|
||||
- feat(cli): overhaul thinking UI by @keithguerin in
|
||||
[#18725](https://github.com/google-gemini/gemini-cli/pull/18725)
|
||||
- fix(ui): unify Ctrl+O expansion hint experience across buffer modes by
|
||||
@jwhelangoog in
|
||||
[#21474](https://github.com/google-gemini/gemini-cli/pull/21474)
|
||||
- fix(cli): correct shell height reporting by @jacob314 in
|
||||
[#21492](https://github.com/google-gemini/gemini-cli/pull/21492)
|
||||
- Make test suite pass when the GEMINI_SYSTEM_MD env variable or
|
||||
GEMINI_WRITE_SYSTEM_MD variable happens to be set locally/ by @jacob314 in
|
||||
[#21480](https://github.com/google-gemini/gemini-cli/pull/21480)
|
||||
- Disallow underspecified types by @gundermanc in
|
||||
[#21485](https://github.com/google-gemini/gemini-cli/pull/21485)
|
||||
- refactor(cli): standardize on 'reload' verb for all components by @keithguerin
|
||||
in [#20654](https://github.com/google-gemini/gemini-cli/pull/20654)
|
||||
- feat(cli): Invert quota language to 'percent used' by @keithguerin in
|
||||
[#20100](https://github.com/google-gemini/gemini-cli/pull/20100)
|
||||
- Docs: Add documentation for notifications (experimental)(macOS) by @jkcinouye
|
||||
in [#21163](https://github.com/google-gemini/gemini-cli/pull/21163)
|
||||
- Code review comments as a pr by @jacob314 in
|
||||
[#21209](https://github.com/google-gemini/gemini-cli/pull/21209)
|
||||
- feat(cli): unify /chat and /resume command UX by @LyalinDotCom in
|
||||
[#20256](https://github.com/google-gemini/gemini-cli/pull/20256)
|
||||
- docs: fix typo 'allowslisted' -> 'allowlisted' in mcp-server.md by
|
||||
@Gyanranjan-Priyam in
|
||||
[#21665](https://github.com/google-gemini/gemini-cli/pull/21665)
|
||||
- fix(core): display actual graph output in tracker_visualize tool by @anj-s in
|
||||
[#21455](https://github.com/google-gemini/gemini-cli/pull/21455)
|
||||
- fix(core): sanitize SSE-corrupted JSON and domain strings in error
|
||||
classification by @gsquared94 in
|
||||
[#21702](https://github.com/google-gemini/gemini-cli/pull/21702)
|
||||
- Docs: Make documentation links relative by @diodesign in
|
||||
[#21490](https://github.com/google-gemini/gemini-cli/pull/21490)
|
||||
- feat(cli): expose /tools desc as explicit subcommand for discoverability by
|
||||
@aworki in [#21241](https://github.com/google-gemini/gemini-cli/pull/21241)
|
||||
- feat(cli): add /compact alias for /compress command by @jackwotherspoon in
|
||||
[#21711](https://github.com/google-gemini/gemini-cli/pull/21711)
|
||||
- feat(plan): enable Plan Mode by default by @jerop in
|
||||
[#21713](https://github.com/google-gemini/gemini-cli/pull/21713)
|
||||
- feat(core): Introduce `AgentLoopContext`. by @joshualitt in
|
||||
[#21198](https://github.com/google-gemini/gemini-cli/pull/21198)
|
||||
- fix(core): resolve symlinks for non-existent paths during validation by
|
||||
@Adib234 in [#21487](https://github.com/google-gemini/gemini-cli/pull/21487)
|
||||
- docs: document tool exclusion from memory via deny policy by @Abhijit-2592 in
|
||||
[#21428](https://github.com/google-gemini/gemini-cli/pull/21428)
|
||||
- perf(core): cache loadApiKey to reduce redundant keychain access by @sehoon38
|
||||
in [#21520](https://github.com/google-gemini/gemini-cli/pull/21520)
|
||||
- feat(cli): implement /upgrade command by @sehoon38 in
|
||||
[#21511](https://github.com/google-gemini/gemini-cli/pull/21511)
|
||||
- Feat/browser agent progress emission by @kunal-10-cloud in
|
||||
[#21218](https://github.com/google-gemini/gemini-cli/pull/21218)
|
||||
- fix(settings): display objects as JSON instead of [object Object] by
|
||||
@Zheyuan-Lin in
|
||||
[#21458](https://github.com/google-gemini/gemini-cli/pull/21458)
|
||||
- Unmarshall update by @DavidAPierce in
|
||||
[#21721](https://github.com/google-gemini/gemini-cli/pull/21721)
|
||||
- Update mcp's list function to check for disablement. by @DavidAPierce in
|
||||
[#21148](https://github.com/google-gemini/gemini-cli/pull/21148)
|
||||
- robustness(core): static checks to validate history is immutable by @jacob314
|
||||
in [#21228](https://github.com/google-gemini/gemini-cli/pull/21228)
|
||||
- refactor(cli): better react patterns for BaseSettingsDialog by @psinha40898 in
|
||||
[#21206](https://github.com/google-gemini/gemini-cli/pull/21206)
|
||||
- feat(security): implement robust IP validation and safeFetch foundation by
|
||||
@alisa-alisa in
|
||||
[#21401](https://github.com/google-gemini/gemini-cli/pull/21401)
|
||||
- feat(core): improve subagent result display by @joshualitt in
|
||||
[#20378](https://github.com/google-gemini/gemini-cli/pull/20378)
|
||||
- docs: fix broken markdown syntax and anchor links in /tools by @campox747 in
|
||||
[#20902](https://github.com/google-gemini/gemini-cli/pull/20902)
|
||||
- feat(policy): support subagent-specific policies in TOML by @akh64bit in
|
||||
[#21431](https://github.com/google-gemini/gemini-cli/pull/21431)
|
||||
- Add script to speed up reviewing PRs adding a worktree. by @jacob314 in
|
||||
[#21748](https://github.com/google-gemini/gemini-cli/pull/21748)
|
||||
- fix(core): prevent infinite recursion in symlink resolution by @Adib234 in
|
||||
[#21750](https://github.com/google-gemini/gemini-cli/pull/21750)
|
||||
- fix(docs): fix headless mode docs by @ame2en in
|
||||
[#21287](https://github.com/google-gemini/gemini-cli/pull/21287)
|
||||
- feat/redesign header compact by @jacob314 in
|
||||
[#20922](https://github.com/google-gemini/gemini-cli/pull/20922)
|
||||
- refactor: migrate to useKeyMatchers hook by @scidomino in
|
||||
[#21753](https://github.com/google-gemini/gemini-cli/pull/21753)
|
||||
- perf(cli): cache loadSettings to reduce redundant disk I/O at startup by
|
||||
@sehoon38 in [#21521](https://github.com/google-gemini/gemini-cli/pull/21521)
|
||||
- fix(core): resolve Windows line ending and path separation bugs across CLI by
|
||||
@muhammadusman586 in
|
||||
[#21068](https://github.com/google-gemini/gemini-cli/pull/21068)
|
||||
- docs: fix heading formatting in commands.md and phrasing in tools-api.md by
|
||||
@campox747 in [#20679](https://github.com/google-gemini/gemini-cli/pull/20679)
|
||||
- refactor(ui): unify keybinding infrastructure and support string
|
||||
initialization by @scidomino in
|
||||
[#21776](https://github.com/google-gemini/gemini-cli/pull/21776)
|
||||
- Add support for updating extension sources and names by @chrstnb in
|
||||
[#21715](https://github.com/google-gemini/gemini-cli/pull/21715)
|
||||
- fix(core): handle GUI editor non-zero exit codes gracefully by @reyyanxahmed
|
||||
in [#20376](https://github.com/google-gemini/gemini-cli/pull/20376)
|
||||
- fix(core): destroy PTY on kill() and exception to prevent fd leak by @nbardy
|
||||
in [#21693](https://github.com/google-gemini/gemini-cli/pull/21693)
|
||||
- fix(docs): update theme screenshots and add missing themes by @ashmod in
|
||||
[#20689](https://github.com/google-gemini/gemini-cli/pull/20689)
|
||||
- refactor(cli): rename 'return' key to 'enter' internally by @scidomino in
|
||||
[#21796](https://github.com/google-gemini/gemini-cli/pull/21796)
|
||||
- build(release): restrict npm bundling to non-stable tags by @sehoon38 in
|
||||
[#21821](https://github.com/google-gemini/gemini-cli/pull/21821)
|
||||
- fix(core): override toolRegistry property for sub-agent schedulers by
|
||||
@gsquared94 in
|
||||
[#21766](https://github.com/google-gemini/gemini-cli/pull/21766)
|
||||
- fix(cli): make footer items equally spaced by @jacob314 in
|
||||
[#21843](https://github.com/google-gemini/gemini-cli/pull/21843)
|
||||
- docs: clarify global policy rules application in plan mode by @jerop in
|
||||
[#21864](https://github.com/google-gemini/gemini-cli/pull/21864)
|
||||
- fix(core): ensure correct flash model steering in plan mode implementation
|
||||
phase by @jerop in
|
||||
[#21871](https://github.com/google-gemini/gemini-cli/pull/21871)
|
||||
- fix(core): update @a2a-js/sdk to 0.3.11 by @adamfweidman in
|
||||
[#21875](https://github.com/google-gemini/gemini-cli/pull/21875)
|
||||
- refactor(core): improve API response error logging when retry by @yunaseoul in
|
||||
[#21784](https://github.com/google-gemini/gemini-cli/pull/21784)
|
||||
- fix(ui): handle headless execution in credits and upgrade dialogs by
|
||||
@gsquared94 in
|
||||
[#21850](https://github.com/google-gemini/gemini-cli/pull/21850)
|
||||
- fix(core): treat retryable errors with >5 min delay as terminal quota errors
|
||||
by @gsquared94 in
|
||||
[#21881](https://github.com/google-gemini/gemini-cli/pull/21881)
|
||||
- feat(telemetry): add specific PR, issue, and custom tracking IDs for GitHub
|
||||
Actions by @cocosheng-g in
|
||||
[#21129](https://github.com/google-gemini/gemini-cli/pull/21129)
|
||||
- feat(core): add OAuth2 Authorization Code auth provider for A2A agents by
|
||||
@SandyTao520 in
|
||||
[#21496](https://github.com/google-gemini/gemini-cli/pull/21496)
|
||||
- feat(cli): give visibility to /tools list command in the TUI and follow the
|
||||
subcommand pattern of other commands by @JayadityaGit in
|
||||
[#21213](https://github.com/google-gemini/gemini-cli/pull/21213)
|
||||
- Handle dirty worktrees better and warn about running scripts/review.sh on
|
||||
untrusted code. by @jacob314 in
|
||||
[#21791](https://github.com/google-gemini/gemini-cli/pull/21791)
|
||||
- feat(policy): support auto-add to policy by default and scoped persistence by
|
||||
@spencer426 in
|
||||
[#20361](https://github.com/google-gemini/gemini-cli/pull/20361)
|
||||
- fix(core): handle AbortError when ESC cancels tool execution by @PrasannaPal21
|
||||
in [#20863](https://github.com/google-gemini/gemini-cli/pull/20863)
|
||||
- fix(release): Improve Patch Release Workflow Comments: Clearer Approval
|
||||
Guidance by @jerop in
|
||||
[#21894](https://github.com/google-gemini/gemini-cli/pull/21894)
|
||||
- docs: clarify telemetry setup and comprehensive data map by @jerop in
|
||||
[#21879](https://github.com/google-gemini/gemini-cli/pull/21879)
|
||||
- feat(core): add per-model token usage to stream-json output by @yongruilin in
|
||||
[#21839](https://github.com/google-gemini/gemini-cli/pull/21839)
|
||||
- docs: remove experimental badge from plan mode in sidebar by @jerop in
|
||||
[#21906](https://github.com/google-gemini/gemini-cli/pull/21906)
|
||||
- fix(cli): prevent race condition in loop detection retry by @skyvanguard in
|
||||
[#17916](https://github.com/google-gemini/gemini-cli/pull/17916)
|
||||
- Add behavioral evals for tracker by @anj-s in
|
||||
[#20069](https://github.com/google-gemini/gemini-cli/pull/20069)
|
||||
- fix(auth): update terminology to 'sign in' and 'sign out' by @clocky in
|
||||
[#20892](https://github.com/google-gemini/gemini-cli/pull/20892)
|
||||
- docs(mcp): standardize mcp tool fqn documentation by @abhipatel12 in
|
||||
[#21664](https://github.com/google-gemini/gemini-cli/pull/21664)
|
||||
- fix(ui): prevent empty tool-group border stubs after filtering by @Aaxhirrr in
|
||||
[#21852](https://github.com/google-gemini/gemini-cli/pull/21852)
|
||||
- make command names consistent by @scidomino in
|
||||
[#21907](https://github.com/google-gemini/gemini-cli/pull/21907)
|
||||
- refactor: remove agent_card_requires_auth config flag by @adamfweidman in
|
||||
[#21914](https://github.com/google-gemini/gemini-cli/pull/21914)
|
||||
- feat(a2a): implement standardized normalization and streaming reassembly by
|
||||
@alisa-alisa in
|
||||
[#21402](https://github.com/google-gemini/gemini-cli/pull/21402)
|
||||
- feat(cli): enable skill activation via slash commands by @NTaylorMullen in
|
||||
[#21758](https://github.com/google-gemini/gemini-cli/pull/21758)
|
||||
- docs(cli): mention per-model token usage in stream-json result event by
|
||||
@yongruilin in
|
||||
[#21908](https://github.com/google-gemini/gemini-cli/pull/21908)
|
||||
- fix(plan): prevent plan truncation in approval dialog by supporting
|
||||
unconstrained heights by @Adib234 in
|
||||
[#21037](https://github.com/google-gemini/gemini-cli/pull/21037)
|
||||
- feat(a2a): switch from callback-based to event-driven tool scheduler by
|
||||
@cocosheng-g in
|
||||
[#21467](https://github.com/google-gemini/gemini-cli/pull/21467)
|
||||
- feat(voice): implement speech-friendly response formatter by @Solventerritory
|
||||
in [#20989](https://github.com/google-gemini/gemini-cli/pull/20989)
|
||||
- feat: add pulsating blue border automation overlay to browser agent by
|
||||
@kunal-10-cloud in
|
||||
[#21173](https://github.com/google-gemini/gemini-cli/pull/21173)
|
||||
- Add extensionRegistryURI setting to change where the registry is read from by
|
||||
@kevinjwang1 in
|
||||
[#20463](https://github.com/google-gemini/gemini-cli/pull/20463)
|
||||
- fix: patch gaxios v7 Array.toString() stream corruption by @gsquared94 in
|
||||
[#21884](https://github.com/google-gemini/gemini-cli/pull/21884)
|
||||
- fix: prevent hangs in non-interactive mode and improve agent guidance by
|
||||
@cocosheng-g in
|
||||
[#20893](https://github.com/google-gemini/gemini-cli/pull/20893)
|
||||
- Add ExtensionDetails dialog and support install by @chrstnb in
|
||||
[#20845](https://github.com/google-gemini/gemini-cli/pull/20845)
|
||||
- chore/release: bump version to 0.34.0-nightly.20260310.4653b126f by
|
||||
@gemini-cli-robot in
|
||||
[#21816](https://github.com/google-gemini/gemini-cli/pull/21816)
|
||||
- Changelog for v0.33.0-preview.13 by @gemini-cli-robot in
|
||||
[#21927](https://github.com/google-gemini/gemini-cli/pull/21927)
|
||||
- fix(cli): stabilize prompt layout to prevent jumping when typing by
|
||||
@NTaylorMullen in
|
||||
[#21081](https://github.com/google-gemini/gemini-cli/pull/21081)
|
||||
- fix: preserve prompt text when cancelling streaming by @Nixxx19 in
|
||||
[#21103](https://github.com/google-gemini/gemini-cli/pull/21103)
|
||||
- fix: robust UX for remote agent errors by @Shyam-Raghuwanshi in
|
||||
[#20307](https://github.com/google-gemini/gemini-cli/pull/20307)
|
||||
- feat: implement background process logging and cleanup by @galz10 in
|
||||
[#21189](https://github.com/google-gemini/gemini-cli/pull/21189)
|
||||
- Changelog for v0.33.0-preview.14 by @gemini-cli-robot in
|
||||
[#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.4
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.1
|
||||
|
||||
@@ -31,7 +31,8 @@ Returns a stream of newline-delimited JSON (JSONL) events.
|
||||
- `tool_use`: Tool call requests with arguments.
|
||||
- `tool_result`: Output from executed tools.
|
||||
- `error`: Non-fatal warnings and system errors.
|
||||
- `result`: Final outcome with aggregated statistics.
|
||||
- `result`: Final outcome with aggregated statistics and per-model token usage
|
||||
breakdowns.
|
||||
|
||||
## Exit codes
|
||||
|
||||
|
||||
@@ -26,6 +26,20 @@ policies.
|
||||
the CLI will use an available fallback model for the current turn or the
|
||||
remainder of the session.
|
||||
|
||||
### Local Model Routing (Experimental)
|
||||
|
||||
Gemini CLI supports using a local model for routing decisions. When configured,
|
||||
Gemini CLI will use a locally-running **Gemma** model to make routing decisions
|
||||
(instead of sending routing decisions to a hosted model). This feature can help
|
||||
reduce costs associated with hosted model usage while offering similar routing
|
||||
decision latency and quality.
|
||||
|
||||
In order to use this feature, the local Gemma model **must** be served behind a
|
||||
Gemini API and accessible via HTTP at an endpoint configured in `settings.json`.
|
||||
|
||||
For more details on how to configure local model routing, see
|
||||
[Local Model Routing](../core/local-model-routing.md).
|
||||
|
||||
### Model selection precedence
|
||||
|
||||
The model used by Gemini CLI is determined by the following order of precedence:
|
||||
@@ -38,5 +52,8 @@ The model used by Gemini CLI is determined by the following order of precedence:
|
||||
3. **`model.name` in `settings.json`:** If neither of the above are set, the
|
||||
model specified in the `model.name` property of your `settings.json` file
|
||||
will be used.
|
||||
4. **Default model:** If none of the above are set, the default model will be
|
||||
4. **Local model (experimental):** If the Gemma local model router is enabled
|
||||
in your `settings.json` file, the CLI will use the local Gemma model
|
||||
(instead of Gemini models) to route the request to an appropriate model.
|
||||
5. **Default model:** If none of the above are set, the default model will be
|
||||
used. The default model is `auto`
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Model steering (experimental)
|
||||
|
||||
Model steering lets you provide real-time guidance and feedback to Gemini CLI
|
||||
while it is actively executing a task. This lets you correct course, add missing
|
||||
context, or skip unnecessary steps without having to stop and restart the agent.
|
||||
|
||||
> **Note:** This is a preview feature under active development. Preview features
|
||||
> may only be available in the **Preview** channel or may need to be enabled
|
||||
> under `/settings`.
|
||||
|
||||
Model steering is particularly useful during complex [Plan Mode](./plan-mode.md)
|
||||
workflows or long-running subagent executions where you want to ensure the agent
|
||||
stays on the right track.
|
||||
|
||||
## Enabling model steering
|
||||
|
||||
Model steering is an experimental feature and is disabled by default. You can
|
||||
enable it using the `/settings` command or by updating your `settings.json`
|
||||
file.
|
||||
|
||||
1. Type `/settings` in the Gemini CLI.
|
||||
2. Search for **Model Steering**.
|
||||
3. Set the value to **true**.
|
||||
|
||||
Alternatively, add the following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"modelSteering": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using model steering
|
||||
|
||||
When model steering is enabled, Gemini CLI treats any text you type while the
|
||||
agent is working as a steering hint.
|
||||
|
||||
1. Start a task (for example, "Refactor the database service").
|
||||
2. While the agent is working (the spinner is visible), type your feedback in
|
||||
the input box.
|
||||
3. Press **Enter**.
|
||||
|
||||
Gemini CLI acknowledges your hint with a brief message and injects it directly
|
||||
into the model's context for the very next turn. The model then re-evaluates its
|
||||
current plan and adjusts its actions accordingly.
|
||||
|
||||
### Common use cases
|
||||
|
||||
You can use steering hints to guide the model in several ways:
|
||||
|
||||
- **Correcting a path:** "Actually, the utilities are in `src/common/utils`."
|
||||
- **Skipping a step:** "Skip the unit tests for now and just focus on the
|
||||
implementation."
|
||||
- **Adding context:** "The `User` type is defined in `packages/core/types.ts`."
|
||||
- **Redirecting the effort:** "Stop searching the codebase and start drafting
|
||||
the plan now."
|
||||
- **Handling ambiguity:** "Use the existing `Logger` class instead of creating a
|
||||
new one."
|
||||
|
||||
## How it works
|
||||
|
||||
When you submit a steering hint, Gemini CLI performs the following actions:
|
||||
|
||||
1. **Immediate acknowledgment:** It uses a small, fast model to generate a
|
||||
one-sentence acknowledgment so you know your hint was received.
|
||||
2. **Context injection:** It prepends an internal instruction to your hint that
|
||||
tells the main agent to:
|
||||
- Re-evaluate the active plan.
|
||||
- Classify the update (for example, as a new task or extra context).
|
||||
- Apply minimal-diff changes to affected tasks.
|
||||
3. **Real-time update:** The hint is delivered to the agent at the beginning of
|
||||
its next turn, ensuring the most immediate course correction possible.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Tackle complex tasks with [Plan Mode](./plan-mode.md).
|
||||
- Build custom [Agent Skills](./skills.md).
|
||||
@@ -61,20 +61,44 @@ Gemini CLI takes action.
|
||||
[`ask_user`](../tools/ask-user.md). Provide your preferences to help guide
|
||||
the design.
|
||||
3. **Review the plan:** Once Gemini CLI has a proposed strategy, it creates a
|
||||
detailed implementation plan as a Markdown file in your plans directory. You
|
||||
can open and read this file to understand the proposed changes.
|
||||
detailed implementation plan as a Markdown file in your plans directory.
|
||||
- **View:** You can open and read this file to understand the proposed
|
||||
changes.
|
||||
- **Edit:** Press `Ctrl+X` to open the plan directly in your configured
|
||||
external editor.
|
||||
|
||||
4. **Approve or iterate:** Gemini CLI will present the finalized plan for your
|
||||
approval.
|
||||
- **Approve:** If you're satisfied with the plan, approve it to start the
|
||||
implementation immediately: **Yes, automatically accept edits** or **Yes,
|
||||
manually accept edits**.
|
||||
- **Iterate:** If the plan needs adjustments, provide feedback. Gemini CLI
|
||||
will refine the strategy and update the plan.
|
||||
- **Iterate:** If the plan needs adjustments, provide feedback in the input
|
||||
box or [edit the plan file directly](#collaborative-plan-editing). Gemini
|
||||
CLI will refine the strategy and update the plan.
|
||||
- **Cancel:** You can cancel your plan with `Esc`.
|
||||
|
||||
For more complex or specialized planning tasks, you can
|
||||
[customize the planning workflow with skills](#custom-planning-with-skills).
|
||||
|
||||
### Collaborative plan editing
|
||||
|
||||
You can collaborate with Gemini CLI by making direct changes or leaving comments
|
||||
in the implementation plan. This is often faster and more precise than
|
||||
describing complex changes in natural language.
|
||||
|
||||
1. **Open the plan:** Press `Ctrl+X` when Gemini CLI presents a plan for
|
||||
review.
|
||||
2. **Edit or comment:** The plan opens in your configured external editor (for
|
||||
example, VS Code or Vim). You can:
|
||||
- **Modify steps:** Directly reorder, delete, or rewrite implementation
|
||||
steps.
|
||||
- **Leave comments:** Add inline questions or feedback (for example, "Wait,
|
||||
shouldn't we use the existing `Logger` class here?").
|
||||
3. **Save and close:** Save your changes and close the editor.
|
||||
4. **Review and refine:** Gemini CLI automatically detects the changes, reviews
|
||||
your comments, and adjusts the implementation strategy. It then presents the
|
||||
refined plan for your final approval.
|
||||
|
||||
## How to exit Plan Mode
|
||||
|
||||
You can exit Plan Mode at any time, whether you have finalized a plan or want to
|
||||
@@ -85,16 +109,6 @@ switch back to another mode.
|
||||
- **Keyboard shortcut:** Press `Shift+Tab` to cycle to the desired mode.
|
||||
- **Natural language:** Ask Gemini CLI to "exit plan mode" or "stop planning."
|
||||
|
||||
## Customization and best practices
|
||||
|
||||
Plan Mode is secure by default, but you can adapt it to fit your specific
|
||||
workflows. You can customize how Gemini CLI plans by using skills, adjusting
|
||||
safety policies, or changing where plans are stored.
|
||||
|
||||
## Commands
|
||||
|
||||
- **`/plan copy`**: Copy the currently approved plan to your clipboard.
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Plan Mode enforces strict safety policies to prevent accidental changes.
|
||||
@@ -122,6 +136,12 @@ These are the only allowed tools:
|
||||
- **Skills:** [`activate_skill`](../cli/skills.md) (allows loading specialized
|
||||
instructions and resources in a read-only manner)
|
||||
|
||||
## Customization and best practices
|
||||
|
||||
Plan Mode is secure by default, but you can adapt it to fit your specific
|
||||
workflows. You can customize how Gemini CLI plans by using skills, adjusting
|
||||
safety policies, changing where plans are stored, or adding hooks.
|
||||
|
||||
### Custom planning with skills
|
||||
|
||||
You can use [Agent Skills](../cli/skills.md) to customize how Gemini CLI
|
||||
@@ -150,6 +170,27 @@ Plan Mode's default tool restrictions are managed by the
|
||||
but you can customize these rules by creating your own policies in your
|
||||
`~/.gemini/policies/` directory (Tier 2).
|
||||
|
||||
#### Global vs. mode-specific rules
|
||||
|
||||
As described in the
|
||||
[policy engine documentation](../reference/policy-engine.md#approval-modes), any
|
||||
rule that does not explicitly specify `modes` is considered "always active" and
|
||||
will apply to Plan Mode as well.
|
||||
|
||||
If you want a rule to apply to other modes but _not_ to Plan Mode, you must
|
||||
explicitly specify the target modes. For example, to allow `npm test` in default
|
||||
and Auto-Edit modes but not in Plan Mode:
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "npm test"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
# By omitting "plan", this rule will not be active in Plan Mode.
|
||||
modes = ["default", "autoEdit"]
|
||||
```
|
||||
|
||||
#### Example: Automatically approve read-only MCP tools
|
||||
|
||||
By default, read-only MCP tools require user confirmation in Plan Mode. You can
|
||||
@@ -249,6 +290,71 @@ modes = ["plan"]
|
||||
argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\""
|
||||
```
|
||||
|
||||
### Using hooks with Plan Mode
|
||||
|
||||
You can use the [hook system](../hooks/writing-hooks.md) to automate parts of
|
||||
the planning workflow or enforce additional checks when Gemini CLI transitions
|
||||
into or out of Plan Mode.
|
||||
|
||||
Hooks such as `BeforeTool` or `AfterTool` can be configured to intercept the
|
||||
`enter_plan_mode` and `exit_plan_mode` tool calls.
|
||||
|
||||
> [!WARNING] When hooks are triggered by **tool executions**, they do **not**
|
||||
> run when you manually toggle Plan Mode using the `/plan` command or the
|
||||
> `Shift+Tab` keyboard shortcut. If you need hooks to execute on mode changes,
|
||||
> ensure the transition is initiated by the agent (e.g., by asking "start a plan
|
||||
> for...").
|
||||
|
||||
#### Example: Archive approved plans to GCS (`AfterTool`)
|
||||
|
||||
If your organizational policy requires a record of all execution plans, you can
|
||||
use an `AfterTool` hook to securely copy the plan artifact to Google Cloud
|
||||
Storage whenever Gemini CLI exits Plan Mode to start the implementation.
|
||||
|
||||
**`.gemini/hooks/archive-plan.sh`:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Extract the plan path from the tool input JSON
|
||||
plan_path=$(jq -r '.tool_input.plan_path // empty')
|
||||
|
||||
if [ -f "$plan_path" ]; then
|
||||
# Generate a unique filename using a timestamp
|
||||
filename="$(date +%s)_$(basename "$plan_path")"
|
||||
|
||||
# Upload the plan to GCS in the background so it doesn't block the CLI
|
||||
gsutil cp "$plan_path" "gs://my-audit-bucket/gemini-plans/$filename" > /dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
# AfterTool hooks should generally allow the flow to continue
|
||||
echo '{"decision": "allow"}'
|
||||
```
|
||||
|
||||
To register this `AfterTool` hook, add it to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"AfterTool": [
|
||||
{
|
||||
"matcher": "exit_plan_mode",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "archive-plan",
|
||||
"type": "command",
|
||||
"command": "./.gemini/hooks/archive-plan.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
- **`/plan copy`**: Copy the currently approved plan to your clipboard.
|
||||
|
||||
## Planning workflows
|
||||
|
||||
Plan Mode provides building blocks for structured research and design. These are
|
||||
|
||||
@@ -22,18 +22,19 @@ they appear in the UI.
|
||||
|
||||
### General
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
|
||||
| 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 | `true` |
|
||||
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
|
||||
| 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` |
|
||||
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `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 | `true` |
|
||||
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
|
||||
|
||||
### Output
|
||||
|
||||
@@ -54,6 +55,7 @@ they appear in the UI.
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
@@ -66,7 +68,7 @@ they appear in the UI.
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
@@ -125,6 +127,7 @@ they appear in the UI.
|
||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
| Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` |
|
||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||
| Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
|
||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `true` |
|
||||
|
||||
@@ -16,6 +16,8 @@ using the `/theme` command within Gemini CLI:
|
||||
- `Default`
|
||||
- `Dracula`
|
||||
- `GitHub`
|
||||
- `Holiday`
|
||||
- `Shades Of Purple`
|
||||
- `Solarized Dark`
|
||||
- **Light themes:**
|
||||
- `ANSI Light`
|
||||
@@ -185,7 +187,7 @@ untrusted sources.
|
||||
|
||||
### Example custom theme
|
||||
|
||||
<img src="../assets/theme-custom.png" alt="Custom theme example" width="600" />
|
||||
<img src="/docs/assets/theme-custom.png" alt="Custom theme example" width="600" />
|
||||
|
||||
### Using your custom theme
|
||||
|
||||
@@ -212,58 +214,66 @@ identify their source, for example: `shades-of-green (green-extension)`.
|
||||
|
||||
### ANSI
|
||||
|
||||
<img src="/assets/theme-ansi.png" alt="ANSI theme" width="600" />
|
||||
<img src="/docs/assets/theme-ansi-dark.png" alt="ANSI theme" width="600">
|
||||
|
||||
### Atom OneDark
|
||||
### Atom One
|
||||
|
||||
<img src="/assets/theme-atom-one.png" alt="Atom One theme" width="600">
|
||||
<img src="/docs/assets/theme-atom-one-dark.png" alt="Atom One theme" width="600">
|
||||
|
||||
### Ayu
|
||||
|
||||
<img src="/assets/theme-ayu.png" alt="Ayu theme" width="600">
|
||||
<img src="/docs/assets/theme-ayu-dark.png" alt="Ayu theme" width="600">
|
||||
|
||||
### Default
|
||||
|
||||
<img src="/assets/theme-default.png" alt="Default theme" width="600">
|
||||
<img src="/docs/assets/theme-default-dark.png" alt="Default theme" width="600">
|
||||
|
||||
### Dracula
|
||||
|
||||
<img src="/assets/theme-dracula.png" alt="Dracula theme" width="600">
|
||||
<img src="/docs/assets/theme-dracula-dark.png" alt="Dracula theme" width="600">
|
||||
|
||||
### GitHub
|
||||
|
||||
<img src="/assets/theme-github.png" alt="GitHub theme" width="600">
|
||||
<img src="/docs/assets/theme-github-dark.png" alt="GitHub theme" width="600">
|
||||
|
||||
### Holiday
|
||||
|
||||
<img src="/docs/assets/theme-holiday-dark.png" alt="Holiday theme" width="600">
|
||||
|
||||
### Shades Of Purple
|
||||
|
||||
<img src="/docs/assets/theme-shades-of-purple-dark.png" alt="Shades Of Purple theme" width="600">
|
||||
|
||||
### Solarized Dark
|
||||
|
||||
<img src="/assets/theme-solarized-dark.png" alt="Solarized Dark theme" width="600">
|
||||
<img src="/docs/assets/theme-solarized-dark.png" alt="Solarized Dark theme" width="600">
|
||||
|
||||
## Light themes
|
||||
|
||||
### ANSI Light
|
||||
|
||||
<img src="/assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
<img src="/docs/assets/theme-ansi-light.png" alt="ANSI Light theme" width="600">
|
||||
|
||||
### Ayu Light
|
||||
|
||||
<img src="/assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
<img src="/docs/assets/theme-ayu-light.png" alt="Ayu Light theme" width="600">
|
||||
|
||||
### Default Light
|
||||
|
||||
<img src="/assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
<img src="/docs/assets/theme-default-light.png" alt="Default Light theme" width="600">
|
||||
|
||||
### GitHub Light
|
||||
|
||||
<img src="/assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
<img src="/docs/assets/theme-github-light.png" alt="GitHub Light theme" width="600">
|
||||
|
||||
### Google Code
|
||||
|
||||
<img src="/assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
<img src="/docs/assets/theme-google-light.png" alt="Google Code theme" width="600">
|
||||
|
||||
### Solarized Light
|
||||
|
||||
<img src="/assets/theme-solarized-light.png" alt="Solarized Light theme" width="600">
|
||||
<img src="/docs/assets/theme-solarized-light.png" alt="Solarized Light theme" width="600">
|
||||
|
||||
### Xcode
|
||||
|
||||
<img src="/assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
<img src="/docs/assets/theme-xcode-light.png" alt="Xcode Light theme" width="600">
|
||||
|
||||
@@ -89,7 +89,7 @@ don't need to learn special commands; just ask in natural language.
|
||||
The agent will:
|
||||
|
||||
1. Recognize the request matches a GitHub tool.
|
||||
2. Call `github_list_pull_requests`.
|
||||
2. Call `mcp_github_list_pull_requests`.
|
||||
3. Present the data to you.
|
||||
|
||||
### Scenario: Creating an issue
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Use Plan Mode with model steering for complex tasks
|
||||
|
||||
Architecting a complex solution requires precision. By combining Plan Mode's
|
||||
structured environment with model steering's real-time feedback, you can guide
|
||||
Gemini CLI through the research and design phases to ensure the final
|
||||
implementation plan is exactly what you need.
|
||||
|
||||
> **Note:** This is a preview feature under active development. Preview features
|
||||
> may only be available in the **Preview** channel or may need to be enabled
|
||||
> under `/settings`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Gemini CLI installed and authenticated.
|
||||
- [Plan Mode](../plan-mode.md) enabled in your settings.
|
||||
- [Model steering](../model-steering.md) enabled in your settings.
|
||||
|
||||
## Why combine Plan Mode and model steering?
|
||||
|
||||
[Plan Mode](../plan-mode.md) typically follows a linear path: research, propose,
|
||||
and draft. Adding model steering lets you:
|
||||
|
||||
1. **Direct the research:** Correct the agent if it's looking in the wrong
|
||||
directory or missing a key dependency.
|
||||
2. **Iterate mid-draft:** Suggest a different architectural pattern while the
|
||||
agent is still writing the plan.
|
||||
3. **Speed up the loop:** Avoid waiting for a full research turn to finish
|
||||
before providing critical context.
|
||||
|
||||
## Step 1: Start a complex task
|
||||
|
||||
Enter Plan Mode and start a task that requires research.
|
||||
|
||||
**Prompt:** `/plan I want to implement a new notification service using Redis.`
|
||||
|
||||
Gemini CLI enters Plan Mode and starts researching your existing codebase to
|
||||
identify where the new service should live.
|
||||
|
||||
## Step 2: Steer the research phase
|
||||
|
||||
As you see the agent calling tools like `list_directory` or `grep_search`, you
|
||||
might realize it's missing the relevant context.
|
||||
|
||||
**Action:** While the spinner is active, type your hint:
|
||||
`"Don't forget to check packages/common/queues for the existing Redis config."`
|
||||
|
||||
**Result:** Gemini CLI acknowledges your hint and immediately incorporates it
|
||||
into its research. You'll see it start exploring the directory you suggested in
|
||||
its very next turn.
|
||||
|
||||
## Step 3: Refine the design mid-turn
|
||||
|
||||
After research, the agent starts drafting the implementation plan. If you notice
|
||||
it's proposing a design that doesn't align with your goals, steer it.
|
||||
|
||||
**Action:** Type:
|
||||
`"Actually, let's use a Publisher/Subscriber pattern instead of a simple queue for this service."`
|
||||
|
||||
**Result:** The agent stops drafting the current version of the plan,
|
||||
re-evaluates the design based on your feedback, and starts a new draft that uses
|
||||
the Pub/Sub pattern.
|
||||
|
||||
## Step 4: Approve and implement
|
||||
|
||||
Once the agent has used your hints to craft the perfect plan, review the final
|
||||
`.md` file.
|
||||
|
||||
**Action:** Type: `"Looks perfect. Let's start the implementation."`
|
||||
|
||||
Gemini CLI exits Plan Mode and transitions to the implementation phase. Because
|
||||
the plan was refined in real-time with your feedback, the agent can now execute
|
||||
each step with higher confidence and fewer errors.
|
||||
|
||||
## Tips for effective steering
|
||||
|
||||
- **Be specific:** Instead of "do it differently," try "use the existing
|
||||
`Logger` class in `src/utils`."
|
||||
- **Steer early:** Providing feedback during the research phase is more
|
||||
efficient than waiting for the final plan to be drafted.
|
||||
- **Use for context:** Steering is a great way to provide knowledge that might
|
||||
not be obvious from reading the code (e.g., "We are planning to deprecate this
|
||||
module next month").
|
||||
|
||||
## Next steps
|
||||
|
||||
- Explore [Agent Skills](../skills.md) to add specialized expertise to your
|
||||
planning turns.
|
||||
- See the [Model steering reference](../model-steering.md) for technical
|
||||
details.
|
||||
@@ -15,6 +15,8 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
|
||||
modular GEMINI.md import feature using @file.md syntax.
|
||||
- **[Policy Engine](../reference/policy-engine.md):** Use the Policy Engine for
|
||||
fine-grained control over tool execution.
|
||||
- **[Local Model Routing (experimental)](./local-model-routing.md):** Learn how
|
||||
to enable use of a local Gemma model for model routing decisions.
|
||||
|
||||
## Role of the core
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Local Model Routing (experimental)
|
||||
|
||||
Gemini CLI supports using a local model for
|
||||
[routing decisions](../cli/model-routing.md). When configured, Gemini CLI will
|
||||
use a locally-running **Gemma** model to make routing decisions (instead of
|
||||
sending routing decisions to a hosted model).
|
||||
|
||||
This feature can help reduce costs associated with hosted model usage while
|
||||
offering similar routing decision latency and quality.
|
||||
|
||||
> **Note: Local model routing is currently an experimental feature.**
|
||||
|
||||
## Setup
|
||||
|
||||
Using a Gemma model for routing decisions requires that an implementation of a
|
||||
Gemma model be running locally on your machine, served behind an HTTP endpoint
|
||||
and accessed via the Gemini API.
|
||||
|
||||
To serve the Gemma model, follow these steps:
|
||||
|
||||
### Download the LiteRT-LM runtime
|
||||
|
||||
The [LiteRT-LM](https://github.com/google-ai-edge/LiteRT-LM) runtime offers
|
||||
pre-built binaries for locally-serving models. Download the binary appropriate
|
||||
for your system.
|
||||
|
||||
#### Windows
|
||||
|
||||
1. Download
|
||||
[lit.windows_x86_64.exe](https://github.com/google-ai-edge/LiteRT-LM/releases/download/v0.9.0-alpha03/lit.windows_x86_64.exe).
|
||||
2. Using GPU on Windows requires the DirectXShaderCompiler. Download the
|
||||
[dxc zip from the latest release](https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.8.2505.1/dxc_2025_07_14.zip).
|
||||
Unzip the archive and from the architecture-appropriate `bin\` directory, and
|
||||
copy the `dxil.dll` and `dxcompiler.dll` into the same location as you saved
|
||||
`lit.windows_x86_64.exe`.
|
||||
3. (Optional) Test starting the runtime:
|
||||
`.\lit.windows_x86_64.exe serve --verbose`
|
||||
|
||||
#### Linux
|
||||
|
||||
1. Download
|
||||
[lit.linux_x86_64](https://github.com/google-ai-edge/LiteRT-LM/releases/download/v0.9.0-alpha03/lit.linux_x86_64).
|
||||
2. Ensure the binary is executable: `chmod a+x lit.linux_x86_64`
|
||||
3. (Optional) Test starting the runtime: `./lit.linux_x86_64 serve --verbose`
|
||||
|
||||
#### MacOS
|
||||
|
||||
1. Download
|
||||
[lit-macos-arm64](https://github.com/google-ai-edge/LiteRT-LM/releases/download/v0.9.0-alpha03/lit.macos_arm64).
|
||||
2. Ensure the binary is executable: `chmod a+x lit.macos_arm64`
|
||||
3. (Optional) Test starting the runtime: `./lit.macos_arm64 serve --verbose`
|
||||
|
||||
> **Note**: MacOS can be configured to only allows binaries from "App Store &
|
||||
> Known Developers". If you encounter an error message when attempting to run
|
||||
> the binary, you will need to allow the application. One option is to visit
|
||||
> `System Settings -> Privacy & Security`, scroll to `Security`, and click
|
||||
> `"Allow Anyway"` for `"lit.macos_arm64"`. Another option is to run
|
||||
> `xattr -d com.apple.quarantine lit.macos_arm64` from the commandline.
|
||||
|
||||
### Download the Gemma Model
|
||||
|
||||
Before using Gemma, you will need to download the model (and agree to the Terms
|
||||
of Service).
|
||||
|
||||
This can be done via the LiteRT-LM runtime.
|
||||
|
||||
#### Windows
|
||||
|
||||
```bash
|
||||
$ .\lit.windows_x86_64.exe pull gemma3-1b-gpu-custom
|
||||
|
||||
[Legal] The model you are about to download is governed by
|
||||
the Gemma Terms of Use and Prohibited Use Policy. Please review these terms and ensure you agree before continuing.
|
||||
|
||||
Full Terms: https://ai.google.dev/gemma/terms
|
||||
Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy
|
||||
|
||||
Do you accept these terms? (Y/N): Y
|
||||
|
||||
Terms accepted.
|
||||
Downloading model 'gemma3-1b-gpu-custom' ...
|
||||
Downloading... 968.6 MB
|
||||
Download complete.
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
$ ./lit.linux_x86_64 pull gemma3-1b-gpu-custom
|
||||
|
||||
[Legal] The model you are about to download is governed by
|
||||
the Gemma Terms of Use and Prohibited Use Policy. Please review these terms and ensure you agree before continuing.
|
||||
|
||||
Full Terms: https://ai.google.dev/gemma/terms
|
||||
Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy
|
||||
|
||||
Do you accept these terms? (Y/N): Y
|
||||
|
||||
Terms accepted.
|
||||
Downloading model 'gemma3-1b-gpu-custom' ...
|
||||
Downloading... 968.6 MB
|
||||
Download complete.
|
||||
```
|
||||
|
||||
#### MacOS
|
||||
|
||||
```bash
|
||||
$ ./lit.lit.macos_arm64 pull gemma3-1b-gpu-custom
|
||||
|
||||
[Legal] The model you are about to download is governed by
|
||||
the Gemma Terms of Use and Prohibited Use Policy. Please review these terms and ensure you agree before continuing.
|
||||
|
||||
Full Terms: https://ai.google.dev/gemma/terms
|
||||
Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy
|
||||
|
||||
Do you accept these terms? (Y/N): Y
|
||||
|
||||
Terms accepted.
|
||||
Downloading model 'gemma3-1b-gpu-custom' ...
|
||||
Downloading... 968.6 MB
|
||||
Download complete.
|
||||
```
|
||||
|
||||
### Start LiteRT-LM Runtime
|
||||
|
||||
Using the command appropriate to your system, start the LiteRT-LM runtime.
|
||||
Configure the port that you want to use for your Gemma model. For the purposes
|
||||
of this document, we will use port `9379`.
|
||||
|
||||
Example command for MacOS: `./lit.macos_arm64 serve --port=9379 --verbose`
|
||||
|
||||
### (Optional) Verify Model Serving
|
||||
|
||||
Send a quick prompt to the model via HTTP to validate successful model serving.
|
||||
This will cause the runtime to download the model and run it once.
|
||||
|
||||
You should see a short joke in the server output as an indicator of success.
|
||||
|
||||
#### Windows
|
||||
|
||||
```
|
||||
# Run this in PowerShell to send a request to the server
|
||||
|
||||
$uri = "http://localhost:9379/v1beta/models/gemma3-1b-gpu-custom:generateContent"
|
||||
$body = @{contents = @( @{
|
||||
role = "user"
|
||||
parts = @( @{ text = "Tell me a joke." } )
|
||||
})} | ConvertTo-Json -Depth 10
|
||||
|
||||
Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json"
|
||||
```
|
||||
|
||||
#### Linux/MacOS
|
||||
|
||||
```bash
|
||||
$ curl "http://localhost:9379/v1beta/models/gemma3-1b-gpu-custom:generateContent" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST \
|
||||
-d '{"contents":[{"role":"user","parts":[{"text":"Tell me a joke."}]}]}'
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
To use a local Gemma model for routing, you must explicitly enable it in your
|
||||
`settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"gemmaModelRouter": {
|
||||
"enabled": true,
|
||||
"classifier": {
|
||||
"host": "http://localhost:9379",
|
||||
"model": "gemma3-1b-gpu-custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Use the port you started your LiteRT-LM runtime on in the setup steps.
|
||||
|
||||
### Configuration schema
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :----------------- | :------ | :------- | :----------------------------------------------------------------------------------------- |
|
||||
| `enabled` | boolean | Yes | Must be `true` to enable the feature. |
|
||||
| `classifier` | object | Yes | The configuration for the local model endpoint. It includes the host and model specifiers. |
|
||||
| `classifier.host` | string | Yes | The URL to the local model server. Should be `http://localhost:<port>`. |
|
||||
| `classifier.model` | string | Yes | The model name to use for decisions. Must be `"gemma3-1b-gpu-custom"`. |
|
||||
|
||||
> **Note: You will need to restart after configuration changes for local model
|
||||
> routing to take effect.**
|
||||
@@ -194,7 +194,7 @@ returns coordinates and element descriptions that the browser agent uses with
|
||||
the `click_at` tool for precise, coordinate-based interactions.
|
||||
|
||||
> **Note:** The visual agent requires API key or Vertex AI authentication. It is
|
||||
> not available when using Google Login.
|
||||
> not available when using "Sign in with Google".
|
||||
|
||||
## Creating custom subagents
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ The manifest file defines the extension's behavior and configuration.
|
||||
},
|
||||
"contextFileName": "GEMINI.md",
|
||||
"excludeTools": ["run_shell_command"],
|
||||
"migratedTo": "https://github.com/new-owner/new-extension-repo",
|
||||
"plan": {
|
||||
"directory": ".gemini/plans"
|
||||
}
|
||||
@@ -138,6 +139,9 @@ The manifest file defines the extension's behavior and configuration.
|
||||
- `version`: The version of the extension.
|
||||
- `description`: A short description of the extension. This will be displayed on
|
||||
[geminicli.com/extensions](https://geminicli.com/extensions).
|
||||
- `migratedTo`: The URL of the new repository source for the extension. If this
|
||||
is set, the CLI will automatically check this new source for updates and
|
||||
migrate the extension's installation to the new source if an update is found.
|
||||
- `mcpServers`: A map of MCP servers to settings. The key is the name of the
|
||||
server, and the value is the server configuration. These servers will be
|
||||
loaded on startup just like MCP servers defined in a
|
||||
@@ -258,12 +262,14 @@ but lower priority than user or admin policies.
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "my_server__dangerous_tool"
|
||||
mcpName = "my_server"
|
||||
toolName = "dangerous_tool"
|
||||
decision = "ask_user"
|
||||
priority = 100
|
||||
|
||||
[[safety_checker]]
|
||||
toolName = "my_server__write_data"
|
||||
mcpName = "my_server"
|
||||
toolName = "write_data"
|
||||
priority = 200
|
||||
[safety_checker.checker]
|
||||
type = "in-process"
|
||||
|
||||
@@ -152,3 +152,29 @@ jobs:
|
||||
release/linux.arm64.my-tool.tar.gz
|
||||
release/win32.arm64.my-tool.zip
|
||||
```
|
||||
|
||||
## Migrating an Extension Repository
|
||||
|
||||
If you need to move your extension to a new repository (e.g., from a personal
|
||||
account to an organization) or rename it, you can use the `migratedTo` property
|
||||
in your `gemini-extension.json` file to seamlessly transition your users.
|
||||
|
||||
1. **Create the new repository**: Setup your extension in its new location.
|
||||
2. **Update the old repository**: In your original repository, update the
|
||||
`gemini-extension.json` file to include the `migratedTo` property, pointing
|
||||
to the new repository URL, and bump the version number. You can optionally
|
||||
change the `name` of your extension at this time in the new repository.
|
||||
```json
|
||||
{
|
||||
"name": "my-extension",
|
||||
"version": "1.1.0",
|
||||
"migratedTo": "https://github.com/new-owner/new-extension-repo"
|
||||
}
|
||||
```
|
||||
3. **Release the update**: Publish this new version in your old repository.
|
||||
|
||||
When users check for updates, the Gemini CLI will detect the `migratedTo` field,
|
||||
verify that the new repository contains a valid extension update, and
|
||||
automatically update their local installation to track the new source and name
|
||||
moving forward. All extension settings will automatically migrate to the new
|
||||
installation.
|
||||
|
||||
@@ -17,8 +17,8 @@ Select the authentication method that matches your situation in the table below:
|
||||
|
||||
| User Type / Scenario | Recommended Authentication Method | Google Cloud Project Required |
|
||||
| :--------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------- |
|
||||
| Individual Google accounts | [Login with Google](#login-google) | No, with exceptions |
|
||||
| Organization users with a company, school, or Google Workspace account | [Login with Google](#login-google) | [Yes](#set-gcp) |
|
||||
| Individual Google accounts | [Sign in with Google](#login-google) | No, with exceptions |
|
||||
| Organization users with a company, school, or Google Workspace account | [Sign in with Google](#login-google) | [Yes](#set-gcp) |
|
||||
| AI Studio user with a Gemini API key | [Use Gemini API Key](#gemini-api) | No |
|
||||
| Google Cloud Vertex AI user | [Vertex AI](#vertex-ai) | [Yes](#set-gcp) |
|
||||
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br> [Yes](#set-gcp) (for Vertex AI) |
|
||||
@@ -36,7 +36,7 @@ Select the authentication method that matches your situation in the table below:
|
||||
[Google AI Ultra for Business](https://support.google.com/a/answer/16345165)
|
||||
subscriptions.
|
||||
|
||||
## (Recommended) Login with Google <a id="login-google"></a>
|
||||
## (Recommended) Sign in with Google <a id="login-google"></a>
|
||||
|
||||
If you run Gemini CLI on your local machine, the simplest authentication method
|
||||
is logging in with your Google account. This method requires a web browser on a
|
||||
@@ -54,9 +54,9 @@ To authenticate and use Gemini CLI:
|
||||
gemini
|
||||
```
|
||||
|
||||
2. Select **Login with Google**. Gemini CLI opens a login prompt using your web
|
||||
browser. Follow the on-screen instructions. Your credentials will be cached
|
||||
locally for future sessions.
|
||||
2. Select **Sign in with Google**. Gemini CLI opens a sign in prompt using your
|
||||
web browser. Follow the on-screen instructions. Your credentials will be
|
||||
cached locally for future sessions.
|
||||
|
||||
### Do I need to set my Google Cloud project?
|
||||
|
||||
@@ -391,7 +391,7 @@ on this page.
|
||||
[Headless mode](../cli/headless) will use your existing authentication method,
|
||||
if an existing authentication credential is cached.
|
||||
|
||||
If you have not already logged in with an authentication credential, you must
|
||||
If you have not already signed in with an authentication credential, you must
|
||||
configure authentication using environment variables:
|
||||
|
||||
- [Use Gemini API Key](#gemini-api)
|
||||
|
||||
@@ -38,7 +38,7 @@ cases, you can log in with your existing Google account:
|
||||
```
|
||||
|
||||
2. When asked "How would you like to authenticate for this project?" select **1.
|
||||
Login with Google**.
|
||||
Sign in with Google**.
|
||||
|
||||
3. Select your Google account.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ compared against the name of the tool being executed.
|
||||
`run_shell_command`). See the [Tools Reference](../reference/tools) for a full
|
||||
list of available tool names.
|
||||
- **MCP Tools**: Tools from MCP servers follow the naming pattern
|
||||
`mcp__<server_name>__<tool_name>`.
|
||||
`mcp_<server_name>_<tool_name>`.
|
||||
- **Regex Support**: Matchers support regular expressions (e.g.,
|
||||
`matcher: "read_.*"` matches all file reading tools).
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
# Local development guide
|
||||
|
||||
This guide provides instructions for setting up and using local development
|
||||
features, such as tracing.
|
||||
features for Gemini CLI.
|
||||
|
||||
## Tracing
|
||||
|
||||
Traces are OpenTelemetry (OTel) records that help you debug your code by
|
||||
instrumenting key events like model calls, tool scheduler operations, and tool
|
||||
calls.
|
||||
Gemini CLI uses OpenTelemetry (OTel) to record traces that help you debug agent
|
||||
behavior. Traces instrument key events like model calls, tool scheduler
|
||||
operations, and tool calls.
|
||||
|
||||
Traces provide deep visibility into agent behavior and are invaluable for
|
||||
debugging complex issues. They are captured automatically when telemetry is
|
||||
enabled.
|
||||
Traces provide deep visibility into agent behavior and help you debug complex
|
||||
issues. They are captured automatically when you enable telemetry.
|
||||
|
||||
### Viewing traces
|
||||
### View traces
|
||||
|
||||
You can view traces using either Jaeger or the Genkit Developer UI.
|
||||
You can view traces using Genkit Developer UI, Jaeger, or Google Cloud.
|
||||
|
||||
#### Using Genkit
|
||||
#### Use Genkit
|
||||
|
||||
Genkit provides a web-based UI for viewing traces and other telemetry data.
|
||||
|
||||
@@ -29,11 +28,8 @@ Genkit provides a web-based UI for viewing traces and other telemetry data.
|
||||
npm run telemetry -- --target=genkit
|
||||
```
|
||||
|
||||
The script will output the URL for the Genkit Developer UI, for example:
|
||||
|
||||
```
|
||||
Genkit Developer UI: http://localhost:4000
|
||||
```
|
||||
The script will output the URL for the Genkit Developer UI. For example:
|
||||
`Genkit Developer UI: http://localhost:4000`
|
||||
|
||||
2. **Run Gemini CLI:**
|
||||
|
||||
@@ -48,21 +44,22 @@ Genkit provides a web-based UI for viewing traces and other telemetry data.
|
||||
Open the Genkit Developer UI URL in your browser and navigate to the
|
||||
**Traces** tab to view the traces.
|
||||
|
||||
#### Using Jaeger
|
||||
#### Use Jaeger
|
||||
|
||||
You can view traces in the Jaeger UI. To get started, follow these steps:
|
||||
You can view traces in the Jaeger UI for local development.
|
||||
|
||||
1. **Start the telemetry collector:**
|
||||
|
||||
Run the following command in your terminal to download and start Jaeger and
|
||||
an OTEL collector:
|
||||
an OTel collector:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
This command also configures your workspace for local telemetry and provides
|
||||
a link to the Jaeger UI (usually `http://localhost:16686`).
|
||||
This command configures your workspace for local telemetry and provides a
|
||||
link to the Jaeger UI (usually `http://localhost:16686`).
|
||||
- **Collector logs:** `~/.gemini/tmp/<projectHash>/otel/collector.log`
|
||||
|
||||
2. **Run Gemini CLI:**
|
||||
|
||||
@@ -77,16 +74,63 @@ You can view traces in the Jaeger UI. To get started, follow these steps:
|
||||
After running your command, open the Jaeger UI link in your browser to view
|
||||
the traces.
|
||||
|
||||
#### Use Google Cloud
|
||||
|
||||
You can use an OpenTelemetry collector to forward telemetry data to Google Cloud
|
||||
Trace for custom processing or routing.
|
||||
|
||||
> **Warning:** Ensure you complete the
|
||||
> [Google Cloud telemetry prerequisites](./cli/telemetry.md#prerequisites)
|
||||
> (Project ID, authentication, IAM roles, and APIs) before using this method.
|
||||
|
||||
1. **Configure `.gemini/settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp",
|
||||
"useCollector": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Start the telemetry collector:**
|
||||
|
||||
Run the following command to start a local OTel collector that forwards to
|
||||
Google Cloud:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
|
||||
The script outputs links to view traces, metrics, and logs in the Google
|
||||
Cloud Console.
|
||||
- **Collector logs:** `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`
|
||||
|
||||
3. **Run Gemini CLI:**
|
||||
|
||||
In a separate terminal, run your Gemini CLI command:
|
||||
|
||||
```bash
|
||||
gemini
|
||||
```
|
||||
|
||||
4. **View logs, metrics, and traces:**
|
||||
|
||||
After sending prompts, view your data in the Google Cloud Console. See the
|
||||
[telemetry documentation](./cli/telemetry.md#view-google-cloud-telemetry)
|
||||
for links to Logs, Metrics, and Trace explorers.
|
||||
|
||||
For more detailed information on telemetry, see the
|
||||
[telemetry documentation](./cli/telemetry.md).
|
||||
|
||||
### Instrumenting code with traces
|
||||
### Instrument code with traces
|
||||
|
||||
You can add traces to your own code for more detailed instrumentation. This is
|
||||
useful for debugging and understanding the flow of execution.
|
||||
You can add traces to your own code for more detailed instrumentation.
|
||||
|
||||
Use the `runInDevTraceSpan` function to wrap any section of code in a trace
|
||||
span.
|
||||
Adding traces helps you debug and understand the flow of execution. Use the
|
||||
`runInDevTraceSpan` function to wrap any section of code in a trace span.
|
||||
|
||||
Here is a basic example:
|
||||
|
||||
@@ -102,13 +146,13 @@ await runInDevTraceSpan(
|
||||
},
|
||||
},
|
||||
async ({ metadata }) => {
|
||||
// The `metadata` object allows you to record the input and output of the
|
||||
// metadata allows you to record the input and output of the
|
||||
// operation as well as other attributes.
|
||||
metadata.input = { key: 'value' };
|
||||
// Set custom attributes.
|
||||
metadata.attributes['custom.attribute'] = 'custom.value';
|
||||
|
||||
// Your code to be traced goes here
|
||||
// Your code to be traced goes here.
|
||||
try {
|
||||
const output = await somethingRisky();
|
||||
metadata.output = output;
|
||||
|
||||
@@ -71,9 +71,9 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
[Checkpointing documentation](../cli/checkpointing.md).
|
||||
- **Equivalent:** `/resume save <tag>`
|
||||
- **`share [filename]`**
|
||||
- **Description** Writes the current conversation to a provided Markdown or
|
||||
- **Description:** Writes the current conversation to a provided Markdown or
|
||||
JSON file. If no filename is provided, then the CLI will generate one.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`.
|
||||
- **Usage:** `/chat share file.md` or `/chat share file.json`.
|
||||
- **Equivalent:** `/resume share [filename]`
|
||||
|
||||
### `/clear`
|
||||
|
||||
@@ -92,6 +92,13 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `[]`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `adminPolicyPaths`
|
||||
|
||||
- **`adminPolicyPaths`** (array):
|
||||
- **Description:** Additional admin policy files or directories to load.
|
||||
- **Default:** `[]`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `general`
|
||||
|
||||
- **`general.preferredEditor`** (string):
|
||||
@@ -105,7 +112,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`general.defaultApprovalMode`** (enum):
|
||||
- **Description:** The default approval mode for tool execution. 'default'
|
||||
prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is
|
||||
read-only mode. 'yolo' is not supported yet.
|
||||
read-only mode. YOLO mode (auto-approve all actions) can only be enabled via
|
||||
command line (--yolo or --approval-mode=yolo).
|
||||
- **Default:** `"default"`
|
||||
- **Values:** `"default"`, `"auto_edit"`, `"plan"`
|
||||
|
||||
@@ -146,7 +154,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`general.retryFetchErrors`** (boolean):
|
||||
- **Description:** Retry on "exception TypeError: fetch failed sending
|
||||
request" errors.
|
||||
- **Default:** `false`
|
||||
- **Default:** `true`
|
||||
|
||||
- **`general.maxAttempts`** (number):
|
||||
- **Description:** Maximum number of attempts for requests to the main chat
|
||||
@@ -237,6 +245,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Hide helpful tips in the UI
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.escapePastedAtSymbols`** (boolean):
|
||||
- **Description:** When enabled, @ symbols in pasted text are escaped to
|
||||
prevent unintended @path expansion.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showShortcutsHint`** (boolean):
|
||||
- **Description:** Show the "? for shortcuts" hint above the input.
|
||||
- **Default:** `true`
|
||||
@@ -297,7 +310,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showUserIdentity`** (boolean):
|
||||
- **Description:** Show the logged-in user's identity (e.g. email) in the UI.
|
||||
- **Description:** Show the signed-in user's identity (e.g. email) in the UI.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.useAlternateBuffer`** (boolean):
|
||||
@@ -664,6 +677,141 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
used.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`modelConfigs.modelDefinitions`** (object):
|
||||
- **Description:** Registry of model metadata, including tier, family, and
|
||||
features.
|
||||
- **Default:**
|
||||
|
||||
```json
|
||||
{
|
||||
"gemini-3.1-pro-preview": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3.1-pro-preview-customtools": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-3-flash-preview": {
|
||||
"tier": "flash",
|
||||
"family": "gemini-3",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": true
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"tier": "pro",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash": {
|
||||
"tier": "flash",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"tier": "flash-lite",
|
||||
"family": "gemini-2.5",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "manual",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto": {
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"pro": {
|
||||
"tier": "pro",
|
||||
"isPreview": false,
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"flash": {
|
||||
"tier": "flash",
|
||||
"isPreview": false,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"flash-lite": {
|
||||
"tier": "flash-lite",
|
||||
"isPreview": false,
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto-gemini-3": {
|
||||
"displayName": "Auto (Gemini 3)",
|
||||
"tier": "auto",
|
||||
"isPreview": true,
|
||||
"dialogLocation": "main",
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
|
||||
"features": {
|
||||
"thinking": true,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
},
|
||||
"auto-gemini-2.5": {
|
||||
"displayName": "Auto (Gemini 2.5)",
|
||||
"tier": "auto",
|
||||
"isPreview": false,
|
||||
"dialogLocation": "main",
|
||||
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
|
||||
"features": {
|
||||
"thinking": false,
|
||||
"multimodalToolUse": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `agents`
|
||||
|
||||
- **`agents.overrides`** (object):
|
||||
@@ -693,6 +841,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`agents.browser.disableUserInput`** (boolean):
|
||||
- **Description:** Disable user input on browser window during automation.
|
||||
- **Default:** `true`
|
||||
|
||||
#### `context`
|
||||
|
||||
- **`context.fileName`** (string | string[]):
|
||||
@@ -755,7 +907,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
#### `tools`
|
||||
|
||||
- **`tools.sandbox`** (boolean | string):
|
||||
- **`tools.sandbox`** (string):
|
||||
- **Description:** Sandbox execution environment. Set to a boolean to enable
|
||||
or disable the sandbox, provide a string path to a sandbox profile, or
|
||||
specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
|
||||
@@ -872,6 +1024,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
confirmation dialogs.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.autoAddToPolicyByDefault`** (boolean):
|
||||
- **Description:** When enabled, the "Allow for all future sessions" option
|
||||
becomes the default choice for low-risk tools in trusted workspaces.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.blockGitExtensions`** (boolean):
|
||||
- **Description:** Blocks installing and loading extensions from Git.
|
||||
- **Default:** `false`
|
||||
@@ -998,6 +1155,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionRegistryURI`** (string):
|
||||
- **Description:** The URI (web URL or local file path) of the extension
|
||||
registry.
|
||||
- **Default:** `"https://geminicli.com/extensions.json"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionReloading`** (boolean):
|
||||
- **Description:** Enables extension loading/unloading within the CLI session.
|
||||
- **Default:** `false`
|
||||
@@ -1040,6 +1203,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.dynamicModelConfiguration`** (boolean):
|
||||
- **Description:** Enable dynamic model configuration (definitions,
|
||||
resolutions, and chains) via settings.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.enabled`** (boolean):
|
||||
- **Description:** Enable the Gemma Model Router (experimental). Requires a
|
||||
local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
@@ -1171,13 +1340,20 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
Configures connections to one or more Model-Context Protocol (MCP) servers for
|
||||
discovering and using custom tools. Gemini CLI attempts to connect to each
|
||||
configured MCP server to discover available tools. If multiple MCP servers
|
||||
expose a tool with the same name, the tool names will be prefixed with the
|
||||
server alias you defined in the configuration (e.g.,
|
||||
`serverAlias__actualToolName`) to avoid conflicts. Note that the system might
|
||||
strip certain schema properties from MCP tool definitions for compatibility. At
|
||||
least one of `command`, `url`, or `httpUrl` must be provided. If multiple are
|
||||
specified, the order of precedence is `httpUrl`, then `url`, then `command`.
|
||||
configured MCP server to discover available tools. Every discovered tool is
|
||||
prepended with the `mcp_` prefix and its server alias to form a fully qualified
|
||||
name (FQN) (e.g., `mcp_serverAlias_actualToolName`) to avoid conflicts. Note
|
||||
that the system might strip certain schema properties from MCP tool definitions
|
||||
for compatibility. At least one of `command`, `url`, or `httpUrl` must be
|
||||
provided. If multiple are specified, the order of precedence is `httpUrl`, then
|
||||
`url`, then `command`.
|
||||
|
||||
> **Warning:** Avoid using underscores (`_`) in your server aliases (e.g., use
|
||||
> `my-server` instead of `my_server`). The underlying policy engine parses Fully
|
||||
> Qualified Names (`mcp_server_tool`) using the first underscore after the
|
||||
> `mcp_` prefix. An underscore in your server alias will cause the parser to
|
||||
> misidentify the server name, which can cause security policies to fail
|
||||
> silently.
|
||||
|
||||
- **`mcpServers.<SERVER_NAME>`** (object): The server parameters for the named
|
||||
server.
|
||||
@@ -1358,6 +1534,13 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- Useful for shared compute environments or keeping CLI state isolated.
|
||||
- Example: `export GEMINI_CLI_HOME="/path/to/user/config"` (Windows
|
||||
PowerShell: `$env:GEMINI_CLI_HOME="C:\path\to\user\config"`)
|
||||
- **`GEMINI_CLI_SURFACE`**:
|
||||
- Specifies a custom label to include in the `User-Agent` header for API
|
||||
traffic reporting.
|
||||
- This is useful for tracking specific internal tools or distribution
|
||||
channels.
|
||||
- Example: `export GEMINI_CLI_SURFACE="my-custom-tool"` (Windows PowerShell:
|
||||
`$env:GEMINI_CLI_SURFACE="my-custom-tool"`)
|
||||
- **`GOOGLE_API_KEY`**:
|
||||
- Your Google Cloud API key.
|
||||
- Required for using Vertex AI in express mode.
|
||||
|
||||
@@ -8,122 +8,201 @@ available combinations.
|
||||
|
||||
#### Basic Controls
|
||||
|
||||
| Action | Keys |
|
||||
| --------------------------------------------------------------- | ------------------- |
|
||||
| Confirm the current selection or choice. | `Enter` |
|
||||
| Dismiss dialogs or cancel the current focus. | `Esc`<br />`Ctrl+[` |
|
||||
| Cancel the current request or quit the CLI when input is empty. | `Ctrl+C` |
|
||||
| Exit the CLI when the input buffer is empty. | `Ctrl+D` |
|
||||
| Command | Action | Keys |
|
||||
| --------------- | --------------------------------------------------------------- | ------------------- |
|
||||
| `basic.confirm` | Confirm the current selection or choice. | `Enter` |
|
||||
| `basic.cancel` | Dismiss dialogs or cancel the current focus. | `Esc`<br />`Ctrl+[` |
|
||||
| `basic.quit` | Cancel the current request or quit the CLI when input is empty. | `Ctrl+C` |
|
||||
| `basic.exit` | Exit the CLI when the input buffer is empty. | `Ctrl+D` |
|
||||
|
||||
#### Cursor Movement
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------- | ------------------------------------------ |
|
||||
| Move the cursor to the start of the line. | `Ctrl+A`<br />`Home` |
|
||||
| Move the cursor to the end of the line. | `Ctrl+E`<br />`End` |
|
||||
| Move the cursor up one line. | `Up` |
|
||||
| Move the cursor down one line. | `Down` |
|
||||
| Move the cursor one character to the left. | `Left` |
|
||||
| Move the cursor one character to the right. | `Right`<br />`Ctrl+F` |
|
||||
| Move the cursor one word to the left. | `Ctrl+Left`<br />`Alt+Left`<br />`Alt+B` |
|
||||
| Move the cursor one word to the right. | `Ctrl+Right`<br />`Alt+Right`<br />`Alt+F` |
|
||||
| Command | Action | Keys |
|
||||
| ------------------ | ------------------------------------------- | ------------------------------------------ |
|
||||
| `cursor.home` | Move the cursor to the start of the line. | `Ctrl+A`<br />`Home` |
|
||||
| `cursor.end` | Move the cursor to the end of the line. | `Ctrl+E`<br />`End` |
|
||||
| `cursor.up` | Move the cursor up one line. | `Up` |
|
||||
| `cursor.down` | Move the cursor down one line. | `Down` |
|
||||
| `cursor.left` | Move the cursor one character to the left. | `Left` |
|
||||
| `cursor.right` | Move the cursor one character to the right. | `Right`<br />`Ctrl+F` |
|
||||
| `cursor.wordLeft` | Move the cursor one word to the left. | `Ctrl+Left`<br />`Alt+Left`<br />`Alt+B` |
|
||||
| `cursor.wordRight` | Move the cursor one word to the right. | `Ctrl+Right`<br />`Alt+Right`<br />`Alt+F` |
|
||||
|
||||
#### Editing
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | -------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl+K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl+U` |
|
||||
| Clear all text in the input field. | `Ctrl+C` |
|
||||
| Delete the previous word. | `Ctrl+Backspace`<br />`Alt+Backspace`<br />`Ctrl+W` |
|
||||
| Delete the next word. | `Ctrl+Delete`<br />`Alt+Delete`<br />`Alt+D` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl+H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl+D` |
|
||||
| Undo the most recent text edit. | `Cmd/Win+Z`<br />`Alt+Z` |
|
||||
| Redo the most recent undone text edit. | `Ctrl+Shift+Z`<br />`Shift+Cmd/Win+Z`<br />`Alt+Shift+Z` |
|
||||
| Command | Action | Keys |
|
||||
| ---------------------- | ------------------------------------------------ | -------------------------------------------------------- |
|
||||
| `edit.deleteRightAll` | Delete from the cursor to the end of the line. | `Ctrl+K` |
|
||||
| `edit.deleteLeftAll` | Delete from the cursor to the start of the line. | `Ctrl+U` |
|
||||
| `edit.clear` | Clear all text in the input field. | `Ctrl+C` |
|
||||
| `edit.deleteWordLeft` | Delete the previous word. | `Ctrl+Backspace`<br />`Alt+Backspace`<br />`Ctrl+W` |
|
||||
| `edit.deleteWordRight` | Delete the next word. | `Ctrl+Delete`<br />`Alt+Delete`<br />`Alt+D` |
|
||||
| `edit.deleteLeft` | Delete the character to the left. | `Backspace`<br />`Ctrl+H` |
|
||||
| `edit.deleteRight` | Delete the character to the right. | `Delete`<br />`Ctrl+D` |
|
||||
| `edit.undo` | Undo the most recent text edit. | `Cmd/Win+Z`<br />`Alt+Z` |
|
||||
| `edit.redo` | Redo the most recent undone text edit. | `Ctrl+Shift+Z`<br />`Shift+Cmd/Win+Z`<br />`Alt+Shift+Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------ | ----------------------------- |
|
||||
| Scroll content up. | `Shift+Up` |
|
||||
| Scroll content down. | `Shift+Down` |
|
||||
| Scroll to the top. | `Ctrl+Home`<br />`Shift+Home` |
|
||||
| Scroll to the bottom. | `Ctrl+End`<br />`Shift+End` |
|
||||
| Scroll up by one page. | `Page Up` |
|
||||
| Scroll down by one page. | `Page Down` |
|
||||
| Command | Action | Keys |
|
||||
| ----------------- | ------------------------ | ----------------------------- |
|
||||
| `scroll.up` | Scroll content up. | `Shift+Up` |
|
||||
| `scroll.down` | Scroll content down. | `Shift+Down` |
|
||||
| `scroll.home` | Scroll to the top. | `Ctrl+Home`<br />`Shift+Home` |
|
||||
| `scroll.end` | Scroll to the bottom. | `Ctrl+End`<br />`Shift+End` |
|
||||
| `scroll.pageUp` | Scroll up by one page. | `Page Up` |
|
||||
| `scroll.pageDown` | Scroll down by one page. | `Page Down` |
|
||||
|
||||
#### History & Search
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------- | ------------ |
|
||||
| Show the previous entry in history. | `Ctrl+P` |
|
||||
| Show the next entry in history. | `Ctrl+N` |
|
||||
| Start reverse search through history. | `Ctrl+R` |
|
||||
| Submit the selected reverse-search match. | `Enter` |
|
||||
| Accept a suggestion while reverse searching. | `Tab` |
|
||||
| Browse and rewind previous interactions. | `Double Esc` |
|
||||
| Command | Action | Keys |
|
||||
| ----------------------- | -------------------------------------------- | -------- |
|
||||
| `history.previous` | Show the previous entry in history. | `Ctrl+P` |
|
||||
| `history.next` | Show the next entry in history. | `Ctrl+N` |
|
||||
| `history.search.start` | Start reverse search through history. | `Ctrl+R` |
|
||||
| `history.search.submit` | Submit the selected reverse-search match. | `Enter` |
|
||||
| `history.search.accept` | Accept a suggestion while reverse searching. | `Tab` |
|
||||
|
||||
#### Navigation
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------------- | --------------- |
|
||||
| Move selection up in lists. | `Up` |
|
||||
| Move selection down in lists. | `Down` |
|
||||
| Move up within dialog options. | `Up`<br />`K` |
|
||||
| Move down within dialog options. | `Down`<br />`J` |
|
||||
| Move to the next item or question in a dialog. | `Tab` |
|
||||
| Move to the previous item or question in a dialog. | `Shift+Tab` |
|
||||
| Command | Action | Keys |
|
||||
| --------------------- | -------------------------------------------------- | --------------- |
|
||||
| `nav.up` | Move selection up in lists. | `Up` |
|
||||
| `nav.down` | Move selection down in lists. | `Down` |
|
||||
| `nav.dialog.up` | Move up within dialog options. | `Up`<br />`K` |
|
||||
| `nav.dialog.down` | Move down within dialog options. | `Down`<br />`J` |
|
||||
| `nav.dialog.next` | Move to the next item or question in a dialog. | `Tab` |
|
||||
| `nav.dialog.previous` | Move to the previous item or question in a dialog. | `Shift+Tab` |
|
||||
|
||||
#### Suggestions & Completions
|
||||
|
||||
| Action | Keys |
|
||||
| --------------------------------------- | -------------------- |
|
||||
| Accept the inline suggestion. | `Tab`<br />`Enter` |
|
||||
| Move to the previous completion option. | `Up`<br />`Ctrl+P` |
|
||||
| Move to the next completion option. | `Down`<br />`Ctrl+N` |
|
||||
| Expand an inline suggestion. | `Right` |
|
||||
| Collapse an inline suggestion. | `Left` |
|
||||
| Command | Action | Keys |
|
||||
| ----------------------- | --------------------------------------- | -------------------- |
|
||||
| `suggest.accept` | Accept the inline suggestion. | `Tab`<br />`Enter` |
|
||||
| `suggest.focusPrevious` | Move to the previous completion option. | `Up`<br />`Ctrl+P` |
|
||||
| `suggest.focusNext` | Move to the next completion option. | `Down`<br />`Ctrl+N` |
|
||||
| `suggest.expand` | Expand an inline suggestion. | `Right` |
|
||||
| `suggest.collapse` | Collapse an inline suggestion. | `Left` |
|
||||
|
||||
#### Text Input
|
||||
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter` |
|
||||
| Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
|
||||
| Open the current prompt or the plan in an external editor. | `Ctrl+X` |
|
||||
| Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
|
||||
| Command | Action | Keys |
|
||||
| -------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `input.submit` | Submit the current prompt. | `Enter` |
|
||||
| `input.newline` | Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
|
||||
| `input.openExternalEditor` | Open the current prompt or the plan in an external editor. | `Ctrl+X` |
|
||||
| `input.paste` | Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
| Action | Keys |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| Toggle detailed error information. | `F12` |
|
||||
| Toggle the full TODO list. | `Ctrl+T` |
|
||||
| Show IDE context details. | `Ctrl+G` |
|
||||
| Toggle Markdown rendering. | `Alt+M` |
|
||||
| Toggle copy mode when in alternate buffer mode. | `Ctrl+S` |
|
||||
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` |
|
||||
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` |
|
||||
| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` |
|
||||
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl+O` |
|
||||
| Toggle current background shell visibility. | `Ctrl+B` |
|
||||
| Toggle background shell list. | `Ctrl+L` |
|
||||
| Kill the active background shell. | `Ctrl+K` |
|
||||
| Confirm selection in background shell list. | `Enter` |
|
||||
| Dismiss background shell list. | `Esc` |
|
||||
| Move focus from background shell to Gemini. | `Shift+Tab` |
|
||||
| Move focus from background shell list to Gemini. | `Tab` |
|
||||
| Show warning when trying to move focus away from background shell. | `Tab` |
|
||||
| Show warning when trying to move focus away from shell input. | `Tab` |
|
||||
| Move focus from Gemini to the active shell. | `Tab` |
|
||||
| Move focus from the shell back to Gemini. | `Shift+Tab` |
|
||||
| Clear the terminal screen and redraw the UI. | `Ctrl+L` |
|
||||
| Restart the application. | `R`<br />`Shift+R` |
|
||||
| Suspend the CLI and move it to the background. | `Ctrl+Z` |
|
||||
| Command | Action | Keys |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| `app.showErrorDetails` | Toggle detailed error information. | `F12` |
|
||||
| `app.showFullTodos` | Toggle the full TODO list. | `Ctrl+T` |
|
||||
| `app.showIdeContextDetail` | Show IDE context details. | `Ctrl+G` |
|
||||
| `app.toggleMarkdown` | Toggle Markdown rendering. | `Alt+M` |
|
||||
| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `Ctrl+S` |
|
||||
| `app.toggleYolo` | Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` |
|
||||
| `app.cycleApprovalMode` | Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` |
|
||||
| `app.showMoreLines` | Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` |
|
||||
| `app.expandPaste` | Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl+O` |
|
||||
| `app.focusShellInput` | Move focus from Gemini to the active shell. | `Tab` |
|
||||
| `app.unfocusShellInput` | Move focus from the shell back to Gemini. | `Shift+Tab` |
|
||||
| `app.clearScreen` | Clear the terminal screen and redraw the UI. | `Ctrl+L` |
|
||||
| `app.restart` | Restart the application. | `R`<br />`Shift+R` |
|
||||
| `app.suspend` | Suspend the CLI and move it to the background. | `Ctrl+Z` |
|
||||
| `app.showShellUnfocusWarning` | Show warning when trying to move focus away from shell input. | `Tab` |
|
||||
|
||||
#### Background Shell Controls
|
||||
|
||||
| Command | Action | Keys |
|
||||
| --------------------------- | ------------------------------------------------------------------ | ----------- |
|
||||
| `background.escape` | Dismiss background shell list. | `Esc` |
|
||||
| `background.select` | Confirm selection in background shell list. | `Enter` |
|
||||
| `background.toggle` | Toggle current background shell visibility. | `Ctrl+B` |
|
||||
| `background.toggleList` | Toggle background shell list. | `Ctrl+L` |
|
||||
| `background.kill` | Kill the active background shell. | `Ctrl+K` |
|
||||
| `background.unfocus` | Move focus from background shell to Gemini. | `Shift+Tab` |
|
||||
| `background.unfocusList` | Move focus from background shell list to Gemini. | `Tab` |
|
||||
| `background.unfocusWarning` | Show warning when trying to move focus away from background shell. | `Tab` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
## Customizing Keybindings
|
||||
|
||||
You can add alternative keybindings or remove default keybindings by creating a
|
||||
`keybindings.json` file in your home gemini directory (typically
|
||||
`~/.gemini/keybindings.json`).
|
||||
|
||||
### Configuration Format
|
||||
|
||||
The configuration uses a JSON array of objects, similar to VS Code's keybinding
|
||||
schema. Each object must specify a `command` from the reference tables above and
|
||||
a `key` combination.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"command": "edit.clear",
|
||||
"key": "cmd+l"
|
||||
},
|
||||
{
|
||||
// prefix "-" to unbind a key
|
||||
"command": "-app.toggleYolo",
|
||||
"key": "ctrl+y"
|
||||
},
|
||||
{
|
||||
"command": "input.submit",
|
||||
"key": "ctrl+y"
|
||||
},
|
||||
{
|
||||
// multiple modifiers
|
||||
"command": "cursor.right",
|
||||
"key": "shift+alt+a"
|
||||
},
|
||||
{
|
||||
// Some mac keyboards send "Å" instead of "shift+option+a"
|
||||
"command": "cursor.right",
|
||||
"key": "Å"
|
||||
},
|
||||
{
|
||||
// some base keys have special multi-char names
|
||||
"command": "cursor.right",
|
||||
"key": "shift+pageup"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- **Unbinding** To remove an existing or default keybinding, prefix a minus sign
|
||||
(`-`) to the `command` name.
|
||||
- **No Auto-unbinding** The same key can be bound to multiple commands in
|
||||
different contexts at the same time. Therefore, creating a binding does not
|
||||
automatically unbind the key from other commands.
|
||||
- **Explicit Modifiers**: Key matching is explicit. For example, a binding for
|
||||
`ctrl+f` will only trigger on exactly `ctrl+f`, not `ctrl+shift+f` or
|
||||
`alt+ctrl+f`.
|
||||
- **Literal Characters**: Terminals often translate complex key combinations
|
||||
(especially on macOS with the `Option` key) into special characters, losing
|
||||
modifier and keystroke information along the way. For example,`shift+5` might
|
||||
be sent as `%`. In these cases, you must bind to the literal character `%` as
|
||||
bindings to `shift+5` will never fire. To see precisely what is being sent,
|
||||
enable `Debug Keystroke Logging` and hit f12 to open the debug log console.
|
||||
- **Key Modifiers**: The supported key modifiers are:
|
||||
- `ctrl`
|
||||
- `shift`,
|
||||
- `alt` (synonyms: `opt`, `option`)
|
||||
- `cmd` (synonym: `meta`)
|
||||
- **Base Key**: The base key can be any single unicode code point or any of the
|
||||
following special keys:
|
||||
- **Navigation**: `up`, `down`, `left`, `right`, `home`, `end`, `pageup`,
|
||||
`pagedown`
|
||||
- **Actions**: `enter`, `escape`, `tab`, `space`, `backspace`, `delete`,
|
||||
`clear`, `insert`, `printscreen`
|
||||
- **Toggles**: `capslock`, `numlock`, `scrolllock`, `pausebreak`
|
||||
- **Function Keys**: `f1` through `f35`
|
||||
- **Numpad**: `numpad0` through `numpad9`, `numpad_add`, `numpad_subtract`,
|
||||
`numpad_multiply`, `numpad_divide`, `numpad_decimal`, `numpad_separator`
|
||||
|
||||
## Additional context-specific shortcuts
|
||||
|
||||
- `Option+B/F/M` (macOS only): Are interpreted as `Cmd+B/F/M` even if your
|
||||
@@ -150,6 +229,9 @@ available combinations.
|
||||
the numbered radio option and confirm when the full number is entered.
|
||||
- `Ctrl + O`: Expand or collapse paste placeholders (`[Pasted Text: X lines]`)
|
||||
inline when the cursor is over the placeholder.
|
||||
- `Ctrl + X` (while a plan is presented): Open the plan in an external editor to
|
||||
[collaboratively edit or comment](../cli/plan-mode.md#collaborative-plan-editing)
|
||||
on the implementation strategy.
|
||||
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
|
||||
view full content inline. Double-click again to collapse.
|
||||
|
||||
|
||||
@@ -76,9 +76,13 @@ The `toolName` in the rule must match the name of the tool being called.
|
||||
|
||||
- **Wildcards**: You can use wildcards to match multiple tools.
|
||||
- `*`: Matches **any tool** (built-in or MCP).
|
||||
- `server__*`: Matches any tool from a specific MCP server.
|
||||
- `*__toolName`: Matches a specific tool name across **all** MCP servers.
|
||||
- `*__*`: Matches **any tool from any MCP server**.
|
||||
- `mcp_server_*`: Matches any tool from a specific MCP server.
|
||||
- `mcp_*_toolName`: Matches a specific tool name across **all** MCP servers.
|
||||
- `mcp_*`: Matches **any tool from any MCP server**.
|
||||
|
||||
> **Recommendation:** While FQN wildcards are supported, the recommended
|
||||
> approach for MCP tools is to use the `mcpName` field in your TOML rules. See
|
||||
> [Special syntax for MCP tools](#special-syntax-for-mcp-tools).
|
||||
|
||||
#### Arguments pattern
|
||||
|
||||
@@ -164,8 +168,8 @@ A rule matches a tool call if all of its conditions are met:
|
||||
|
||||
1. **Tool name**: The `toolName` in the rule must match the name of the tool
|
||||
being called.
|
||||
- **Wildcards**: You can use wildcards like `*`, `server__*`, or
|
||||
`*__toolName` to match multiple tools. See [Tool Name](#tool-name) for
|
||||
- **Wildcards**: You can use wildcards like `*`, `mcp_server_*`, or
|
||||
`mcp_*_toolName` to match multiple tools. See [Tool Name](#tool-name) for
|
||||
details.
|
||||
2. **Arguments pattern**: If `argsPattern` is specified, the tool's arguments
|
||||
are converted to a stable JSON string, which is then tested against the
|
||||
@@ -187,9 +191,13 @@ User, and (if configured) Admin directories.
|
||||
|
||||
#### System-wide policies (Admin)
|
||||
|
||||
Administrators can enforce system-wide policies (Tier 3) that override all user
|
||||
and default settings. These policies must be placed in specific, secure
|
||||
directories:
|
||||
Administrators can enforce system-wide policies (Tier 4) that override all user
|
||||
and default settings. These policies can be loaded from standard system
|
||||
locations or supplemental paths.
|
||||
|
||||
##### Standard Locations
|
||||
|
||||
These are the default paths the CLI searches for admin policies:
|
||||
|
||||
| OS | Policy Directory Path |
|
||||
| :---------- | :------------------------------------------------ |
|
||||
@@ -197,10 +205,25 @@ directories:
|
||||
| **macOS** | `/Library/Application Support/GeminiCli/policies` |
|
||||
| **Windows** | `C:\ProgramData\gemini-cli\policies` |
|
||||
|
||||
**Security Requirements:**
|
||||
##### Supplemental Admin Policies
|
||||
|
||||
To prevent privilege escalation, the CLI enforces strict security checks on
|
||||
admin directories. If checks fail, system policies are **ignored**.
|
||||
Administrators can also specify supplemental policy paths using:
|
||||
|
||||
- The `--admin-policy` command-line flag.
|
||||
- The `adminPolicyPaths` setting in a system settings file.
|
||||
|
||||
These supplemental policies are assigned the same **Admin** tier (Base 4) as
|
||||
policies in standard locations.
|
||||
|
||||
**Security Guard**: Supplemental admin policies are **ignored** if any `.toml`
|
||||
policy files are found in the standard system location. This prevents flag-based
|
||||
overrides when a central system policy has already been established.
|
||||
|
||||
#### Security Requirements
|
||||
|
||||
To prevent privilege escalation, the CLI enforces strict security checks on the
|
||||
**standard system policy directory**. If checks fail, the policies in that
|
||||
directory are **ignored**.
|
||||
|
||||
- **Linux / macOS:** Must be owned by `root` (UID 0) and NOT writable by group
|
||||
or others (e.g., `chmod 755`).
|
||||
@@ -210,6 +233,11 @@ admin directories. If checks fail, system policies are **ignored**.
|
||||
for non-admin groups. You may need to "Disable inheritance" in Advanced
|
||||
Security Settings._
|
||||
|
||||
**Note:** Supplemental admin policies (provided via `--admin-policy` or
|
||||
`adminPolicyPaths` settings) are **NOT** subject to these strict ownership
|
||||
checks, as they are explicitly provided by the user or administrator in their
|
||||
current execution context.
|
||||
|
||||
### TOML rule schema
|
||||
|
||||
Here is a breakdown of the fields available in a TOML policy rule:
|
||||
@@ -224,7 +252,7 @@ toolName = "run_shell_command"
|
||||
subagent = "generalist"
|
||||
|
||||
# (Optional) The name of an MCP server. Can be combined with toolName
|
||||
# to form a composite name like "mcpName__toolName".
|
||||
# to form a composite FQN internally like "mcp_mcpName_toolName".
|
||||
mcpName = "my-custom-server"
|
||||
|
||||
# (Optional) Metadata hints provided by the tool. A rule matches if all
|
||||
@@ -301,11 +329,22 @@ priority = 100
|
||||
### Special syntax for MCP tools
|
||||
|
||||
You can create rules that target tools from Model Context Protocol (MCP) servers
|
||||
using the `mcpName` field or composite wildcard patterns.
|
||||
using the `mcpName` field. **This is the recommended approach** for defining MCP
|
||||
policies, as it is much more robust than manually writing Fully Qualified Names
|
||||
(FQNs) or string wildcards.
|
||||
|
||||
> **Warning:** Do not use underscores (`_`) in your MCP server names (e.g., use
|
||||
> `my-server` rather than `my_server`). The policy parser splits Fully Qualified
|
||||
> Names (`mcp_server_tool`) on the _first_ underscore following the `mcp_`
|
||||
> prefix. If your server name contains an underscore, the parser will
|
||||
> misinterpret the server identity, which can cause wildcard rules and security
|
||||
> policies to fail silently.
|
||||
|
||||
**1. Targeting a specific tool on a server**
|
||||
|
||||
Combine `mcpName` and `toolName` to target a single operation.
|
||||
Combine `mcpName` and `toolName` to target a single operation. When using
|
||||
`mcpName`, the `toolName` field should strictly be the simple name of the tool
|
||||
(e.g., `search`), **not** the Fully Qualified Name (e.g., `mcp_server_search`).
|
||||
|
||||
```toml
|
||||
# Allows the `search` tool on the `my-jira-server` MCP
|
||||
|
||||
@@ -79,8 +79,8 @@ manually run through this checklist.
|
||||
- [ ] Verify version: `gemini --version`
|
||||
|
||||
- **Authentication:**
|
||||
- [ ] In interactive mode run `/auth` and verify all login flows work:
|
||||
- [ ] Login With Google
|
||||
- [ ] In interactive mode run `/auth` and verify all sign in flows work:
|
||||
- [ ] Sign in with Google
|
||||
- [ ] API Key
|
||||
- [ ] Vertex AI
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ for further information.
|
||||
| Gemini Developer API Key | Gemini API - Paid Services | [Gemini API Terms of Service - Paid Services](https://ai.google.dev/gemini-api/terms#paid-services) | [Google Privacy Policy](https://policies.google.com/privacy) |
|
||||
| Vertex AI GenAI API Key | Vertex AI GenAI API | [Google Cloud Platform Terms of Service](https://cloud.google.com/terms/service-terms/) | [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) |
|
||||
|
||||
## 1. If you have logged in with your Google account to Gemini Code Assist
|
||||
## 1. If you have signed in with your Google account to Gemini Code Assist
|
||||
|
||||
For users who use their Google account to access
|
||||
[Gemini Code Assist](https://codeassist.google), these Terms of Service and
|
||||
@@ -68,7 +68,7 @@ Code Assist Standard or Enterprise edition, the terms and privacy policy of
|
||||
Gemini Code Assist Standard or Enterprise edition will apply to all your use of
|
||||
Gemini Code Assist._
|
||||
|
||||
## 2. If you have logged in with a Gemini API key to the Gemini Developer API
|
||||
## 2. If you have signed in with a Gemini API key to the Gemini Developer API
|
||||
|
||||
If you are using a Gemini API key for authentication with the
|
||||
[Gemini Developer API](https://ai.google.dev/gemini-api/docs), these Terms of
|
||||
@@ -84,7 +84,7 @@ Service and Privacy Notice documents apply:
|
||||
- Privacy Notice: The collection and use of your data is described in the
|
||||
[Google Privacy Policy](https://policies.google.com/privacy).
|
||||
|
||||
## 3. If you have logged in with a Gemini API key to the Vertex AI GenAI API
|
||||
## 3. If you have signed in with a Gemini API key to the Vertex AI GenAI API
|
||||
|
||||
If you are using a Gemini API key for authentication with a
|
||||
[Vertex AI GenAI API](https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest)
|
||||
|
||||
@@ -29,13 +29,13 @@ topics on:
|
||||
added to your organization's Gemini Code Assist subscription.
|
||||
|
||||
- **Error:
|
||||
`Failed to login. Message: Your current account is not eligible... because it is not currently available in your location.`**
|
||||
`Failed to sign in. Message: Your current account is not eligible... because it is not currently available in your location.`**
|
||||
- **Cause:** Gemini CLI does not currently support your location. For a full
|
||||
list of supported locations, see the following pages:
|
||||
- Gemini Code Assist for individuals:
|
||||
[Available locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
|
||||
|
||||
- **Error: `Failed to login. Message: Request contains an invalid argument`**
|
||||
- **Error: `Failed to sign in. Message: Request contains an invalid argument`**
|
||||
- **Cause:** Users with Google Workspace accounts or Google Cloud accounts
|
||||
associated with their Gmail accounts may not be able to activate the free
|
||||
tier of the Google Code Assist plan.
|
||||
@@ -124,6 +124,21 @@ topics on:
|
||||
`advanced.excludedEnvVars` setting in your `settings.json` to exclude fewer
|
||||
variables.
|
||||
|
||||
- **Warning: `npm WARN deprecated node-domexception@1.0.0` or
|
||||
`npm WARN deprecated glob` during install/update**
|
||||
- **Issue:** When installing or updating the Gemini CLI globally via
|
||||
`npm install -g @google/gemini-cli` or `npm update -g @google/gemini-cli`,
|
||||
you might see deprecation warnings regarding `node-domexception` or old
|
||||
versions of `glob`.
|
||||
- **Cause:** These warnings occur because some dependencies (or their
|
||||
sub-dependencies, like `google-auth-library`) rely on older package
|
||||
versions. Since Gemini CLI requires Node.js 20 or higher, the platform's
|
||||
native features (like the native `DOMException`) are used, making these
|
||||
warnings purely informational.
|
||||
- **Solution:** These warnings are harmless and can be safely ignored. Your
|
||||
installation or update will complete successfully and function properly
|
||||
without any action required.
|
||||
|
||||
## Exit codes
|
||||
|
||||
The Gemini CLI uses specific exit codes to indicate the reason for termination.
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
"label": "Plan tasks with todos",
|
||||
"slug": "docs/cli/tutorials/task-planning"
|
||||
},
|
||||
{
|
||||
"label": "Use Plan Mode with model steering",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/tutorials/plan-mode-steering"
|
||||
},
|
||||
{
|
||||
"label": "Web search and fetch",
|
||||
"slug": "docs/cli/tutorials/web-tools"
|
||||
@@ -106,12 +111,17 @@
|
||||
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
|
||||
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
|
||||
{ "label": "Model selection", "slug": "docs/cli/model" },
|
||||
{
|
||||
"label": "Model steering",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/model-steering"
|
||||
},
|
||||
{
|
||||
"label": "Notifications",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/notifications"
|
||||
},
|
||||
{ "label": "Plan mode", "badge": "🔬", "slug": "docs/cli/plan-mode" },
|
||||
{ "label": "Plan mode", "slug": "docs/cli/plan-mode" },
|
||||
{
|
||||
"label": "Subagents",
|
||||
"badge": "🔬",
|
||||
|
||||
@@ -555,21 +555,34 @@ Upon successful connection:
|
||||
`excludeTools` configuration
|
||||
4. **Name sanitization:** Tool names are cleaned to meet Gemini API
|
||||
requirements:
|
||||
- Invalid characters (non-alphanumeric, underscore, dot, hyphen) are replaced
|
||||
with underscores
|
||||
- Characters other than letters, numbers, underscore (`_`), hyphen (`-`), dot
|
||||
(`.`), and colon (`:`) are replaced with underscores
|
||||
- Names longer than 63 characters are truncated with middle replacement
|
||||
(`___`)
|
||||
(`...`)
|
||||
|
||||
### 3. Conflict resolution
|
||||
### 3. Tool naming and namespaces
|
||||
|
||||
When multiple servers expose tools with the same name:
|
||||
To prevent collisions across multiple servers or conflicting built-in tools,
|
||||
every discovered MCP tool is assigned a strict namespace.
|
||||
|
||||
1. **First registration wins:** The first server to register a tool name gets
|
||||
the unprefixed name
|
||||
2. **Automatic prefixing:** Subsequent servers get prefixed names:
|
||||
`serverName__toolName`
|
||||
3. **Registry tracking:** The tool registry maintains mappings between server
|
||||
names and their tools
|
||||
1. **Automatic FQN:** All MCP tools are unconditionally assigned a fully
|
||||
qualified name (FQN) using the format `mcp_{serverName}_{toolName}`.
|
||||
2. **Registry tracking:** The tool registry maintains metadata mappings between
|
||||
these FQNs and their original server identities.
|
||||
3. **Overwrites:** If two servers share the exact same alias in your
|
||||
configuration and provide tools with the exact same name, the last registered
|
||||
tool overwrites the previous one.
|
||||
4. **Policies:** To configure permissions (like auto-approval or denial) for MCP
|
||||
tools, see
|
||||
[Special syntax for MCP tools](../reference/policy-engine.md#special-syntax-for-mcp-tools)
|
||||
in the Policy Engine documentation.
|
||||
|
||||
> **Warning:** Do not use underscores (`_`) in your MCP server names (e.g., use
|
||||
> `my-server` rather than `my_server`). The policy parser splits Fully Qualified
|
||||
> Names (`mcp_server_tool`) on the _first_ underscore following the `mcp_`
|
||||
> prefix. If your server name contains an underscore, the parser will
|
||||
> misinterpret the server identity, which can cause wildcard rules and security
|
||||
> policies to fail silently.
|
||||
|
||||
### 4. Schema processing
|
||||
|
||||
@@ -695,7 +708,7 @@ MCP Servers Status:
|
||||
|
||||
🐳 dockerizedServer (CONNECTED)
|
||||
Command: docker run -i --rm -e API_KEY my-mcp-server:latest
|
||||
Tools: docker__deploy, docker__status
|
||||
Tools: mcp_dockerizedServer_docker_deploy, mcp_dockerizedServer_docker_status
|
||||
|
||||
Discovery State: COMPLETED
|
||||
```
|
||||
|
||||
@@ -120,6 +120,14 @@ tools to detect if they are being run from within the Gemini CLI.
|
||||
|
||||
## Command restrictions
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!WARNING]
|
||||
> The `tools.core` setting is an **allowlist for _all_ built-in
|
||||
> tools**, not just shell commands. When you set `tools.core` to any value,
|
||||
> _only_ the tools explicitly listed will be enabled. This includes all built-in
|
||||
> tools like `read_file`, `write_file`, `glob`, `grep_search`, `list_directory`,
|
||||
> `replace`, etc.
|
||||
|
||||
You can restrict the commands that can be executed by the `run_shell_command`
|
||||
tool by using the `tools.core` and `tools.exclude` settings in your
|
||||
configuration file.
|
||||
|
||||
@@ -82,11 +82,14 @@ const commonAliases = {
|
||||
const cliConfig = {
|
||||
...baseConfig,
|
||||
banner: {
|
||||
js: `const require = (await import('node:module')).createRequire(import.meta.url); globalThis.__filename = (await import('node:url')).fileURLToPath(import.meta.url); globalThis.__dirname = (await import('node:path')).dirname(globalThis.__filename);`,
|
||||
js: `const require = (await import('node:module')).createRequire(import.meta.url); const __chunk_filename = (await import('node:url')).fileURLToPath(import.meta.url); const __chunk_dirname = (await import('node:path')).dirname(__chunk_filename);`,
|
||||
},
|
||||
entryPoints: ['packages/cli/index.ts'],
|
||||
outfile: 'bundle/gemini.js',
|
||||
entryPoints: { gemini: 'packages/cli/index.ts' },
|
||||
outdir: 'bundle',
|
||||
splitting: true,
|
||||
define: {
|
||||
__filename: '__chunk_filename',
|
||||
__dirname: '__chunk_dirname',
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
'process.env.GEMINI_SANDBOX_IMAGE_DEFAULT': JSON.stringify(
|
||||
pkg.config?.sandboxImageUri,
|
||||
@@ -103,11 +106,13 @@ const cliConfig = {
|
||||
const a2aServerConfig = {
|
||||
...baseConfig,
|
||||
banner: {
|
||||
js: `const require = (await import('node:module')).createRequire(import.meta.url); globalThis.__filename = (await import('node:url')).fileURLToPath(import.meta.url); globalThis.__dirname = (await import('node:path')).dirname(globalThis.__filename);`,
|
||||
js: `const require = (await import('node:module')).createRequire(import.meta.url); const __chunk_filename = (await import('node:url')).fileURLToPath(import.meta.url); const __chunk_dirname = (await import('node:path')).dirname(__chunk_filename);`,
|
||||
},
|
||||
entryPoints: ['packages/a2a-server/src/http/server.ts'],
|
||||
outfile: 'packages/a2a-server/dist/a2a-server.mjs',
|
||||
define: {
|
||||
__filename: '__chunk_filename',
|
||||
__dirname: '__chunk_dirname',
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
|
||||
@@ -35,11 +35,6 @@ const commonRestrictedSyntaxRules = [
|
||||
message:
|
||||
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.name="fetch"]',
|
||||
message:
|
||||
'Use safeFetch() from "@/utils/fetch" instead of the global fetch() to ensure SSRF protection. If you are implementing a custom security layer, use an eslint-disable comment and explain why.',
|
||||
},
|
||||
];
|
||||
|
||||
export default tseslint.config(
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import {
|
||||
TRACKER_CREATE_TASK_TOOL_NAME,
|
||||
TRACKER_UPDATE_TASK_TOOL_NAME,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { evalTest, assertModelHasOutput } from './test-helper.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const FILES = {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-project',
|
||||
version: '1.0.0',
|
||||
scripts: { test: 'echo "All tests passed!"' },
|
||||
}),
|
||||
'src/login.js':
|
||||
'function login(username, password) {\n if (!username) throw new Error("Missing username");\n // BUG: missing password check\n return true;\n}',
|
||||
} as const;
|
||||
|
||||
describe('tracker_mode', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should manage tasks in the tracker when explicitly requested during a bug fix',
|
||||
params: {
|
||||
settings: { experimental: { taskTracker: true } },
|
||||
},
|
||||
files: FILES,
|
||||
prompt:
|
||||
'We have a bug in src/login.js: the password check is missing. First, create a task in the tracker to fix it. Then fix the bug, and mark the task as closed.',
|
||||
assert: async (rig, result) => {
|
||||
const wasCreateCalled = await rig.waitForToolCall(
|
||||
TRACKER_CREATE_TASK_TOOL_NAME,
|
||||
);
|
||||
expect(
|
||||
wasCreateCalled,
|
||||
'Expected tracker_create_task tool to be called',
|
||||
).toBe(true);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const createCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === TRACKER_CREATE_TASK_TOOL_NAME,
|
||||
);
|
||||
expect(createCall).toBeDefined();
|
||||
const args = JSON.parse(createCall!.toolRequest.args);
|
||||
expect(
|
||||
(args.title?.toLowerCase() ?? '') +
|
||||
(args.description?.toLowerCase() ?? ''),
|
||||
).toContain('login');
|
||||
|
||||
const wasUpdateCalled = await rig.waitForToolCall(
|
||||
TRACKER_UPDATE_TASK_TOOL_NAME,
|
||||
);
|
||||
expect(
|
||||
wasUpdateCalled,
|
||||
'Expected tracker_update_task tool to be called',
|
||||
).toBe(true);
|
||||
|
||||
const updateCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === TRACKER_UPDATE_TASK_TOOL_NAME,
|
||||
);
|
||||
expect(updateCall).toBeDefined();
|
||||
const updateArgs = JSON.parse(updateCall!.toolRequest.args);
|
||||
expect(updateArgs.status).toBe('closed');
|
||||
|
||||
const loginContent = fs.readFileSync(
|
||||
path.join(rig.testDir!, 'src/login.js'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(loginContent).not.toContain('// BUG: missing password check');
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should implicitly create tasks when asked to build a feature plan',
|
||||
params: {
|
||||
settings: { experimental: { taskTracker: true } },
|
||||
},
|
||||
files: FILES,
|
||||
prompt:
|
||||
'I need to build a complex new feature for user authentication in our project. Create a detailed implementation plan and organize the work into bite-sized chunks. Do not actually implement the code yet, just plan it.',
|
||||
assert: async (rig, result) => {
|
||||
// The model should proactively use tracker_create_task to organize the work
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
TRACKER_CREATE_TASK_TOOL_NAME,
|
||||
);
|
||||
expect(
|
||||
wasToolCalled,
|
||||
'Expected tracker_create_task to be called implicitly to organize plan',
|
||||
).toBe(true);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const createCalls = toolLogs.filter(
|
||||
(log) => log.toolRequest.name === TRACKER_CREATE_TASK_TOOL_NAME,
|
||||
);
|
||||
|
||||
// We expect it to create at least one task for authentication, likely more.
|
||||
expect(createCalls.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify it didn't write any code since we asked it to just plan
|
||||
const loginContent = fs.readFileSync(
|
||||
path.join(rig.testDir!, 'src/login.js'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(loginContent).toContain('// BUG: missing password check');
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
/**
|
||||
* integration test to ensure no node.js deprecation warnings are emitted.
|
||||
* must run for all supported node versions as warnings may vary by version.
|
||||
*/
|
||||
describe('deprecation-warnings', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it.each([
|
||||
{ command: '--version', description: 'running --version' },
|
||||
{ command: '--help', description: 'running with --help' },
|
||||
])(
|
||||
'should not emit any deprecation warnings when $description',
|
||||
async ({ command, description }) => {
|
||||
await rig.setup(
|
||||
`should not emit any deprecation warnings when ${description}`,
|
||||
);
|
||||
|
||||
const { stderr, exitCode } = await rig.runWithStreams([command]);
|
||||
|
||||
// node.js deprecation warnings: (node:12345) [DEP0040] DeprecationWarning: ...
|
||||
const deprecationWarningPattern = /\[DEP\d+\].*DeprecationWarning/i;
|
||||
const hasDeprecationWarning = deprecationWarningPattern.test(stderr);
|
||||
|
||||
if (hasDeprecationWarning) {
|
||||
const deprecationMatches = stderr.match(
|
||||
/\[DEP\d+\].*DeprecationWarning:.*/gi,
|
||||
);
|
||||
const warnings = deprecationMatches
|
||||
? deprecationMatches.map((m) => m.trim()).join('\n')
|
||||
: 'Unknown deprecation warning format';
|
||||
|
||||
throw new Error(
|
||||
`Deprecation warnings detected in CLI output:\n${warnings}\n\n` +
|
||||
`Full stderr:\n${stderr}\n\n` +
|
||||
`This test ensures no deprecated Node.js modules are used. ` +
|
||||
`Please update dependencies to use non-deprecated alternatives.`,
|
||||
);
|
||||
}
|
||||
|
||||
// only check exit code if no deprecation warnings found
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`CLI exited with code ${exitCode} (expected 0). This may indicate a setup issue.\n` +
|
||||
`Stderr: ${stderr}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"run_shell_command","args":{"command":"ls -F"}}}]},"finishReason":"STOP","index":0}]},{"candidates":[{"content":{"parts":[{"text":"I ran ls -F"}]},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I ran ls -F"}]},"finishReason":"STOP","index":0}]}]}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { join } from 'node:path';
|
||||
import { TestRig, GEMINI_DIR } from './test-helper.js';
|
||||
import fs from 'node:fs';
|
||||
|
||||
describe('User Policy Regression Repro', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (rig) {
|
||||
await rig.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('should respect policies in ~/.gemini/policies/allowed-tools.toml', async () => {
|
||||
rig.setup('user-policy-test', {
|
||||
fakeResponsesPath: join(import.meta.dirname, 'user-policy.responses'),
|
||||
});
|
||||
|
||||
// Create ~/.gemini/policies/allowed-tools.toml
|
||||
const userPoliciesDir = join(rig.homeDir!, GEMINI_DIR, 'policies');
|
||||
fs.mkdirSync(userPoliciesDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
join(userPoliciesDir, 'allowed-tools.toml'),
|
||||
`
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "ls -F"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
`,
|
||||
);
|
||||
|
||||
// Run gemini with a prompt that triggers ls -F
|
||||
// approvalMode: 'default' in headless mode will DENY if it hits ASK_USER
|
||||
const result = await rig.run({
|
||||
args: ['-p', 'Run ls -F', '--model', 'gemini-3.1-pro-preview'],
|
||||
approvalMode: 'default',
|
||||
});
|
||||
|
||||
expect(result).toContain('I ran ls -F');
|
||||
expect(result).not.toContain('Tool execution denied by policy');
|
||||
expect(result).not.toContain('Tool "run_shell_command" not found');
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const lsLog = toolLogs.find(
|
||||
(l) =>
|
||||
l.toolRequest.name === 'run_shell_command' &&
|
||||
l.toolRequest.args.includes('ls -F'),
|
||||
);
|
||||
expect(lsLog).toBeDefined();
|
||||
expect(lsLog?.toolRequest.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should FAIL if policy is not present (sanity check)', async () => {
|
||||
rig.setup('user-policy-sanity-check', {
|
||||
fakeResponsesPath: join(import.meta.dirname, 'user-policy.responses'),
|
||||
});
|
||||
|
||||
// DO NOT create the policy file here
|
||||
|
||||
// Run gemini with a prompt that triggers ls -F
|
||||
const result = await rig.run({
|
||||
args: ['-p', 'Run ls -F', '--model', 'gemini-3.1-pro-preview'],
|
||||
approvalMode: 'default',
|
||||
});
|
||||
|
||||
// In non-interactive mode, it should be denied
|
||||
expect(result).toContain('Tool "run_shell_command" not found');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260304.28af4e127",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"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.34.0-nightly.20260304.28af4e127"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260311.657f19c1f"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.34.0-nightly.20260304.28af4e127",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"express": "^5.1.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/genai": "^1.30.0",
|
||||
"@google/genai": "1.30.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/supertest": "^6.0.3",
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { CoderAgentExecutor } from './executor.js';
|
||||
import type {
|
||||
ExecutionEventBus,
|
||||
RequestContext,
|
||||
TaskStore,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { requestStorage } from '../http/requestStorage.js';
|
||||
|
||||
// Mocks for constructor dependencies
|
||||
vi.mock('../config/config.js', () => ({
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
getSessionId: () => 'test-session',
|
||||
getTargetDir: () => '/tmp',
|
||||
getCheckpointingEnabled: () => false,
|
||||
}),
|
||||
loadEnvironment: vi.fn(),
|
||||
setTargetDir: vi.fn().mockReturnValue('/tmp'),
|
||||
}));
|
||||
|
||||
vi.mock('../config/settings.js', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
vi.mock('../config/extension.js', () => ({
|
||||
loadExtensions: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
|
||||
vi.mock('../http/requestStorage.js', () => ({
|
||||
requestStorage: {
|
||||
getStore: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./task.js', () => {
|
||||
const mockTaskInstance = (taskId: string, contextId: string) => ({
|
||||
id: taskId,
|
||||
contextId,
|
||||
taskState: 'working',
|
||||
acceptUserMessage: vi
|
||||
.fn()
|
||||
.mockImplementation(async function* (context, aborted) {
|
||||
const isConfirmation = (
|
||||
context.userMessage.parts as Array<{ kind: string }>
|
||||
).some((p) => p.kind === 'confirmation');
|
||||
// Hang only for main user messages (text), allow confirmations to finish quickly
|
||||
if (!isConfirmation && aborted) {
|
||||
await new Promise((resolve) => {
|
||||
aborted.addEventListener('abort', resolve, { once: true });
|
||||
});
|
||||
}
|
||||
yield { type: 'content', value: 'hello' };
|
||||
}),
|
||||
acceptAgentMessage: vi.fn().mockResolvedValue(undefined),
|
||||
scheduleToolCalls: vi.fn().mockResolvedValue(undefined),
|
||||
waitForPendingTools: vi.fn().mockResolvedValue(undefined),
|
||||
getAndClearCompletedTools: vi.fn().mockReturnValue([]),
|
||||
addToolResponsesToHistory: vi.fn(),
|
||||
sendCompletedToolsToLlm: vi.fn().mockImplementation(async function* () {}),
|
||||
cancelPendingTools: vi.fn(),
|
||||
setTaskStateAndPublishUpdate: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
getMetadata: vi.fn().mockResolvedValue({}),
|
||||
geminiClient: {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
toSDKTask: () => ({
|
||||
id: taskId,
|
||||
contextId,
|
||||
kind: 'task',
|
||||
status: { state: 'working', timestamp: new Date().toISOString() },
|
||||
metadata: {},
|
||||
history: [],
|
||||
artifacts: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const MockTask = vi.fn().mockImplementation(mockTaskInstance);
|
||||
(MockTask as unknown as { create: Mock }).create = vi
|
||||
.fn()
|
||||
.mockImplementation(async (taskId: string, contextId: string) =>
|
||||
mockTaskInstance(taskId, contextId),
|
||||
);
|
||||
|
||||
return { Task: MockTask };
|
||||
});
|
||||
|
||||
describe('CoderAgentExecutor', () => {
|
||||
let executor: CoderAgentExecutor;
|
||||
let mockTaskStore: TaskStore;
|
||||
let mockEventBus: ExecutionEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockTaskStore = {
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
list: vi.fn().mockResolvedValue([]),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockEventBus = new EventEmitter() as unknown as ExecutionEventBus;
|
||||
mockEventBus.publish = vi.fn();
|
||||
mockEventBus.finished = vi.fn();
|
||||
|
||||
executor = new CoderAgentExecutor(mockTaskStore);
|
||||
});
|
||||
|
||||
it('should distinguish between primary and secondary execution', async () => {
|
||||
const taskId = 'test-task';
|
||||
const contextId = 'test-context';
|
||||
|
||||
const mockSocket = new EventEmitter();
|
||||
const requestContext = {
|
||||
userMessage: {
|
||||
messageId: 'msg-1',
|
||||
taskId,
|
||||
contextId,
|
||||
parts: [{ kind: 'text', text: 'hi' }],
|
||||
metadata: {
|
||||
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
||||
},
|
||||
},
|
||||
} as unknown as RequestContext;
|
||||
|
||||
// Mock requestStorage for primary
|
||||
(requestStorage.getStore as Mock).mockReturnValue({
|
||||
req: { socket: mockSocket },
|
||||
});
|
||||
|
||||
// First execution (Primary)
|
||||
const primaryPromise = executor.execute(requestContext, mockEventBus);
|
||||
|
||||
// Give it enough time to reach line 490 in executor.ts
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(true);
|
||||
const wrapper = executor.getTask(taskId);
|
||||
expect(wrapper).toBeDefined();
|
||||
|
||||
// Mock requestStorage for secondary
|
||||
const secondarySocket = new EventEmitter();
|
||||
(requestStorage.getStore as Mock).mockReturnValue({
|
||||
req: { socket: secondarySocket },
|
||||
});
|
||||
|
||||
const secondaryRequestContext = {
|
||||
userMessage: {
|
||||
messageId: 'msg-2',
|
||||
taskId,
|
||||
contextId,
|
||||
parts: [{ kind: 'confirmation', callId: '1', outcome: 'proceed' }],
|
||||
metadata: {
|
||||
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
||||
},
|
||||
},
|
||||
} as unknown as RequestContext;
|
||||
|
||||
const secondaryPromise = executor.execute(
|
||||
secondaryRequestContext,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
// Secondary execution should NOT add to executingTasks (already there)
|
||||
// and should return early after its loop
|
||||
await secondaryPromise;
|
||||
|
||||
// Task should still be in executingTasks and NOT disposed
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(true);
|
||||
expect(wrapper?.task.dispose).not.toHaveBeenCalled();
|
||||
|
||||
// Now simulate secondary socket closure - it should NOT affect primary
|
||||
secondarySocket.emit('end');
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(true);
|
||||
expect(wrapper?.task.dispose).not.toHaveBeenCalled();
|
||||
|
||||
// Set to terminal state to verify disposal on finish
|
||||
wrapper!.task.taskState = 'completed';
|
||||
|
||||
// Now close primary socket
|
||||
mockSocket.emit('end');
|
||||
|
||||
await primaryPromise;
|
||||
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(false);
|
||||
expect(wrapper?.task.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should evict task from cache when it reaches terminal state', async () => {
|
||||
const taskId = 'test-task-terminal';
|
||||
const contextId = 'test-context';
|
||||
|
||||
const mockSocket = new EventEmitter();
|
||||
(requestStorage.getStore as Mock).mockReturnValue({
|
||||
req: { socket: mockSocket },
|
||||
});
|
||||
|
||||
const requestContext = {
|
||||
userMessage: {
|
||||
messageId: 'msg-1',
|
||||
taskId,
|
||||
contextId,
|
||||
parts: [{ kind: 'text', text: 'hi' }],
|
||||
metadata: {
|
||||
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
||||
},
|
||||
},
|
||||
} as unknown as RequestContext;
|
||||
|
||||
const primaryPromise = executor.execute(requestContext, mockEventBus);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const wrapper = executor.getTask(taskId)!;
|
||||
expect(wrapper).toBeDefined();
|
||||
// Simulate terminal state
|
||||
wrapper.task.taskState = 'completed';
|
||||
|
||||
// Finish primary execution
|
||||
mockSocket.emit('end');
|
||||
await primaryPromise;
|
||||
|
||||
expect(executor.getTask(taskId)).toBeUndefined();
|
||||
expect(wrapper.task.dispose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -252,6 +252,10 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
);
|
||||
await this.taskStore?.save(wrapper.toSDKTask());
|
||||
logger.info(`[CoderAgentExecutor] Task ${taskId} state CANCELED saved.`);
|
||||
|
||||
// Cleanup listener subscriptions to avoid memory leaks.
|
||||
wrapper.task.dispose();
|
||||
this.tasks.delete(taskId);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
@@ -320,23 +324,26 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
if (store) {
|
||||
// Grab the raw socket from the request object
|
||||
const socket = store.req.socket;
|
||||
const onClientEnd = () => {
|
||||
const onSocketEnd = () => {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Client socket closed for task ${taskId}. Cancelling execution.`,
|
||||
`[CoderAgentExecutor] Socket ended for message ${userMessage.messageId} (task ${taskId}). Aborting execution loop.`,
|
||||
);
|
||||
if (!abortController.signal.aborted) {
|
||||
abortController.abort();
|
||||
}
|
||||
// Clean up the listener to prevent memory leaks
|
||||
socket.removeListener('close', onClientEnd);
|
||||
socket.removeListener('end', onSocketEnd);
|
||||
};
|
||||
|
||||
// Listen on the socket's 'end' event (remote closed the connection)
|
||||
socket.on('end', onClientEnd);
|
||||
socket.on('end', onSocketEnd);
|
||||
socket.once('close', () => {
|
||||
socket.removeListener('end', onSocketEnd);
|
||||
});
|
||||
|
||||
// It's also good practice to remove the listener if the task completes successfully
|
||||
abortSignal.addEventListener('abort', () => {
|
||||
socket.removeListener('end', onClientEnd);
|
||||
socket.removeListener('end', onSocketEnd);
|
||||
});
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Socket close handler set up for task ${taskId}.`,
|
||||
@@ -457,6 +464,26 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the primary/initial execution for this task
|
||||
const isPrimaryExecution = !this.executingTasks.has(taskId);
|
||||
|
||||
if (!isPrimaryExecution) {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Primary execution already active for task ${taskId}. Starting secondary loop for message ${userMessage.messageId}.`,
|
||||
);
|
||||
currentTask.eventBus = eventBus;
|
||||
for await (const _ of currentTask.acceptUserMessage(
|
||||
requestContext,
|
||||
abortController.signal,
|
||||
)) {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Processing user message ${userMessage.messageId} in secondary execution loop for task ${taskId}.`,
|
||||
);
|
||||
}
|
||||
// End this execution-- the original/source will be resumed.
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Starting main execution for message ${userMessage.messageId} for task ${taskId}.`,
|
||||
);
|
||||
@@ -598,18 +625,30 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.executingTasks.delete(taskId);
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Saving final state for task ${taskId}.`,
|
||||
);
|
||||
try {
|
||||
await this.taskStore?.save(wrapper.toSDKTask());
|
||||
logger.info(`[CoderAgentExecutor] Task ${taskId} state saved.`);
|
||||
} catch (saveError) {
|
||||
logger.error(
|
||||
`[CoderAgentExecutor] Failed to save task ${taskId} state in finally block:`,
|
||||
saveError,
|
||||
if (isPrimaryExecution) {
|
||||
this.executingTasks.delete(taskId);
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Saving final state for task ${taskId}.`,
|
||||
);
|
||||
try {
|
||||
await this.taskStore?.save(wrapper.toSDKTask());
|
||||
logger.info(`[CoderAgentExecutor] Task ${taskId} state saved.`);
|
||||
} catch (saveError) {
|
||||
logger.error(
|
||||
`[CoderAgentExecutor] Failed to save task ${taskId} state in finally block:`,
|
||||
saveError,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
['canceled', 'failed', 'completed'].includes(currentTask.taskState)
|
||||
) {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Task ${taskId} reached terminal state ${currentTask.taskState}. Evicting and disposing.`,
|
||||
);
|
||||
wrapper.task.dispose();
|
||||
this.tasks.delete(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { Task } from './task.js';
|
||||
import {
|
||||
type Config,
|
||||
MessageBusType,
|
||||
ToolConfirmationOutcome,
|
||||
ApprovalMode,
|
||||
Scheduler,
|
||||
type MessageBus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
|
||||
describe('Task Event-Driven Scheduler', () => {
|
||||
let mockConfig: Config;
|
||||
let mockEventBus: ExecutionEventBus;
|
||||
let messageBus: MessageBus;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConfig = createMockConfig({
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
}) as Config;
|
||||
messageBus = mockConfig.messageBus;
|
||||
mockEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should instantiate Scheduler when enabled', () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
expect(task.scheduler).toBeInstanceOf(Scheduler);
|
||||
});
|
||||
|
||||
it('should subscribe to TOOL_CALLS_UPDATE and map status changes', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'executing',
|
||||
};
|
||||
|
||||
// Simulate MessageBus event
|
||||
// Simulate MessageBus event
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
||||
}
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
});
|
||||
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: expect.objectContaining({
|
||||
state: 'submitted', // initial task state
|
||||
}),
|
||||
metadata: expect.objectContaining({
|
||||
coderAgent: expect.objectContaining({
|
||||
kind: 'tool-call-update',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle tool confirmations by publishing to MessageBus', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
|
||||
// Simulate MessageBus event to stash the correlationId
|
||||
// Simulate MessageBus event
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
||||
}
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
});
|
||||
|
||||
// Simulate A2A client confirmation
|
||||
const part = {
|
||||
kind: 'data',
|
||||
data: {
|
||||
callId: '1',
|
||||
outcome: 'proceed_once',
|
||||
},
|
||||
};
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart(part);
|
||||
expect(handled).toBe(true);
|
||||
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-1',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Rejection (Cancel) and Modification (ModifyWithEditor)', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// Simulate Rejection (Cancel)
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'cancel' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-1',
|
||||
confirmed: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const toolCall2 = {
|
||||
request: { callId: '2', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
|
||||
|
||||
// Simulate ModifyWithEditor
|
||||
const handled2 = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '2', outcome: 'modify_with_editor' },
|
||||
});
|
||||
expect(handled2).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-2',
|
||||
confirmed: false,
|
||||
outcome: ToolConfirmationOutcome.ModifyWithEditor,
|
||||
payload: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool operations correctly', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-1',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// Simulate ProceedOnce for MCP
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_once' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-1',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool ProceedAlwaysServer outcome', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-2',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_always_server' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-2',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool ProceedAlwaysTool outcome', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-3',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_always_tool' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-3',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool ProceedAlwaysAndSave outcome', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-4',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_always_and_save' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-4',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute without confirmation in YOLO mode and not transition to input-required', async () => {
|
||||
// Enable YOLO mode
|
||||
const yoloConfig = createMockConfig({
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
getApprovalMode: () => ApprovalMode.YOLO,
|
||||
}) as Config;
|
||||
const yoloMessageBus = yoloConfig.messageBus;
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', yoloConfig, mockEventBus);
|
||||
task.setTaskStateAndPublishUpdate = vi.fn();
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
|
||||
const handler = (yoloMessageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
|
||||
expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should NOT transition to input-required since it was auto-approved
|
||||
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith(
|
||||
'input-required',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle output updates via the message bus', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'executing',
|
||||
liveOutput: 'chunk1',
|
||||
};
|
||||
|
||||
// Simulate MessageBus event
|
||||
// Simulate MessageBus event
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
||||
}
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
});
|
||||
|
||||
// Should publish artifact update for output
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'artifact-update',
|
||||
artifact: expect.objectContaining({
|
||||
artifactId: 'tool-1-output',
|
||||
parts: [{ kind: 'text', text: 'chunk1' }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should complete artifact creation without hanging', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCallId = 'create-file-123';
|
||||
task['_registerToolCall'](toolCallId, 'executing');
|
||||
|
||||
const toolCall = {
|
||||
request: {
|
||||
callId: toolCallId,
|
||||
name: 'writeFile',
|
||||
args: { path: 'test.sh' },
|
||||
},
|
||||
status: 'success',
|
||||
result: { ok: true },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// The tool should be complete and registered appropriately, eventually
|
||||
// triggering the toolCompletionPromise resolution when all clear.
|
||||
const internalTask = task as unknown as {
|
||||
completedToolCalls: unknown[];
|
||||
pendingToolCalls: Map<string, string>;
|
||||
};
|
||||
expect(internalTask.completedToolCalls.length).toBe(1);
|
||||
expect(internalTask.pendingToolCalls.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should preserve messageId across multiple text chunks to prevent UI duplication', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
// Initialize the ID for the first turn (happens internally upon LLM stream)
|
||||
task.currentAgentMessageId = 'test-id-123';
|
||||
|
||||
// Simulate sending multiple text chunks
|
||||
task._sendTextContent('chunk 1');
|
||||
task._sendTextContent('chunk 2');
|
||||
|
||||
// Both text contents should have been published with the same messageId
|
||||
const textCalls = (mockEventBus.publish as Mock).mock.calls.filter(
|
||||
(call) => call[0].status?.message?.kind === 'message',
|
||||
);
|
||||
expect(textCalls.length).toBe(2);
|
||||
expect(textCalls[0][0].status.message.messageId).toBe('test-id-123');
|
||||
expect(textCalls[1][0].status.message.messageId).toBe('test-id-123');
|
||||
|
||||
// Simulate starting a new turn by calling getAndClearCompletedTools
|
||||
// (which precedes sendCompletedToolsToLlm where a new ID is minted)
|
||||
task.getAndClearCompletedTools();
|
||||
|
||||
// sendCompletedToolsToLlm internally rolls the ID forward.
|
||||
// Simulate what sendCompletedToolsToLlm does:
|
||||
const internalTask = task as unknown as {
|
||||
setTaskStateAndPublishUpdate: (state: string, change: unknown) => void;
|
||||
};
|
||||
internalTask.setTaskStateAndPublishUpdate('working', {});
|
||||
|
||||
// Simulate what sendCompletedToolsToLlm does: generate a new UUID for the next turn
|
||||
task.currentAgentMessageId = 'test-id-456';
|
||||
|
||||
task._sendTextContent('chunk 3');
|
||||
|
||||
const secondTurnCalls = (mockEventBus.publish as Mock).mock.calls.filter(
|
||||
(call) => call[0].status?.message?.messageId === 'test-id-456',
|
||||
);
|
||||
expect(secondTurnCalls.length).toBe(1);
|
||||
expect(secondTurnCalls[0][0].status.message.parts[0].text).toBe('chunk 3');
|
||||
});
|
||||
|
||||
it('should handle parallel tool calls correctly', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall1 = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test 1', prompt: 'test 1' },
|
||||
};
|
||||
|
||||
const toolCall2 = {
|
||||
request: { callId: '2', name: 'pwd', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
// Publish update for both tool calls simultaneously
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1, toolCall2],
|
||||
});
|
||||
|
||||
// Confirm first tool call
|
||||
const handled1 = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_once' },
|
||||
});
|
||||
expect(handled1).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-1',
|
||||
confirmed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Confirm second tool call
|
||||
const handled2 = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '2', outcome: 'cancel' },
|
||||
});
|
||||
expect(handled2).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-2',
|
||||
confirmed: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should wait for executing tools before transitioning to input-required state', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
task.setTaskStateAndPublishUpdate = vi.fn();
|
||||
|
||||
// Register tool 1 as executing
|
||||
task['_registerToolCall']('1', 'executing');
|
||||
|
||||
const toolCall1 = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'executing',
|
||||
};
|
||||
|
||||
const toolCall2 = {
|
||||
request: { callId: '2', name: 'pwd', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1, toolCall2],
|
||||
});
|
||||
|
||||
// Should NOT transition to input-required yet
|
||||
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith(
|
||||
'input-required',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
// Complete tool 1
|
||||
const toolCall1Complete = {
|
||||
...toolCall1,
|
||||
status: 'success',
|
||||
result: { ok: true },
|
||||
};
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1Complete, toolCall2],
|
||||
});
|
||||
|
||||
// Now it should transition
|
||||
expect(task.setTaskStateAndPublishUpdate).toHaveBeenCalledWith(
|
||||
'input-required',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore confirmations for unknown tool calls', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: 'unknown-id', outcome: 'proceed_once' },
|
||||
});
|
||||
|
||||
// Should return false for unhandled tool call
|
||||
expect(handled).toBe(false);
|
||||
|
||||
// Should not publish anything to the message bus
|
||||
expect(messageBus.publish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -4,25 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { Task } from './task.js';
|
||||
import {
|
||||
GeminiEventType,
|
||||
ApprovalMode,
|
||||
ToolConfirmationOutcome,
|
||||
type Config,
|
||||
type ToolCallRequestInfo,
|
||||
type GitService,
|
||||
type CompletedToolCall,
|
||||
type ToolCall,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
||||
@@ -389,188 +378,6 @@ describe('Task', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('_schedulerToolCallsUpdate', () => {
|
||||
let task: Task;
|
||||
type SpyInstance = ReturnType<typeof vi.spyOn>;
|
||||
let setTaskStateAndPublishUpdateSpy: SpyInstance;
|
||||
let mockConfig: Config;
|
||||
let mockEventBus: ExecutionEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = createMockConfig() as Config;
|
||||
mockEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
// Spy on the method we want to check calls for
|
||||
setTaskStateAndPublishUpdateSpy = vi.spyOn(
|
||||
task,
|
||||
'setTaskStateAndPublishUpdate',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set state to input-required when a tool is awaiting approval and none are executing', () => {
|
||||
const toolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
] as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
// The last call should be the final state update
|
||||
expect(setTaskStateAndPublishUpdateSpy).toHaveBeenLastCalledWith(
|
||||
'input-required',
|
||||
{ kind: 'state-change' },
|
||||
undefined,
|
||||
undefined,
|
||||
true, // final: true
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT set state to input-required if a tool is awaiting approval but another is executing', () => {
|
||||
const toolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
{ request: { callId: '2' }, status: 'executing' },
|
||||
] as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
// It will be called for status updates, but not with final: true
|
||||
const finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set state to input-required once an executing tool finishes, leaving one awaiting approval', () => {
|
||||
const initialToolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
{ request: { callId: '2' }, status: 'executing' },
|
||||
] as ToolCall[];
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(initialToolCalls);
|
||||
|
||||
// No final call yet
|
||||
let finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
|
||||
// Now, the executing tool finishes. The scheduler would call _resolveToolCall for it.
|
||||
// @ts-expect-error - Calling private method
|
||||
task._resolveToolCall('2');
|
||||
|
||||
// Then another update comes in for the awaiting tool (e.g., a re-check)
|
||||
const subsequentToolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
] as ToolCall[];
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(subsequentToolCalls);
|
||||
|
||||
// NOW we should get the final call
|
||||
finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeDefined();
|
||||
expect(finalCall?.[0]).toBe('input-required');
|
||||
});
|
||||
|
||||
it('should NOT set state to input-required if skipFinalTrueAfterInlineEdit is true', () => {
|
||||
task.skipFinalTrueAfterInlineEdit = true;
|
||||
const toolCalls = [
|
||||
{ request: { callId: '1' }, status: 'awaiting_approval' },
|
||||
] as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
const finalCall = setTaskStateAndPublishUpdateSpy.mock.calls.find(
|
||||
(call) => call[4] === true,
|
||||
);
|
||||
expect(finalCall).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('auto-approval', () => {
|
||||
it('should auto-approve tool calls when autoExecute is true', () => {
|
||||
task.autoExecute = true;
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
onConfirm: onConfirmSpy,
|
||||
},
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
});
|
||||
|
||||
it('should auto-approve tool calls when approval mode is YOLO', () => {
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
|
||||
task.autoExecute = false;
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
onConfirm: onConfirmSpy,
|
||||
},
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT auto-approve when autoExecute is false and mode is not YOLO', () => {
|
||||
task.autoExecute = false;
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: { onConfirm: onConfirmSpy },
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
// @ts-expect-error - Calling private method
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentPromptId and promptCount', () => {
|
||||
it('should correctly initialize and update promptId and promptCount', async () => {
|
||||
const mockConfig = createMockConfig();
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CoreToolScheduler,
|
||||
type AgentLoopContext,
|
||||
Scheduler,
|
||||
type GeminiClient,
|
||||
GeminiEventType,
|
||||
ToolConfirmationOutcome,
|
||||
@@ -34,6 +35,8 @@ import {
|
||||
isSubagentProgress,
|
||||
EDIT_TOOL_NAMES,
|
||||
processRestorableToolCalls,
|
||||
MessageBusType,
|
||||
type ToolCallsUpdateMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type ExecutionEventBus,
|
||||
@@ -66,51 +69,33 @@ import type { PartUnion, Part as genAiPart } from '@google/genai';
|
||||
|
||||
type UnionKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
type ConfirmationType = ToolCallConfirmationDetails['type'];
|
||||
|
||||
const VALID_CONFIRMATION_TYPES: readonly ConfirmationType[] = [
|
||||
'edit',
|
||||
'exec',
|
||||
'mcp',
|
||||
'info',
|
||||
'ask_user',
|
||||
'exit_plan_mode',
|
||||
] as const;
|
||||
|
||||
function isToolCallConfirmationDetails(
|
||||
value: unknown,
|
||||
): value is ToolCallConfirmationDetails {
|
||||
if (
|
||||
typeof value !== 'object' ||
|
||||
value === null ||
|
||||
!('onConfirm' in value) ||
|
||||
typeof value.onConfirm !== 'function' ||
|
||||
!('type' in value) ||
|
||||
typeof value.type !== 'string'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (VALID_CONFIRMATION_TYPES as readonly string[]).includes(value.type);
|
||||
}
|
||||
|
||||
export class Task {
|
||||
id: string;
|
||||
contextId: string;
|
||||
scheduler: CoreToolScheduler;
|
||||
scheduler: Scheduler;
|
||||
config: Config;
|
||||
geminiClient: GeminiClient;
|
||||
pendingToolConfirmationDetails: Map<string, ToolCallConfirmationDetails>;
|
||||
pendingCorrelationIds: Map<string, string> = new Map();
|
||||
taskState: TaskState;
|
||||
eventBus?: ExecutionEventBus;
|
||||
completedToolCalls: CompletedToolCall[];
|
||||
processedToolCallIds: Set<string> = new Set();
|
||||
skipFinalTrueAfterInlineEdit = false;
|
||||
modelInfo?: string;
|
||||
currentPromptId: string | undefined;
|
||||
currentAgentMessageId = uuidv4();
|
||||
promptCount = 0;
|
||||
autoExecute: boolean;
|
||||
private get isYoloMatch(): boolean {
|
||||
return (
|
||||
this.autoExecute || this.config.getApprovalMode() === ApprovalMode.YOLO
|
||||
);
|
||||
}
|
||||
|
||||
// For tool waiting logic
|
||||
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
|
||||
private toolsAlreadyConfirmed: Set<string> = new Set();
|
||||
private toolCompletionPromise?: Promise<void>;
|
||||
private toolCompletionNotifier?: {
|
||||
resolve: () => void;
|
||||
@@ -127,8 +112,11 @@ export class Task {
|
||||
this.id = id;
|
||||
this.contextId = contextId;
|
||||
this.config = config;
|
||||
this.scheduler = this.createScheduler();
|
||||
this.geminiClient = this.config.getGeminiClient();
|
||||
|
||||
this.scheduler = this.setupEventDrivenScheduler();
|
||||
|
||||
const loopContext: AgentLoopContext = this.config;
|
||||
this.geminiClient = loopContext.geminiClient;
|
||||
this.pendingToolConfirmationDetails = new Map();
|
||||
this.taskState = 'submitted';
|
||||
this.eventBus = eventBus;
|
||||
@@ -157,7 +145,8 @@ export class Task {
|
||||
// process. This is not scoped to the individual task but reflects the global connection
|
||||
// state managed within the @gemini-cli/core module.
|
||||
async getMetadata(): Promise<TaskMetadata> {
|
||||
const toolRegistry = this.config.getToolRegistry();
|
||||
const loopContext: AgentLoopContext = this.config;
|
||||
const toolRegistry = loopContext.toolRegistry;
|
||||
const mcpServers = this.config.getMcpClientManager()?.getMcpServers() || {};
|
||||
const serverStatuses = getAllMCPServerStatuses();
|
||||
const servers = Object.keys(mcpServers).map((serverName) => ({
|
||||
@@ -227,7 +216,7 @@ export class Task {
|
||||
logger.info(
|
||||
`[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`,
|
||||
);
|
||||
return this.toolCompletionPromise;
|
||||
await this.toolCompletionPromise;
|
||||
}
|
||||
|
||||
cancelPendingTools(reason: string): void {
|
||||
@@ -240,6 +229,9 @@ export class Task {
|
||||
this.toolCompletionNotifier.reject(new Error(reason));
|
||||
}
|
||||
this.pendingToolCalls.clear();
|
||||
this.pendingCorrelationIds.clear();
|
||||
|
||||
this.scheduler.cancelAll();
|
||||
// Reset the promise for any future operations, ensuring it's in a clean state.
|
||||
this._resetToolCompletionPromise();
|
||||
}
|
||||
@@ -252,7 +244,7 @@ export class Task {
|
||||
kind: 'message',
|
||||
role,
|
||||
parts: [{ kind: 'text', text }],
|
||||
messageId: uuidv4(),
|
||||
messageId: role === 'agent' ? this.currentAgentMessageId : uuidv4(),
|
||||
taskId: this.id,
|
||||
contextId: this.contextId,
|
||||
};
|
||||
@@ -384,104 +376,156 @@ export class Task {
|
||||
this.eventBus?.publish(artifactEvent);
|
||||
}
|
||||
|
||||
private async _schedulerAllToolCallsComplete(
|
||||
completedToolCalls: CompletedToolCall[],
|
||||
): Promise<void> {
|
||||
logger.info(
|
||||
'[Task] All tool calls completed by scheduler (batch):',
|
||||
completedToolCalls.map((tc) => tc.request.callId),
|
||||
);
|
||||
this.completedToolCalls.push(...completedToolCalls);
|
||||
completedToolCalls.forEach((tc) => {
|
||||
this._resolveToolCall(tc.request.callId);
|
||||
private messageBusListener?: (message: ToolCallsUpdateMessage) => void;
|
||||
|
||||
private setupEventDrivenScheduler(): Scheduler {
|
||||
const loopContext: AgentLoopContext = this.config;
|
||||
const messageBus = loopContext.messageBus;
|
||||
const scheduler = new Scheduler({
|
||||
schedulerId: this.id,
|
||||
context: this.config,
|
||||
messageBus,
|
||||
getPreferredEditor: () => DEFAULT_GUI_EDITOR,
|
||||
});
|
||||
|
||||
this.messageBusListener = this.handleEventDrivenToolCallsUpdate.bind(this);
|
||||
messageBus.subscribe<ToolCallsUpdateMessage>(
|
||||
MessageBusType.TOOL_CALLS_UPDATE,
|
||||
this.messageBusListener,
|
||||
);
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
private _schedulerToolCallsUpdate(toolCalls: ToolCall[]): void {
|
||||
logger.info(
|
||||
'[Task] Scheduler tool calls updated:',
|
||||
toolCalls.map((tc) => `${tc.request.callId} (${tc.status})`),
|
||||
);
|
||||
|
||||
// Update state and send continuous, non-final updates
|
||||
toolCalls.forEach((tc) => {
|
||||
const previousStatus = this.pendingToolCalls.get(tc.request.callId);
|
||||
const hasChanged = previousStatus !== tc.status;
|
||||
|
||||
// Resolve tool call if it has reached a terminal state
|
||||
if (['success', 'error', 'cancelled'].includes(tc.status)) {
|
||||
this._resolveToolCall(tc.request.callId);
|
||||
} else {
|
||||
// This will update the map
|
||||
this._registerToolCall(tc.request.callId, tc.status);
|
||||
}
|
||||
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
const details = tc.confirmationDetails;
|
||||
if (isToolCallConfirmationDetails(details)) {
|
||||
this.pendingToolConfirmationDetails.set(tc.request.callId, details);
|
||||
}
|
||||
}
|
||||
|
||||
// Only send an update if the status has actually changed.
|
||||
if (hasChanged) {
|
||||
const coderAgentMessage: CoderAgentMessage =
|
||||
tc.status === 'awaiting_approval'
|
||||
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
||||
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
|
||||
const event = this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
coderAgentMessage,
|
||||
message,
|
||||
false, // Always false for these continuous updates
|
||||
);
|
||||
this.eventBus?.publish(event);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
this.autoExecute ||
|
||||
this.config.getApprovalMode() === ApprovalMode.YOLO
|
||||
) {
|
||||
logger.info(
|
||||
'[Task] ' +
|
||||
(this.autoExecute ? '' : 'YOLO mode enabled. ') +
|
||||
'Auto-approving all tool calls.',
|
||||
dispose(): void {
|
||||
if (this.messageBusListener) {
|
||||
const loopContext: AgentLoopContext = this.config;
|
||||
loopContext.messageBus.unsubscribe(
|
||||
MessageBusType.TOOL_CALLS_UPDATE,
|
||||
this.messageBusListener,
|
||||
);
|
||||
toolCalls.forEach((tc: ToolCall) => {
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
const details = tc.confirmationDetails;
|
||||
if (isToolCallConfirmationDetails(details)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
details.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
||||
this.pendingToolConfirmationDetails.delete(tc.request.callId);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.messageBusListener = undefined;
|
||||
}
|
||||
|
||||
this.scheduler.dispose();
|
||||
}
|
||||
|
||||
private handleEventDrivenToolCallsUpdate(
|
||||
event: ToolCallsUpdateMessage,
|
||||
): void {
|
||||
if (event.type !== MessageBusType.TOOL_CALLS_UPDATE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allPendingStatuses = Array.from(this.pendingToolCalls.values());
|
||||
const isAwaitingApproval = allPendingStatuses.some(
|
||||
(status) => status === 'awaiting_approval',
|
||||
);
|
||||
const isExecuting = allPendingStatuses.some(
|
||||
(status) => status === 'executing',
|
||||
);
|
||||
const toolCalls = event.toolCalls;
|
||||
|
||||
toolCalls.forEach((tc) => {
|
||||
this.handleEventDrivenToolCall(tc);
|
||||
});
|
||||
|
||||
this.checkInputRequiredState();
|
||||
}
|
||||
|
||||
private handleEventDrivenToolCall(tc: ToolCall): void {
|
||||
const callId = tc.request.callId;
|
||||
|
||||
// Do not process events for tools that have already been finalized.
|
||||
// This prevents duplicate completions if the state manager emits a snapshot containing
|
||||
// already resolved tools whose IDs were removed from pendingToolCalls.
|
||||
if (
|
||||
this.processedToolCallIds.has(callId) ||
|
||||
this.completedToolCalls.some((c) => c.request.callId === callId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousStatus = this.pendingToolCalls.get(callId);
|
||||
const hasChanged = previousStatus !== tc.status;
|
||||
|
||||
// 1. Handle Output
|
||||
if (tc.status === 'executing' && tc.liveOutput) {
|
||||
this._schedulerOutputUpdate(callId, tc.liveOutput);
|
||||
}
|
||||
|
||||
// 2. Handle terminal states
|
||||
if (
|
||||
tc.status === 'success' ||
|
||||
tc.status === 'error' ||
|
||||
tc.status === 'cancelled'
|
||||
) {
|
||||
this.toolsAlreadyConfirmed.delete(callId);
|
||||
if (hasChanged) {
|
||||
logger.info(
|
||||
`[Task] Tool call ${callId} completed with status: ${tc.status}`,
|
||||
);
|
||||
this.completedToolCalls.push(tc);
|
||||
this._resolveToolCall(callId);
|
||||
}
|
||||
} else {
|
||||
// Keep track of pending tools
|
||||
this._registerToolCall(callId, tc.status);
|
||||
}
|
||||
|
||||
// 3. Handle Confirmation Stash
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
const details = tc.confirmationDetails;
|
||||
|
||||
if (tc.correlationId) {
|
||||
this.pendingCorrelationIds.set(callId, tc.correlationId);
|
||||
}
|
||||
|
||||
this.pendingToolConfirmationDetails.set(callId, {
|
||||
...details,
|
||||
onConfirm: async () => {},
|
||||
} as ToolCallConfirmationDetails);
|
||||
}
|
||||
|
||||
// 4. Publish Status Updates to A2A event bus
|
||||
if (hasChanged) {
|
||||
const coderAgentMessage: CoderAgentMessage =
|
||||
tc.status === 'awaiting_approval'
|
||||
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
||||
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
||||
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
const statusUpdate = this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
coderAgentMessage,
|
||||
message,
|
||||
false,
|
||||
);
|
||||
this.eventBus?.publish(statusUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private checkInputRequiredState(): void {
|
||||
if (this.isYoloMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Handle Input Required State
|
||||
let isAwaitingApproval = false;
|
||||
let isExecuting = false;
|
||||
|
||||
for (const [callId, status] of this.pendingToolCalls.entries()) {
|
||||
if (status === 'executing' || status === 'scheduled') {
|
||||
isExecuting = true;
|
||||
} else if (
|
||||
status === 'awaiting_approval' &&
|
||||
!this.toolsAlreadyConfirmed.has(callId)
|
||||
) {
|
||||
isAwaitingApproval = true;
|
||||
}
|
||||
}
|
||||
|
||||
// The turn is complete and requires user input if at least one tool
|
||||
// is waiting for the user's decision, and no other tool is actively
|
||||
// running in the background.
|
||||
if (
|
||||
isAwaitingApproval &&
|
||||
!isExecuting &&
|
||||
!this.skipFinalTrueAfterInlineEdit
|
||||
) {
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
const wasAlreadyInputRequired = this.taskState === 'input-required';
|
||||
|
||||
// We don't need to send another message, just a final status update.
|
||||
this.setTaskStateAndPublishUpdate(
|
||||
'input-required',
|
||||
{ kind: CoderAgentEvent.StateChangeEvent },
|
||||
@@ -489,18 +533,13 @@ export class Task {
|
||||
undefined,
|
||||
/*final*/ true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private createScheduler(): CoreToolScheduler {
|
||||
const scheduler = new CoreToolScheduler({
|
||||
outputUpdateHandler: this._schedulerOutputUpdate.bind(this),
|
||||
onAllToolCallsComplete: this._schedulerAllToolCallsComplete.bind(this),
|
||||
onToolCallsUpdate: this._schedulerToolCallsUpdate.bind(this),
|
||||
getPreferredEditor: () => DEFAULT_GUI_EDITOR,
|
||||
config: this.config,
|
||||
});
|
||||
return scheduler;
|
||||
// Unblock waitForPendingTools to correctly end the executor loop and release the HTTP response stream.
|
||||
// The IDE client will open a new stream with the confirmation reply.
|
||||
if (!wasAlreadyInputRequired && this.toolCompletionNotifier) {
|
||||
this.toolCompletionNotifier.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _pickFields<
|
||||
@@ -713,7 +752,16 @@ export class Task {
|
||||
};
|
||||
this.setTaskStateAndPublishUpdate('working', stateChange);
|
||||
|
||||
await this.scheduler.schedule(updatedRequests, abortSignal);
|
||||
// Pre-register tools to ensure waitForPendingTools sees them as pending
|
||||
// before the async scheduler enqueues them and fires the event bus update.
|
||||
for (const req of updatedRequests) {
|
||||
if (!this.pendingToolCalls.has(req.callId)) {
|
||||
this._registerToolCall(req.callId, 'scheduled');
|
||||
}
|
||||
}
|
||||
|
||||
// Fire and forget so we don't block the executor loop before waitForPendingTools can be called
|
||||
void this.scheduler.schedule(updatedRequests, abortSignal);
|
||||
}
|
||||
|
||||
async acceptAgentMessage(event: ServerGeminiStreamEvent): Promise<void> {
|
||||
@@ -839,9 +887,15 @@ export class Task {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!part.data['outcome']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callId = part.data['callId'];
|
||||
const outcomeString = part.data['outcome'];
|
||||
|
||||
this.toolsAlreadyConfirmed.add(callId);
|
||||
|
||||
let confirmationOutcome: ToolConfirmationOutcome | undefined;
|
||||
|
||||
if (outcomeString === 'proceed_once') {
|
||||
@@ -854,6 +908,8 @@ export class Task {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysServer;
|
||||
} else if (outcomeString === 'proceed_always_tool') {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysTool;
|
||||
} else if (outcomeString === 'proceed_always_and_save') {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysAndSave;
|
||||
} else if (outcomeString === 'modify_with_editor') {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ModifyWithEditor;
|
||||
} else {
|
||||
@@ -864,8 +920,9 @@ export class Task {
|
||||
}
|
||||
|
||||
const confirmationDetails = this.pendingToolConfirmationDetails.get(callId);
|
||||
const correlationId = this.pendingCorrelationIds.get(callId);
|
||||
|
||||
if (!confirmationDetails) {
|
||||
if (!confirmationDetails && !correlationId) {
|
||||
logger.warn(
|
||||
`[Task] Received tool confirmation for unknown or already processed callId: ${callId}`,
|
||||
);
|
||||
@@ -887,24 +944,36 @@ export class Task {
|
||||
// This will trigger the scheduler to continue or cancel the specific tool.
|
||||
// The scheduler's onToolCallsUpdate will then reflect the new state (e.g., executing or cancelled).
|
||||
|
||||
// If `edit` tool call, pass updated payload if presesent
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
const newContent = part.data['newContent'];
|
||||
const payload =
|
||||
typeof newContent === 'string'
|
||||
? ({ newContent } as ToolConfirmationPayload)
|
||||
: undefined;
|
||||
this.skipFinalTrueAfterInlineEdit = !!payload;
|
||||
try {
|
||||
// If `edit` tool call, pass updated payload if present
|
||||
const newContent = part.data['newContent'];
|
||||
const payload =
|
||||
confirmationDetails?.type === 'edit' && typeof newContent === 'string'
|
||||
? ({ newContent } as ToolConfirmationPayload)
|
||||
: undefined;
|
||||
this.skipFinalTrueAfterInlineEdit = !!payload;
|
||||
|
||||
try {
|
||||
if (correlationId) {
|
||||
const loopContext: AgentLoopContext = this.config;
|
||||
await loopContext.messageBus.publish({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId,
|
||||
confirmed:
|
||||
confirmationOutcome !== ToolConfirmationOutcome.Cancel &&
|
||||
confirmationOutcome !==
|
||||
ToolConfirmationOutcome.ModifyWithEditor,
|
||||
outcome: confirmationOutcome,
|
||||
payload,
|
||||
});
|
||||
} else if (confirmationDetails?.onConfirm) {
|
||||
// Fallback for legacy callback-based confirmation
|
||||
await confirmationDetails.onConfirm(confirmationOutcome, payload);
|
||||
} finally {
|
||||
// Once confirmationDetails.onConfirm finishes (or fails) with a payload,
|
||||
// reset skipFinalTrueAfterInlineEdit so that external callers receive
|
||||
// their call has been completed.
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
}
|
||||
} else {
|
||||
await confirmationDetails.onConfirm(confirmationOutcome);
|
||||
} finally {
|
||||
// Once confirmation payload is sent or callback finishes,
|
||||
// reset skipFinalTrueAfterInlineEdit so that external callers receive
|
||||
// their call has been completed.
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
}
|
||||
} finally {
|
||||
if (gcpProject) {
|
||||
@@ -920,6 +989,7 @@ export class Task {
|
||||
// Note !== ToolConfirmationOutcome.ModifyWithEditor does not work!
|
||||
if (confirmationOutcome !== 'modify_with_editor') {
|
||||
this.pendingToolConfirmationDetails.delete(callId);
|
||||
this.pendingCorrelationIds.delete(callId);
|
||||
}
|
||||
|
||||
// If outcome is Cancel, scheduler should update status to 'cancelled', which then resolves the tool.
|
||||
@@ -953,6 +1023,9 @@ export class Task {
|
||||
|
||||
getAndClearCompletedTools(): CompletedToolCall[] {
|
||||
const tools = [...this.completedToolCalls];
|
||||
for (const tool of tools) {
|
||||
this.processedToolCallIds.add(tool.request.callId);
|
||||
}
|
||||
this.completedToolCalls = [];
|
||||
return tools;
|
||||
}
|
||||
@@ -1013,6 +1086,7 @@ export class Task {
|
||||
};
|
||||
// Set task state to working as we are about to call LLM
|
||||
this.setTaskStateAndPublishUpdate('working', stateChange);
|
||||
this.currentAgentMessageId = uuidv4();
|
||||
yield* this.geminiClient.sendMessageStream(
|
||||
llmParts,
|
||||
aborted,
|
||||
@@ -1034,6 +1108,10 @@ export class Task {
|
||||
if (confirmationHandled) {
|
||||
anyConfirmationHandled = true;
|
||||
// If a confirmation was handled, the scheduler will now run the tool (or cancel it).
|
||||
// We resolve the toolCompletionPromise manually in checkInputRequiredState
|
||||
// to break the original execution loop, so we must reset it here so the
|
||||
// new loop correctly awaits the tool's final execution.
|
||||
this._resetToolCompletionPromise();
|
||||
// We don't send anything to the LLM for this part.
|
||||
// The subsequent tool execution will eventually lead to resolveToolCall.
|
||||
continue;
|
||||
@@ -1048,6 +1126,7 @@ export class Task {
|
||||
if (hasContentForLlm) {
|
||||
this.currentPromptId =
|
||||
this.config.getSessionId() + '########' + this.promptCount++;
|
||||
this.currentAgentMessageId = uuidv4();
|
||||
logger.info('[Task] Sending new parts to LLM.');
|
||||
const stateChange: StateChange = {
|
||||
kind: CoderAgentEvent.StateChangeEvent,
|
||||
@@ -1093,7 +1172,6 @@ export class Task {
|
||||
if (content === '') {
|
||||
return;
|
||||
}
|
||||
logger.info('[Task] Sending text content to event bus.');
|
||||
const message = this._createTextMessage(content);
|
||||
const textContent: TextContent = {
|
||||
kind: CoderAgentEvent.TextContentEvent,
|
||||
@@ -1125,7 +1203,7 @@ export class Task {
|
||||
data: content,
|
||||
} as Part,
|
||||
],
|
||||
messageId: uuidv4(),
|
||||
messageId: this.currentAgentMessageId,
|
||||
taskId: this.id,
|
||||
contextId: this.contextId,
|
||||
};
|
||||
|
||||
@@ -59,6 +59,9 @@ describe('a2a-server memory commands', () => {
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
mockConfig = {
|
||||
get toolRegistry() {
|
||||
return mockToolRegistry;
|
||||
},
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -168,7 +171,6 @@ describe('a2a-server memory commands', () => {
|
||||
]);
|
||||
|
||||
expect(mockAddMemory).toHaveBeenCalledWith(fact);
|
||||
expect(mockConfig.getToolRegistry).toHaveBeenCalled();
|
||||
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('save_memory');
|
||||
expect(mockSaveMemoryTool.buildAndExecute).toHaveBeenCalledWith(
|
||||
{ fact },
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import type { AgentLoopContext } from '@google/gemini-cli-core';
|
||||
|
||||
const DEFAULT_SANITIZATION_CONFIG = {
|
||||
allowedEnvironmentVariables: [],
|
||||
@@ -95,7 +96,8 @@ export class AddMemoryCommand implements Command {
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
|
||||
const toolRegistry = context.config.getToolRegistry();
|
||||
const loopContext: AgentLoopContext = context.config;
|
||||
const toolRegistry = loopContext.toolRegistry;
|
||||
const tool = toolRegistry.getTool(result.toolName);
|
||||
if (tool) {
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -91,6 +91,15 @@ describe('loadConfig', () => {
|
||||
expect(fetchAdminControlsOnce).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass clientName as a2a-server to Config', async () => {
|
||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||
expect(Config).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
clientName: 'a2a-server',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when admin controls experiment is enabled', () => {
|
||||
beforeEach(() => {
|
||||
// We need to cast to any here to modify the mock implementation
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function loadConfig(
|
||||
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: taskId,
|
||||
clientName: 'a2a-server',
|
||||
model: PREVIEW_GEMINI_MODEL,
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: undefined, // Sandbox might not be relevant for a server-side agent
|
||||
|
||||
@@ -37,6 +37,9 @@ export interface Settings {
|
||||
showMemoryUsage?: boolean;
|
||||
checkpointing?: CheckpointingSettings;
|
||||
folderTrust?: boolean;
|
||||
general?: {
|
||||
previewFeatures?: boolean;
|
||||
};
|
||||
|
||||
// Git-aware file filtering settings
|
||||
fileFiltering?: {
|
||||
|
||||
@@ -65,7 +65,12 @@ vi.mock('../utils/logger.js', () => ({
|
||||
}));
|
||||
|
||||
let config: Config;
|
||||
const getToolRegistrySpy = vi.fn().mockReturnValue(ApprovalMode.DEFAULT);
|
||||
const getToolRegistrySpy = vi.fn().mockReturnValue({
|
||||
getTool: vi.fn(),
|
||||
getAllToolNames: vi.fn().mockReturnValue([]),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolsByServer: vi.fn().mockReturnValue([]),
|
||||
});
|
||||
const getApprovalModeSpy = vi.fn();
|
||||
const getShellExecutionConfigSpy = vi.fn();
|
||||
const getExtensionsSpy = vi.fn();
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
GeminiClient,
|
||||
HookSystem,
|
||||
type MessageBus,
|
||||
PolicyDecision,
|
||||
tmpdir,
|
||||
type Config,
|
||||
type Storage,
|
||||
type ToolRegistry,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
|
||||
import { expect, vi } from 'vitest';
|
||||
@@ -30,6 +32,28 @@ export function createMockConfig(
|
||||
const tmpDir = tmpdir();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const mockConfig = {
|
||||
get config() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return this as unknown as Config;
|
||||
},
|
||||
get toolRegistry() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const config = this as unknown as Config;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return config.getToolRegistry?.() as unknown as ToolRegistry;
|
||||
},
|
||||
get messageBus() {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(this as unknown as Config).getMessageBus?.() as unknown as MessageBus
|
||||
);
|
||||
},
|
||||
get geminiClient() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const config = this as unknown as Config;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return config.getGeminiClient?.() as unknown as GeminiClient;
|
||||
},
|
||||
getToolRegistry: vi.fn().mockReturnValue({
|
||||
getTool: vi.fn(),
|
||||
getAllToolNames: vi.fn().mockReturnValue([]),
|
||||
@@ -76,9 +100,6 @@ export function createMockConfig(
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(mockConfig as unknown as { config: Config; promptId: string }).config =
|
||||
mockConfig;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(mockConfig as unknown as { config: Config; promptId: string }).promptId =
|
||||
'test-prompt-id';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260304.28af4e127",
|
||||
"version": "0.35.0-nightly.20260311.657f19c1f",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,12 +26,12 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260304.28af4e127"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260311.657f19c1f"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.41.0",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"ansi-escapes": "^7.3.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
|
||||
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -12,7 +12,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
|
||||
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -23,7 +23,7 @@ exports[`runNonInteractive > should emit appropriate events for streaming JSON o
|
||||
{"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}}
|
||||
{"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -57,15 +57,17 @@ function hasMeta(obj: unknown): obj is { _meta?: Record<string, unknown> } {
|
||||
return typeof obj === 'object' && obj !== null && '_meta' in obj;
|
||||
}
|
||||
import type { Content, Part, FunctionCall } from '@google/genai';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import { SettingScope, loadSettings } from '../config/settings.js';
|
||||
import {
|
||||
SettingScope,
|
||||
loadSettings,
|
||||
type LoadedSettings,
|
||||
} from '../config/settings.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { CliArgs } from '../config/config.js';
|
||||
import { loadCliConfig } from '../config/config.js';
|
||||
import { loadCliConfig, type CliArgs } from '../config/config.js';
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { SessionSelector } from '../utils/sessionUtils.js';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { listExtensions } from '@google/gemini-cli-core';
|
||||
import { listExtensions, type Config } from '@google/gemini-cli-core';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
ExtensionManager,
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
export class ExtensionsCommand implements Command {
|
||||
readonly name = 'extensions';
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
import { handleInstall, installCommand } from './install.js';
|
||||
import yargs from 'yargs';
|
||||
import * as core from '@google/gemini-cli-core';
|
||||
import type { inferInstallMetadata } from '../../config/extension-manager.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
ExtensionManager,
|
||||
type inferInstallMetadata,
|
||||
} from '../../config/extension-manager.js';
|
||||
import type {
|
||||
promptForConsentNonInteractive,
|
||||
requestConsentNonInteractive,
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mcpCommand } from './mcp.js';
|
||||
import { type Argv } from 'yargs';
|
||||
import yargs from 'yargs';
|
||||
import yargs, { type Argv } from 'yargs';
|
||||
|
||||
describe('mcp command', () => {
|
||||
it('should have correct command definition', () => {
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { coreEvents, type Config } from '@google/gemini-cli-core';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { loadCliConfig } from '../../config/config.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
|
||||
@@ -1773,7 +1773,7 @@ describe('loadCliConfig model selection', () => {
|
||||
});
|
||||
|
||||
it('always prefers model from argv', async () => {
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview'];
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings({
|
||||
@@ -1785,11 +1785,11 @@ describe('loadCliConfig model selection', () => {
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash-preview');
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('selects the model from argv if provided', async () => {
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview'];
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings({
|
||||
@@ -1799,7 +1799,7 @@ describe('loadCliConfig model selection', () => {
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash-preview');
|
||||
expect(config.getModel()).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('selects the default auto model if provided via auto alias', async () => {
|
||||
@@ -3616,3 +3616,58 @@ describe('loadCliConfig mcpEnabled', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig acpMode and clientName', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should set acpMode to true and detect clientName when --acp flag is used', async () => {
|
||||
process.argv = ['node', 'script.js', '--acp'];
|
||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||
vi.stubEnv('VSCODE_GIT_ASKPASS_MAIN', '');
|
||||
vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', '');
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getAcpMode()).toBe(true);
|
||||
expect(config.getClientName()).toBe('acp-vscode');
|
||||
});
|
||||
|
||||
it('should set acpMode to true but leave clientName undefined for generic terminals', async () => {
|
||||
process.argv = ['node', 'script.js', '--acp'];
|
||||
vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); // Generic terminal
|
||||
vi.stubEnv('VSCODE_GIT_ASKPASS_MAIN', '');
|
||||
vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', '');
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getAcpMode()).toBe(true);
|
||||
expect(config.getClientName()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set acpMode to false and clientName to undefined by default', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getAcpMode()).toBe(false);
|
||||
expect(config.getClientName()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import yargs from 'yargs/yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
@@ -30,14 +31,18 @@ import {
|
||||
type HierarchicalMemory,
|
||||
coreEvents,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
isValidModelOrAlias,
|
||||
getValidModelsAndAliases,
|
||||
getAdminErrorMessage,
|
||||
isHeadlessMode,
|
||||
Config,
|
||||
resolveToRealPath,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
detectIdeFromEnv,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Settings,
|
||||
@@ -74,6 +79,7 @@ export interface CliArgs {
|
||||
yolo: boolean | undefined;
|
||||
approvalMode: string | undefined;
|
||||
policy: string[] | undefined;
|
||||
adminPolicy: string[] | undefined;
|
||||
allowedMcpServerNames: string[] | undefined;
|
||||
allowedTools: string[] | undefined;
|
||||
acp?: boolean;
|
||||
@@ -95,6 +101,21 @@ export interface CliArgs {
|
||||
isCommand: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to coerce comma-separated or multiple flag values into a flat array.
|
||||
*/
|
||||
const coerceCommaSeparated = (values: string[]): string[] => {
|
||||
if (values.length === 1 && values[0] === '') {
|
||||
return [''];
|
||||
}
|
||||
return values.flatMap((v) =>
|
||||
v
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
};
|
||||
|
||||
export async function parseArguments(
|
||||
settings: MergedSettings,
|
||||
): Promise<CliArgs> {
|
||||
@@ -164,14 +185,15 @@ export async function parseArguments(
|
||||
nargs: 1,
|
||||
description:
|
||||
'Additional policy files or directories to load (comma-separated or multiple --policy)',
|
||||
coerce: (policies: string[]) =>
|
||||
// Handle comma-separated values
|
||||
policies.flatMap((p) =>
|
||||
p
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
coerce: coerceCommaSeparated,
|
||||
})
|
||||
.option('admin-policy', {
|
||||
type: 'array',
|
||||
string: true,
|
||||
nargs: 1,
|
||||
description:
|
||||
'Additional admin policy files or directories to load (comma-separated or multiple --admin-policy)',
|
||||
coerce: coerceCommaSeparated,
|
||||
})
|
||||
.option('acp', {
|
||||
type: 'boolean',
|
||||
@@ -187,11 +209,7 @@ export async function parseArguments(
|
||||
string: true,
|
||||
nargs: 1,
|
||||
description: 'Allowed MCP server names',
|
||||
coerce: (mcpServerNames: string[]) =>
|
||||
// Handle comma-separated values
|
||||
mcpServerNames.flatMap((mcpServerName) =>
|
||||
mcpServerName.split(',').map((m) => m.trim()),
|
||||
),
|
||||
coerce: coerceCommaSeparated,
|
||||
})
|
||||
.option('allowed-tools', {
|
||||
type: 'array',
|
||||
@@ -199,9 +217,7 @@ export async function parseArguments(
|
||||
nargs: 1,
|
||||
description:
|
||||
'[DEPRECATED: Use Policy Engine instead See https://geminicli.com/docs/core/policy-engine] Tools that are allowed to run without confirmation',
|
||||
coerce: (tools: string[]) =>
|
||||
// Handle comma-separated values
|
||||
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
|
||||
coerce: coerceCommaSeparated,
|
||||
})
|
||||
.option('extensions', {
|
||||
alias: 'e',
|
||||
@@ -210,11 +226,7 @@ export async function parseArguments(
|
||||
nargs: 1,
|
||||
description:
|
||||
'A list of extensions to use. If not provided, all extensions are used.',
|
||||
coerce: (extensions: string[]) =>
|
||||
// Handle comma-separated values
|
||||
extensions.flatMap((extension) =>
|
||||
extension.split(',').map((e) => e.trim()),
|
||||
),
|
||||
coerce: coerceCommaSeparated,
|
||||
})
|
||||
.option('list-extensions', {
|
||||
alias: 'l',
|
||||
@@ -256,9 +268,7 @@ export async function parseArguments(
|
||||
nargs: 1,
|
||||
description:
|
||||
'Additional directories to include in the workspace (comma-separated or multiple --include-directories)',
|
||||
coerce: (dirs: string[]) =>
|
||||
// Handle comma-separated values
|
||||
dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())),
|
||||
coerce: coerceCommaSeparated,
|
||||
})
|
||||
.option('screen-reader', {
|
||||
type: 'boolean',
|
||||
@@ -488,6 +498,15 @@ export async function loadCliConfig(
|
||||
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
|
||||
let extensionRegistryURI: string | undefined = trustedFolder
|
||||
? settings.experimental?.extensionRegistryURI
|
||||
: undefined;
|
||||
if (extensionRegistryURI && !extensionRegistryURI.startsWith('http')) {
|
||||
extensionRegistryURI = resolveToRealPath(
|
||||
path.resolve(cwd, resolvePath(extensionRegistryURI)),
|
||||
);
|
||||
}
|
||||
|
||||
let memoryContent: string | HierarchicalMemory = '';
|
||||
let fileCount = 0;
|
||||
let filePaths: string[] = [];
|
||||
@@ -632,7 +651,8 @@ export async function loadCliConfig(
|
||||
...settings.mcp,
|
||||
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
|
||||
},
|
||||
policyPaths: argv.policy,
|
||||
policyPaths: argv.policy ?? settings.policyPaths,
|
||||
adminPolicyPaths: argv.adminPolicy ?? settings.adminPolicyPaths,
|
||||
};
|
||||
|
||||
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
|
||||
@@ -653,6 +673,18 @@ export async function loadCliConfig(
|
||||
const specifiedModel =
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
|
||||
|
||||
// Validate the model if one was explicitly specified
|
||||
if (specifiedModel && specifiedModel !== GEMINI_MODEL_ALIAS_AUTO) {
|
||||
if (!isValidModelOrAlias(specifiedModel)) {
|
||||
const validModels = getValidModelsAndAliases();
|
||||
|
||||
throw new FatalConfigError(
|
||||
`Invalid model: "${specifiedModel}"\n\n` +
|
||||
`Valid models and aliases:\n${validModels.map((m) => ` - ${m}`).join('\n')}\n\n` +
|
||||
`Use /model to switch models interactively.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const resolvedModel =
|
||||
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
? defaultModel
|
||||
@@ -693,8 +725,21 @@ export async function loadCliConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
|
||||
let clientName: string | undefined = undefined;
|
||||
if (isAcpMode) {
|
||||
const ide = detectIdeFromEnv();
|
||||
if (
|
||||
ide &&
|
||||
(ide.name !== 'vscode' || process.env['TERM_PROGRAM'] === 'vscode')
|
||||
) {
|
||||
clientName = `acp-${ide.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
return new Config({
|
||||
acpMode: !!argv.acp || !!argv.experimentalAcp,
|
||||
acpMode: isAcpMode,
|
||||
clientName,
|
||||
sessionId,
|
||||
clientVersion: await getVersion(),
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
@@ -764,6 +809,7 @@ export async function loadCliConfig(
|
||||
deleteSession: argv.deleteSession,
|
||||
enabledExtensions: argv.extensions,
|
||||
extensionLoader: extensionManager,
|
||||
extensionRegistryURI,
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.experimental?.plan,
|
||||
@@ -811,6 +857,7 @@ export async function loadCliConfig(
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
rawOutput: argv.rawOutput,
|
||||
acceptRawOutputRisk: argv.acceptRawOutputRisk,
|
||||
dynamicModelConfiguration: settings.experimental?.dynamicModelConfiguration,
|
||||
modelConfigServiceConfig: settings.modelConfigs,
|
||||
// TODO: loading of hooks based on workspace trust
|
||||
enableHooks: settings.hooksConfig.enabled,
|
||||
|
||||
@@ -9,16 +9,16 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import { createTestMergedSettings, type MergedSettings } from './settings.js';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
import { themeManager } from '../ui/themes/theme-manager.js';
|
||||
import {
|
||||
TrustLevel,
|
||||
loadTrustedFolders,
|
||||
isWorkspaceTrusted,
|
||||
} from './trustedFolders.js';
|
||||
import { getRealPath } from '@google/gemini-cli-core';
|
||||
import type { MergedSettings } from './settings.js';
|
||||
import { getRealPath, type CustomTheme } from '@google/gemini-cli-core';
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
||||
|
||||
@@ -39,6 +39,26 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const testTheme: CustomTheme = {
|
||||
type: 'custom',
|
||||
name: 'MyTheme',
|
||||
background: {
|
||||
primary: '#282828',
|
||||
diff: { added: '#2b3312', removed: '#341212' },
|
||||
},
|
||||
text: {
|
||||
primary: '#ebdbb2',
|
||||
secondary: '#a89984',
|
||||
link: '#83a598',
|
||||
accent: '#d3869b',
|
||||
},
|
||||
status: {
|
||||
success: '#b8bb26',
|
||||
warning: '#fabd2f',
|
||||
error: '#fb4934',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ExtensionManager', () => {
|
||||
let tempHomeDir: string;
|
||||
let tempWorkspaceDir: string;
|
||||
@@ -66,6 +86,7 @@ describe('ExtensionManager', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
themeManager.clearExtensionThemes();
|
||||
try {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
} catch (_e) {
|
||||
@@ -345,4 +366,185 @@ describe('ExtensionManager', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension Renaming', () => {
|
||||
it('should support renaming an extension during update', async () => {
|
||||
// 1. Setup existing extension
|
||||
const oldName = 'old-name';
|
||||
const newName = 'new-name';
|
||||
const extDir = path.join(userExtensionsDir, oldName);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: oldName, version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: extDir }),
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// 2. Create a temporary "new" version with a different name
|
||||
const newSourceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'new-source-'),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: newName, version: '1.1.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: newSourceDir }),
|
||||
);
|
||||
|
||||
// 3. Update the extension
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
{ type: 'local', source: newSourceDir },
|
||||
{ name: oldName, version: '1.0.0' },
|
||||
);
|
||||
|
||||
// 4. Verify old directory is gone and new one exists
|
||||
expect(fs.existsSync(path.join(userExtensionsDir, oldName))).toBe(false);
|
||||
expect(fs.existsSync(path.join(userExtensionsDir, newName))).toBe(true);
|
||||
|
||||
// Verify the loaded state is updated
|
||||
const extensions = extensionManager.getExtensions();
|
||||
expect(extensions.some((e) => e.name === newName)).toBe(true);
|
||||
expect(extensions.some((e) => e.name === oldName)).toBe(false);
|
||||
});
|
||||
|
||||
it('should carry over enablement status when renaming', async () => {
|
||||
const oldName = 'old-name';
|
||||
const newName = 'new-name';
|
||||
const extDir = path.join(userExtensionsDir, oldName);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: oldName, version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: extDir }),
|
||||
);
|
||||
|
||||
// Enable it
|
||||
const enablementManager = extensionManager.getEnablementManager();
|
||||
enablementManager.enable(oldName, true, tempHomeDir);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
const extension = extensionManager.getExtensions()[0];
|
||||
expect(extension.isActive).toBe(true);
|
||||
|
||||
const newSourceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'new-source-'),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: newName, version: '1.1.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: newSourceDir }),
|
||||
);
|
||||
|
||||
await extensionManager.installOrUpdateExtension(
|
||||
{ type: 'local', source: newSourceDir },
|
||||
{ name: oldName, version: '1.0.0' },
|
||||
);
|
||||
|
||||
// Verify new name is enabled
|
||||
expect(enablementManager.isEnabled(newName, tempHomeDir)).toBe(true);
|
||||
// Verify old name is removed from enablement
|
||||
expect(enablementManager.readConfig()[oldName]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prevent renaming if the new name conflicts with an existing extension', async () => {
|
||||
// Setup two extensions
|
||||
const ext1Dir = path.join(userExtensionsDir, 'ext1');
|
||||
fs.mkdirSync(ext1Dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ext1Dir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: 'ext1', version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(ext1Dir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: ext1Dir }),
|
||||
);
|
||||
|
||||
const ext2Dir = path.join(userExtensionsDir, 'ext2');
|
||||
fs.mkdirSync(ext2Dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ext2Dir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: 'ext2', version: '1.0.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(ext2Dir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: ext2Dir }),
|
||||
);
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// Try to update ext1 to name 'ext2'
|
||||
const newSourceDir = fs.mkdtempSync(
|
||||
path.join(tempHomeDir, 'new-source-'),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'gemini-extension.json'),
|
||||
JSON.stringify({ name: 'ext2', version: '1.1.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(newSourceDir, 'metadata.json'),
|
||||
JSON.stringify({ type: 'local', source: newSourceDir }),
|
||||
);
|
||||
|
||||
await expect(
|
||||
extensionManager.installOrUpdateExtension(
|
||||
{ type: 'local', source: newSourceDir },
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
),
|
||||
).rejects.toThrow(/already installed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('early theme registration', () => {
|
||||
it('should register themes with ThemeManager during loadExtensions for active extensions', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'themed-ext',
|
||||
version: '1.0.0',
|
||||
themes: [testTheme],
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
expect(themeManager.getCustomThemeNames()).toContain(
|
||||
'MyTheme (themed-ext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not register themes for inactive extensions', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'disabled-ext',
|
||||
version: '1.0.0',
|
||||
themes: [testTheme],
|
||||
});
|
||||
|
||||
// Disable the extension by creating an enablement override
|
||||
const manager = new ExtensionManager({
|
||||
enabledExtensionOverrides: ['none'],
|
||||
settings: createTestMergedSettings(),
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: null,
|
||||
});
|
||||
|
||||
await manager.loadExtensions();
|
||||
|
||||
expect(themeManager.getCustomThemeNames()).not.toContain(
|
||||
'MyTheme (disabled-ext)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +129,10 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
this.requestSetting = options.requestSetting ?? undefined;
|
||||
}
|
||||
|
||||
getEnablementManager(): ExtensionEnablementManager {
|
||||
return this.extensionEnablementManager;
|
||||
}
|
||||
|
||||
setRequestConsent(
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
): void {
|
||||
@@ -153,6 +157,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
async installOrUpdateExtension(
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
requestConsentOverride?: (consent: string) => Promise<boolean>,
|
||||
): Promise<GeminiCLIExtension> {
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
@@ -243,7 +248,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
(result.failureReason === 'no release data' &&
|
||||
installMetadata.type === 'git') ||
|
||||
// Otherwise ask the user if they would like to try a git clone.
|
||||
(await this.requestConsent(
|
||||
(await (requestConsentOverride ?? this.requestConsent)(
|
||||
`Error downloading github release for ${installMetadata.source} with the following error: ${result.errorMessage}.
|
||||
|
||||
Would you like to attempt to install via "git clone" instead?`,
|
||||
@@ -271,17 +276,28 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
|
||||
|
||||
const newExtensionName = newExtensionConfig.name;
|
||||
const previousName = previousExtensionConfig?.name ?? newExtensionName;
|
||||
const previous = this.getExtensions().find(
|
||||
(installed) => installed.name === newExtensionName,
|
||||
(installed) => installed.name === previousName,
|
||||
);
|
||||
const nameConflict = this.getExtensions().find(
|
||||
(installed) =>
|
||||
installed.name === newExtensionName &&
|
||||
installed.name !== previousName,
|
||||
);
|
||||
|
||||
if (isUpdate && !previous) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" was not already installed, cannot update it.`,
|
||||
`Extension "${previousName}" was not already installed, cannot update it.`,
|
||||
);
|
||||
} else if (!isUpdate && previous) {
|
||||
throw new Error(
|
||||
`Extension "${newExtensionName}" is already installed. Please uninstall it first.`,
|
||||
);
|
||||
} else if (isUpdate && nameConflict) {
|
||||
throw new Error(
|
||||
`Cannot update to "${newExtensionName}" because an extension with that name is already installed.`,
|
||||
);
|
||||
}
|
||||
|
||||
const newHasHooks = fs.existsSync(
|
||||
@@ -298,28 +314,60 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
path.join(localSourcePath, 'skills'),
|
||||
);
|
||||
const previousSkills = previous?.skills ?? [];
|
||||
const isMigrating = Boolean(
|
||||
previous &&
|
||||
previous.installMetadata &&
|
||||
previous.installMetadata.source !== installMetadata.source,
|
||||
);
|
||||
|
||||
await maybeRequestConsentOrFail(
|
||||
newExtensionConfig,
|
||||
this.requestConsent,
|
||||
requestConsentOverride ?? this.requestConsent,
|
||||
newHasHooks,
|
||||
previousExtensionConfig,
|
||||
previousHasHooks,
|
||||
newSkills,
|
||||
previousSkills,
|
||||
isMigrating,
|
||||
);
|
||||
const extensionId = getExtensionId(newExtensionConfig, installMetadata);
|
||||
const destinationPath = new ExtensionStorage(
|
||||
newExtensionName,
|
||||
).getExtensionDir();
|
||||
|
||||
if (
|
||||
(!isUpdate || newExtensionName !== previousName) &&
|
||||
fs.existsSync(destinationPath)
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot install extension "${newExtensionName}" because a directory with that name already exists. Please remove it manually.`,
|
||||
);
|
||||
}
|
||||
|
||||
let previousSettings: Record<string, string> | undefined;
|
||||
if (isUpdate) {
|
||||
let wasEnabledGlobally = false;
|
||||
let wasEnabledWorkspace = false;
|
||||
if (isUpdate && previousExtensionConfig) {
|
||||
const previousExtensionId = previous?.installMetadata
|
||||
? getExtensionId(previousExtensionConfig, previous.installMetadata)
|
||||
: extensionId;
|
||||
previousSettings = await getEnvContents(
|
||||
previousExtensionConfig,
|
||||
extensionId,
|
||||
previousExtensionId,
|
||||
this.workspaceDir,
|
||||
);
|
||||
await this.uninstallExtension(newExtensionName, isUpdate);
|
||||
if (newExtensionName !== previousName) {
|
||||
wasEnabledGlobally = this.extensionEnablementManager.isEnabled(
|
||||
previousName,
|
||||
homedir(),
|
||||
);
|
||||
wasEnabledWorkspace = this.extensionEnablementManager.isEnabled(
|
||||
previousName,
|
||||
this.workspaceDir,
|
||||
);
|
||||
this.extensionEnablementManager.remove(previousName);
|
||||
}
|
||||
await this.uninstallExtension(previousName, isUpdate);
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
@@ -392,6 +440,18 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
CoreToolCallStatus.Success,
|
||||
),
|
||||
);
|
||||
|
||||
if (newExtensionName !== previousName) {
|
||||
if (wasEnabledGlobally) {
|
||||
await this.enableExtension(newExtensionName, SettingScope.User);
|
||||
}
|
||||
if (wasEnabledWorkspace) {
|
||||
await this.enableExtension(
|
||||
newExtensionName,
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await logExtensionInstallEvent(
|
||||
this.telemetryConfig,
|
||||
@@ -504,7 +564,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
protected override async startExtension(extension: GeminiCLIExtension) {
|
||||
await super.startExtension(extension);
|
||||
if (extension.themes) {
|
||||
if (extension.themes && !themeManager.hasExtensionThemes(extension.name)) {
|
||||
themeManager.registerExtensionThemes(extension.name, extension.themes);
|
||||
}
|
||||
}
|
||||
@@ -564,6 +624,13 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
this.loadedExtensions = builtExtensions;
|
||||
|
||||
// Register extension themes early so they're available at startup.
|
||||
for (const ext of this.loadedExtensions) {
|
||||
if (ext.isActive && ext.themes) {
|
||||
themeManager.registerExtensionThemes(ext.name, ext.themes);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.loadedExtensions.map((ext) => this.maybeStartExtension(ext)),
|
||||
);
|
||||
@@ -873,6 +940,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
path: effectiveExtensionPath,
|
||||
contextFiles,
|
||||
installMetadata,
|
||||
migratedTo: config.migratedTo,
|
||||
mcpServers: config.mcpServers,
|
||||
excludeTools: config.excludeTools,
|
||||
hooks,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
loadSettings,
|
||||
createTestMergedSettings,
|
||||
SettingScope,
|
||||
resetSettingsCacheForTesting,
|
||||
} from './settings.js';
|
||||
import {
|
||||
isWorkspaceTrusted,
|
||||
@@ -161,6 +162,7 @@ describe('extension tests', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetSettingsCacheForTesting();
|
||||
keychainData = {};
|
||||
mockKeychainStorage = {
|
||||
getSecret: vi
|
||||
|
||||
@@ -42,6 +42,10 @@ export interface ExtensionConfig {
|
||||
*/
|
||||
directory?: string;
|
||||
};
|
||||
/**
|
||||
* Used to migrate an extension to a new repository source.
|
||||
*/
|
||||
migratedTo?: string;
|
||||
}
|
||||
|
||||
export interface ExtensionUpdateInfo {
|
||||
|
||||
@@ -13,14 +13,24 @@ import {
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import {
|
||||
ExtensionRegistryClient,
|
||||
type RegistryExtension,
|
||||
} from './extensionRegistryClient.js';
|
||||
import { fetchWithTimeout } from '@google/gemini-cli-core';
|
||||
import { fetchWithTimeout, resolveToRealPath } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
fetchWithTimeout: vi.fn(),
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
fetchWithTimeout: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
readFile: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockExtensions: RegistryExtension[] = [
|
||||
@@ -279,4 +289,32 @@ describe('ExtensionRegistryClient', () => {
|
||||
expect(ids).not.toContain('dataplex');
|
||||
expect(ids).toContain('conductor');
|
||||
});
|
||||
|
||||
it('should fetch extensions from a local file path', async () => {
|
||||
const filePath = '/path/to/extensions.json';
|
||||
const clientWithFile = new ExtensionRegistryClient(filePath);
|
||||
const mockReadFile = vi.mocked(fs.readFile);
|
||||
mockReadFile.mockResolvedValue(JSON.stringify(mockExtensions));
|
||||
|
||||
const result = await clientWithFile.getExtensions();
|
||||
expect(result.extensions).toHaveLength(3);
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
resolveToRealPath(filePath),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch extensions from a file:// URL', async () => {
|
||||
const fileUrl = 'file:///path/to/extensions.json';
|
||||
const clientWithFileUrl = new ExtensionRegistryClient(fileUrl);
|
||||
const mockReadFile = vi.mocked(fs.readFile);
|
||||
mockReadFile.mockResolvedValue(JSON.stringify(mockExtensions));
|
||||
|
||||
const result = await clientWithFileUrl.getExtensions();
|
||||
expect(result.extensions).toHaveLength(3);
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
resolveToRealPath(fileUrl),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { fetchWithTimeout } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import {
|
||||
fetchWithTimeout,
|
||||
resolveToRealPath,
|
||||
isPrivateIp,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { AsyncFzf } from 'fzf';
|
||||
|
||||
export interface RegistryExtension {
|
||||
@@ -29,12 +34,19 @@ export interface RegistryExtension {
|
||||
}
|
||||
|
||||
export class ExtensionRegistryClient {
|
||||
private static readonly REGISTRY_URL =
|
||||
static readonly DEFAULT_REGISTRY_URL =
|
||||
'https://geminicli.com/extensions.json';
|
||||
private static readonly FETCH_TIMEOUT_MS = 10000; // 10 seconds
|
||||
|
||||
private static fetchPromise: Promise<RegistryExtension[]> | null = null;
|
||||
|
||||
private readonly registryURI: string;
|
||||
|
||||
constructor(registryURI?: string) {
|
||||
this.registryURI =
|
||||
registryURI || ExtensionRegistryClient.DEFAULT_REGISTRY_URL;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static resetCache() {
|
||||
ExtensionRegistryClient.fetchPromise = null;
|
||||
@@ -97,18 +109,34 @@ export class ExtensionRegistryClient {
|
||||
return ExtensionRegistryClient.fetchPromise;
|
||||
}
|
||||
|
||||
const uri = this.registryURI;
|
||||
ExtensionRegistryClient.fetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetchWithTimeout(
|
||||
ExtensionRegistryClient.REGISTRY_URL,
|
||||
ExtensionRegistryClient.FETCH_TIMEOUT_MS,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch extensions: ${response.statusText}`);
|
||||
}
|
||||
if (uri.startsWith('http')) {
|
||||
if (isPrivateIp(uri)) {
|
||||
throw new Error(
|
||||
'Private IP addresses are not allowed for the extension registry.',
|
||||
);
|
||||
}
|
||||
const response = await fetchWithTimeout(
|
||||
uri,
|
||||
ExtensionRegistryClient.FETCH_TIMEOUT_MS,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch extensions: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return (await response.json()) as RegistryExtension[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return (await response.json()) as RegistryExtension[];
|
||||
} else {
|
||||
// Handle local file path
|
||||
const filePath = resolveToRealPath(uri);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return JSON.parse(content) as RegistryExtension[];
|
||||
}
|
||||
} catch (error) {
|
||||
ExtensionRegistryClient.fetchPromise = null;
|
||||
throw error;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="122" viewBox="0 0 920 122">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="122" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Migrating extension "old-ext" to a new repository, renaming to "test-ext", and installing updates. </text>
|
||||
<text x="0" y="36" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">The extension you are about to install may have been created by a third-party developer and sourced</text>
|
||||
<text x="0" y="53" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">from a public repository. Google does not vet, endorse, or guarantee the functionality or security</text>
|
||||
<text x="0" y="70" fill="#cdcd00" textLength="846" lengthAdjust="spacingAndGlyphs">of extensions. Please carefully inspect any extension and its source code before installing to</text>
|
||||
<text x="0" y="87" fill="#cdcd00" textLength="630" lengthAdjust="spacingAndGlyphs">understand the permissions it requires and the actions it may perform.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -24,6 +24,15 @@ of extensions. Please carefully inspect any extension and its source code before
|
||||
understand the permissions it requires and the actions it may perform."
|
||||
`;
|
||||
|
||||
exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if extension is migrated 1`] = `
|
||||
"Migrating extension "old-ext" to a new repository, renaming to "test-ext", and installing updates.
|
||||
|
||||
The extension you are about to install may have been created by a third-party developer and sourced
|
||||
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
|
||||
of extensions. Please carefully inspect any extension and its source code before installing to
|
||||
understand the permissions it requires and the actions it may perform."
|
||||
`;
|
||||
|
||||
exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if skills change 1`] = `
|
||||
"Installing extension "test-ext".
|
||||
This extension will run the following MCP servers:
|
||||
|
||||
@@ -287,6 +287,25 @@ describe('consent', () => {
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should request consent if extension is migrated', async () => {
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(
|
||||
baseConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
{ ...baseConfig, name: 'old-ext' },
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
let consentString = requestConsent.mock.calls[0][0] as string;
|
||||
consentString = normalizePathsForSnapshot(consentString, tempDir);
|
||||
await expectConsentSnapshot(consentString);
|
||||
});
|
||||
|
||||
it('should request consent if skills change', async () => {
|
||||
const skill1Dir = path.join(tempDir, 'skill1');
|
||||
const skill2Dir = path.join(tempDir, 'skill2');
|
||||
|
||||