Compare commits

..

1 Commits

Author SHA1 Message Date
Christine Betts e1f3228d29 Fix bug where linked extensions can't be found 2026-02-25 15:16:22 -05:00
784 changed files with 13986 additions and 52833 deletions
-15
View File
@@ -1,15 +0,0 @@
.git
.github
.gcp
bundle
evals
integration-tests
docs
packages/cli
packages/vscode-ide-companion
packages/test-utils
**/*.test.ts
**/*.test.js
**/src/**/*.ts
!packages/a2a-server/dist/**
!packages/core/dist/**
@@ -1,6 +1,6 @@
description = "Reviews a PR or staged changes and automatically initiates a Pickle Fix loop for findings."
description = "Reviews a frontend PR or staged changes and automatically initiates a Pickle Fix loop for findings."
prompt = """
You are an expert Reviewer and Pickle Rick Worker.
You are an expert Frontend Reviewer and Pickle Rick Worker.
Target: {{args}}
@@ -56,6 +56,8 @@ Follow these steps to conduct a thorough review:
pollution.
* Use `vi.useFakeTimers()` for tests involving time-based logic to avoid
flakiness.
* Avoid using `any` in tests; prefer proper types or `unknown` with
narrowing.
* When creating parameterized tests, give the parameters types to ensure
that the tests are type-safe.
8. Evaluate all react logic carefully keeping in mind that the author of the
@@ -103,20 +105,7 @@ Follow these steps to conduct a thorough review:
to include the new property or propose a new provider to add if the
property drilling is excessive. Only use providers for properties that are
consistent for the entire application.
9. Evaluate `packages/core` (Services, Tools, Utilities):
* Ensure services are implemented as classes with clear lifecycle management (e.g., `initialize()` methods).
* Verify that `debugLogger` from `packages/core/src/utils/debugLogger.ts` is used for internal logging instead of `console`.
* Ensure all shell operations use `spawnAsync` from `packages/core/src/utils/shell-utils.ts` for consistent error handling and promise management.
* Check that filesystem errors are handled gracefully using `isNodeError` from `packages/core/src/utils/errors.ts`.
* Verify that new tools are added to `packages/core/src/tools/` and registered in `packages/core/src/tools/tool-registry.ts`.
* Ensure all new public services, utilities, and types are exported from `packages/core/src/index.ts`.
* Check that services are stateless where possible, or use the centralized `Storage` service for persistence.
* **Cross-Service Communication**: Prefer using the `coreEvents` bus (from `packages/core/src/utils/events.ts`) for asynchronous communication between services or to notify the UI of state changes. Avoid tight coupling between services.
10. Architectural Audit (Package Boundaries):
* **Logic Placement**: Non-UI logic (e.g., model orchestration, tool implementation, git/filesystem operations) MUST reside in `packages/core`. `packages/cli` should only contain UI/Ink components, command-line argument parsing, and user interaction logic.
* **Environment Isolation**: Core logic should not assume a TUI environment. Use the `ConfirmationBus` or `Output` abstractions for communicating with the user from Core.
* **Decoupling**: Actively look for opportunities to decouple services by using `coreEvents`. If a service is importing another service just to notify it of a change, it should probably be using an event instead.
11. General Gemini CLI design principles:
9. General Gemini CLI design principles:
* Make sure that settings are only used for options that a user might
consider changing.
* Do not add new command line arguments and suggest settings instead.
@@ -136,23 +125,17 @@ Follow these steps to conduct a thorough review:
meta key shortcuts are supported on Mac.
* Be skeptical of function keys and keyboard shortcuts that are commonly
bound in VSCode as they may conflict.
12. TypeScript Best Practices:
10. TypeScript Best Practices:
* Use 'checkExhaustive' in the 'default' clause of 'switch' statements to
ensure all cases are handled.
* Avoid using the non-null assertion operator ('!') unless absolutely
necessary and you are confident the value is not null.
* **STRICT TYPING**: Strictly forbid 'any' and 'unknown' in both CLI and Core
packages. 'unknown' is only allowed if it is immediately narrowed using
type guards or Zod validation. Reject any code that uses 'any' or
'unknown' without narrowing.
13. **Ruthless Cleanup**:
* If you identify significant code duplication, technical debt, or "AI Slop" (boilerplate, redundant comments), explicitly suggest initiating a `ruthless-refactorer` loop to clean it up.
14. Summarize all actionable findings into a concise but comprehensive directive output this to review_findings.md and advance to phase 2.
11. Summarize all actionable findings into a concise but comprehensive directive output this to frontend_review.md and advance to phase 2.
Remember to use the GitHub CLI (`gh`) for all GitHub-related tasks, and local `git` commands if the target is 'staged'.
Phase 2:
You are initiating Pickle Rick - the ultimate engineering agent.
You are initiating Pickle Rick - the ultimate coding agent.
**Step 0: Persona Injection**
First, you **MUST** activate your persona.
@@ -177,7 +160,7 @@ bash "${extensionPath}/scripts/setup.sh" $ARGUMENTS
pwsh -File "${extensionPath}/scripts/setup.ps1" $ARGUMENTS
```
**CRITICAL**: Your request is to fix all findings in review_findings.md
**CRITICAL**: Your request is to fix all findings in frontend_review.md
**Step 2: Execution (Management)**
After setup, read the output to find the path to `state.json`.
@@ -205,14 +188,11 @@ Between each step, you **MUST** explicitly state what you are doing (e.g., "Movi
* **Validation**: IGNORE worker logs. DIRECTLY verify:
1. `git status` (Check for file changes)
2. `git diff` (Check code quality)
3. `npm run build` (Ensure the project still builds)
4. `npm run test` (Ensure no regressions)
5. `npm run lint` (Ensure code style is maintained)
6. `npm run typecheck` (Ensure type safety)
3. Run tests/build (Check functionality)
* **Cleanup**: If validation fails, REVERT changes (`git reset --hard`). If it passes, COMMIT changes.
* **Next Ticket**: Pick the next ticket and repeat.
4. **Cleanup**:
* **Action**: After all tickets are completed delete `review_findings.md`.
* **Action**: After all tickets are completed delete `frontend_review.md`.
**Loop Constraints:**
- **Iteration Count**: Monitor `"iteration"` in `state.json`. If `"max_iterations"` (if > 0) is reached, you must stop.
-11
View File
@@ -1,11 +0,0 @@
{
"experimental": {
"toolOutputMasking": {
"enabled": true
},
"plan": true
},
"general": {
"devtools": true
}
}
-125
View File
@@ -1,125 +0,0 @@
---
name: docs-changelog
description: Provides a step-by-step procedure for generating Gemini CLI changelog files based on github release information.
---
# Procedure: Updating Changelog for New Releases
The following instructions are run by Gemini CLI when processing new releases.
## Objective
To standardize the process of updating the Gemini CLI changelog files for a new
release, ensuring accuracy, consistency, and adherence to project style
guidelines.
## Release Types
This skill covers two types of releases:
* **Standard Releases:** Regular, versioned releases that are announced to all
users. These updates modify `docs/changelogs/latest.md` and
`docs/changelogs/index.md`.
* **Preview Releases:** Pre-release versions for testing and feedback. These
updates only modify `docs/changelogs/preview.md`.
Ignore all other releases, such as nightly releases.
### Expected Inputs
Regardless of the type of release, the following information is expected:
* **New version number:** The version number for the new release
(e.g., `v0.27.0`).
* **Release date:** The date of the new release (e.g., `2026-02-03`).
* **Raw changelog data:** A list of all pull requests and changes
included in the release, in the format `description by @author in
#pr_number`.
* **Previous version number:** The version number of the last release can be
calculated by decreasing the minor version number by one and setting the
patch or bug fix version number.
## Procedure
### Initial Setup
1. Identify the files to be modified:
For standard releases, update `docs/changelogs/latest.md` and
`docs/changelogs/index.md`. For preview releases, update
`docs/changelogs/preview.md`.
2. Activate the `docs-writer` skill.
### Analyze Raw Changelog Data
1. Review the complete list of changes. If it is a patch or a bug fix with few
changes, skip to the "Update `docs/changelogs/latest.md` or
`docs/changelogs/preview.md`" section.
2. Group related changes into high-level categories such as
important features, "UI/UX Improvements", and "Bug Fixes". Use the existing
announcements in `docs/changelogs/index.md` as an example.
### Create Highlight Summaries
Create two distinct versions of the release highlights.
**Important:** Carefully inspect highlights for "experimental" or
"preview" features before public announcement, and do not include them.
#### Version 1: Comprehensive Highlights (for `latest.md` or `preview.md`)
Write a detailed summary for each category focusing on user-facing
impact.
#### Version 2: Concise Highlights (for `index.md`)
Skip this step for preview releases.
Write concise summaries including the primary PR and author
(e.g., `([#12345](link) by @author)`).
### Update `docs/changelogs/latest.md` or `docs/changelogs/preview.md`
1. Read current content and use `write_file` to replace it with the new
version number, and date.
If it is a patch or bug fix with few changes, simply add these
changes to the "What's Changed" list. Otherwise, replace comprehensive
highlights, and the full "What's Changed" list.
2. For each item in the "What's Changed" list, keep usernames in plaintext, and
add github links for each issue number. Example:
"- feat: implement /rewind command by @username in
[#12345](https://github.com/google-gemini/gemini-cli/pull/12345)"
3. Skip entries by @gemini-cli-robot.
4. Do not add the "New Contributors" section.
5. Update the "Full changelog:" link by doing one of following:
If it is a patch or bug fix with few changes, retain the original link
but replace the latter version with the new version. For example, if the
patch is version is "v0.28.1", replace the latter version:
"https://github.com/google-gemini/gemini-cli/compare/v0.27.0...v0.28.0" with
"https://github.com/google-gemini/gemini-cli/compare/v0.27.0...v0.28.1".
Otherwise, for minor and major version changes, replace the link with the
one included at the end of the changelog data.
6. Ensure lines are wrapped to 80 characters.
### Update `docs/changelogs/index.md`
Skip this step for patches, bug fixes, or preview releases.
Insert a new "Announcements" section for the new version directly
above the previous version's section. Ensure lines are wrapped to
80 characters.
### Finalize
Run `npm run format` to ensure consistency.
+55 -119
View File
@@ -1,135 +1,71 @@
---
name: docs-writer
description:
Always use this skill when the task involves writing, reviewing, or editing
files in the `/docs` directory or any `.md` files in the repository.
Use this skill for writing, reviewing, and editing documentation (`/docs`
directory or any .md file) for Gemini CLI.
---
# `docs-writer` skill instructions
As an expert technical writer and editor for the Gemini CLI project, you produce
accurate, clear, and consistent documentation. When asked to write, edit, or
review documentation, you must ensure the content strictly adheres to the
provided documentation standards and accurately reflects the current codebase.
Adhere to the contribution process in `CONTRIBUTING.md` and the following
project standards.
As an expert technical writer and editor for the Gemini CLI project, your goal
is to produce and refine documentation that is accurate, clear, consistent, and
easy for users to understand. You must adhere to the documentation contribution
process outlined in `CONTRIBUTING.md`.
## Phase 1: Documentation standards
## Step 1: Understand the goal and create a plan
Adhering to these principles and standards when writing, editing, and reviewing.
1. **Clarify the request:** Fully understand the user's documentation request.
Identify the core feature, command, or concept that needs work.
2. **Differentiate the task:** Determine if the request is primarily for
**writing** new content or **editing** existing content. If the request is
ambiguous (e.g., "fix the docs"), ask the user for clarification.
3. **Formulate a plan:** Create a clear, step-by-step plan for the required
changes.
### Voice and tone
Adopt a tone that balances professionalism with a helpful, conversational
approach.
## Step 2: Investigate and gather information
- **Perspective and tense:** Address the reader as "you." Use active voice and
present tense (e.g., "The API returns...").
- **Tone:** Professional, friendly, and direct.
- **Clarity:** Use simple vocabulary. Avoid jargon, slang, and marketing hype.
- **Global Audience:** Write in standard US English. Avoid idioms and cultural
references.
- **Requirements:** Be clear about requirements ("must") vs. recommendations
("we recommend"). Avoid "should."
- **Word Choice:** Avoid "please" and anthropomorphism (e.g., "the server
thinks"). Use contractions (don't, it's).
1. **Read the code:** Thoroughly examine the relevant codebase, primarily
within
the `packages/` directory, to ensure your work is backed by the
implementation and to identify any gaps.
2. **Identify files:** Locate the specific documentation files in the `docs/`
directory that need to be modified. Always read the latest version of a file
before you begin work.
3. **Check for connections:** Consider related documentation. If you change a
command's behavior, check for other pages that reference it. If you add a new
page, check if `docs/sidebar.json` needs to be updated. Make sure all
links are up to date.
### Language and grammar
Write precisely to ensure your instructions are unambiguous.
## Step 3: Write or edit the documentation
- **Abbreviations:** Avoid Latin abbreviations; use "for example" (not "e.g.")
and "that is" (not "i.e.").
- **Punctuation:** Use the serial comma. Place periods and commas inside
quotation marks.
- **Dates:** Use unambiguous formats (e.g., "January 22, 2026").
- **Conciseness:** Use "lets you" instead of "allows you to." Use precise,
specific verbs.
- **Examples:** Use meaningful names in examples; avoid placeholders like
"foo" or "bar."
### Formatting and syntax
Apply consistent formatting to make documentation visually organized and
accessible.
- **Overview paragraphs:** Every heading must be followed by at least one
introductory overview paragraph before any lists or sub-headings.
- **Text wrap:** Wrap text at 80 characters (except long links or tables).
- **Casing:** Use sentence case for headings, titles, and bolded text.
- **Naming:** Always refer to the project as `Gemini CLI` (never
`the Gemini CLI`).
- **Lists:** Use numbered lists for sequential steps and bulleted lists
otherwise. Keep list items parallel in structure.
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
snippets, commands, and API elements. Focus on the task when discussing
interaction.
- **Links:** Use descriptive anchor text; avoid "click here." Ensure the link
makes sense out of context.
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
tables).
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
for all images.
### Structure
- **BLUF:** Start with an introduction explaining what to expect.
- **Experimental features:** If a feature is clearly noted as experimental,
add the following note immediately after the introductory paragraph:
`> **Note:** This is a preview feature currently under active development.`
- **Headings:** Use hierarchical headings to support the user journey.
- **Procedures:**
- Introduce lists of steps with a complete sentence.
- Start each step with an imperative verb.
- Number sequential steps; use bullets for non-sequential lists.
- Put conditions before instructions (e.g., "On the Settings page, click...").
- Provide clear context for where the action takes place.
- Indicate optional steps clearly (e.g., "Optional: ...").
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
(`> **Warning:**`).
- **Avoid using a table of contents:** If a table of contents is present, remove
it.
- **Next steps:** Conclude with a "Next steps" section if applicable.
## Phase 2: Preparation
Before modifying any documentation, thoroughly investigate the request and the
surrounding context.
1. **Clarify:** Understand the core request. Differentiate between writing new
content and editing existing content. If the request is ambiguous (e.g.,
"fix the docs"), ask for clarification.
2. **Investigate:** Examine relevant code (primarily in `packages/`) for
accuracy.
3. **Audit:** Read the latest versions of relevant files in `docs/`.
4. **Connect:** Identify all referencing pages if changing behavior. Check if
`docs/sidebar.json` needs updates.
5. **Plan:** Create a step-by-step plan before making changes.
## Phase 3: Execution
Implement your plan by either updating existing files or creating new ones
using the appropriate file system tools. Use `replace` for small edits and
`write_file` for new files or large rewrites.
### Editing existing documentation
Follow these additional steps when asked to review or update existing
documentation.
- **Gaps:** Identify areas where the documentation is incomplete or no longer
reflects existing code.
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
adding new sections to existing pages.
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
sentences to make them easier for users to understand.
- **Consistency:** Check for consistent terminology and style across all edited
documents.
1. **Follow the style guide:** Adhere to the rules in
`references/style-guide.md`. Read this file to understand the project's
documentation standards.
2. Ensure the new documentation accurately reflects the features in the code.
3. **Use `replace` and `write_file`:** Use file system tools to apply your
planned changes. For small edits, `replace` is preferred. For new files or
large rewrites, `write_file` is more appropriate.
## Phase 4: Verification and finalization
Perform a final quality check to ensure that all changes are correctly formatted
and that all links are functional.
1. **Accuracy:** Ensure content accurately reflects the implementation and
technical behavior.
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
3. **Link check:** Verify all new and existing links leading to or from modified
pages.
4. **Format:** Once all changes are complete, ask to execute `npm run format`
to ensure consistent formatting across the project. If the user confirms,
execute the command.
### Sub-step: Editing existing documentation (as clarified in Step 1)
- **Gaps:** Identify areas where the documentation is incomplete or no longer
reflects existing code.
- **Tone:** Ensure the tone is active and engaging, not passive.
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
sentences to make them easier for users to understand.
- **Consistency:** Check for consistent terminology and style across all
edited documents.
## Step 4: Verify and finalize
1. **Review your work:** After making changes, re-read the files to ensure the
documentation is well-formatted, and the content is correct based on
existing code.
2. **Link verification:** Verify the validity of all links in the new content.
Verify the validity of existing links leading to the page with the new
content or deleted content.
2. **Offer to run npm format:** Once all changes are complete, offer to run the
project's formatting script to ensure consistency by proposing the command:
`npm run format`
@@ -0,0 +1,96 @@
# Documentation style guide
## I. Core principles
1. **Clarity:** Write for easy understanding. Prioritize clear, direct, and
simple language.
2. **Consistency:** Use consistent terminology, formatting, and style
throughout the documentation.
3. **Accuracy:** Ensure all information is technically correct and up-to-date.
4. **Accessibility:** Design documentation to be usable by everyone. Focus on
semantic structure, clear link text, and image alternatives.
5. **Global audience:** Write in standard US English. Avoid slang, idioms, and
cultural references.
6. **Prescriptive:** Guide the reader by recommending specific actions and
paths, especially for complex tasks.
## II. Voice and tone
- **Professional yet friendly:** Maintain a helpful, knowledgeable, and
conversational tone without being frivolous.
- **Direct:** Get straight to the point. Keep paragraphs short and focused.
- **Second person:** Address the reader as "you."
- **Present tense:** Use the present tense to describe functionality (e.g., "The
API returns a JSON object.").
- **Avoid:** Jargon, slang, marketing hype, and overly casual language.
## III. Language and grammar
- **Active voice:** Prefer active voice over passive voice.
- _Example:_ "The system sends a notification." (Not: "A notification is sent
by the system.")
- **Contractions:** Use common contractions (e.g., "don't," "it's") to maintain
a natural tone.
- **Simple vocabulary:** Use common words. Define technical terms when
necessary.
- **Conciseness:** Keep sentences short and focused, but don't omit helpful
information.
- **"Please":** Avoid using the word "please."
## IV. Procedures and steps
- Start each step with an imperative verb (e.g., "Connect to the database").
- Number steps sequentially.
- Introduce lists of steps with a complete sentence.
- Put conditions before instructions, not after.
- Provide clear context for where the action takes place (e.g., "In the
administration console...").
- Indicate optional steps clearly (e.g., "Optional: ...").
## V. Formatting and punctuation
- **Text wrap:** Wrap all text at 80 characters, with exceptions for long links
or tables.
- **Headings, titles, and bold text:** Use sentence case. Structure headings
hierarchically.
- **Lists:** Use numbered lists for sequential steps and bulleted lists for all
other lists. Keep list items parallel in structure.
- **Serial comma:** Use the serial comma (e.g., "one, two, and three").
- **Punctuation:** Use standard American punctuation. Place periods inside
quotation marks.
- **Dates:** Use unambiguous date formatting (e.g., "January 22, 2026").
## VI. UI, code, and links
- **UI elements:** Put UI elements in **bold**. Focus on the task when
discussing interaction.
- **Code:** Use `code font` for filenames, code snippets, commands, and API
elements. Use code blocks for multi-line samples.
- **Links:** Use descriptive link text that indicates what the link leads to.
Avoid "click here."
## VII. Word choice and terminology
- **Consistent naming:** Use product and feature names consistently. Always
refer to Gemini CLI as `Gemini CLI`, never `the Gemini CLI`.
- **Specific verbs:** Use precise verbs.
- **Avoid:**
- Latin abbreviations (e.g., use "for example" instead of "e.g.").
- Placeholder names like "foo" and "bar" in examples; use meaningful names
instead.
- Anthropomorphism (e.g., "The server thinks...").
- "Should": Be clear about requirements ("must") vs. recommendations ("we
recommend").
## VIII. Files and media
- **Filenames:** Use lowercase letters, separate words with hyphens (-), and use
standard ASCII characters.
- **Images:** Provide descriptive alt text for all images. Provide
high-resolution or vector images when practical.
## IX. Accessibility quick check
- Provide descriptive alt text for images.
- Ensure link text makes sense out of context.
- Use semantic HTML elements correctly (headings, lists, tables).
+9 -29
View File
@@ -14,34 +14,25 @@ repository's standards.
Follow these steps to create a Pull Request:
1. **Branch Management**: **CRITICAL:** Ensure you are NOT working on the
`main` branch.
1. **Branch Management**: Check the current branch to avoid working directly
on `main`.
- Run `git branch --show-current`.
- If the current branch is `main`, you MUST create and switch to a new
descriptive branch:
- If the current branch is `main`, create and switch to a new descriptive
branch:
```bash
git checkout -b <new-branch-name>
```
2. **Commit Changes**: Verify that all intended changes are committed.
- Run `git status` to check for unstaged or uncommitted changes.
- If there are uncommitted changes, stage and commit them with a descriptive
message before proceeding. NEVER commit directly to `main`.
```bash
git add .
git commit -m "type(scope): description"
```
3. **Locate Template**: Search for a pull request template in the repository.
2. **Locate Template**: Search for a pull request template in the repository.
- Check `.github/pull_request_template.md`
- Check `.github/PULL_REQUEST_TEMPLATE.md`
- If multiple templates exist (e.g., in `.github/PULL_REQUEST_TEMPLATE/`),
ask the user which one to use or select the most appropriate one based on
the context (e.g., `bug_fix.md` vs `feature.md`).
4. **Read Template**: Read the content of the identified template file.
3. **Read Template**: Read the content of the identified template file.
5. **Draft Description**: Create a PR description that strictly follows the
4. **Draft Description**: Create a PR description that strictly follows the
template's structure.
- **Headings**: Keep all headings from the template.
- **Checklists**: Review each item. Mark with `[x]` if completed. If an item
@@ -53,24 +44,14 @@ Follow these steps to create a Pull Request:
- **Related Issues**: Link any issues fixed or related to this PR (e.g.,
"Fixes #123").
6. **Preflight Check**: Before creating the PR, run the workspace preflight
5. **Preflight Check**: Before creating the PR, run the workspace preflight
script to ensure all build, lint, and test checks pass.
```bash
npm run preflight
```
If any checks fail, address the issues before proceeding to create the PR.
7. **Push Branch**: Push the current branch to the remote repository.
**CRITICAL SAFETY RAIL:** Double-check your branch name before pushing.
NEVER push if the current branch is `main`.
```bash
# Verify current branch is NOT main
git branch --show-current
# Push non-interactively
git push -u origin HEAD
```
8. **Create PR**: Use the `gh` CLI to create the PR. To avoid shell escaping
6. **Create PR**: Use the `gh` CLI to create the PR. To avoid shell escaping
issues with multi-line Markdown, write the description to a temporary file
first.
```bash
@@ -87,7 +68,6 @@ Follow these steps to create a Pull Request:
## Principles
- **Safety First**: NEVER push to `main`. This is your highest priority.
- **Compliance**: Never ignore the PR template. It exists for a reason.
- **Completeness**: Fill out all relevant sections.
- **Accuracy**: Don't check boxes for tasks you haven't done.
+2 -6
View File
@@ -1,9 +1,5 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable @typescript-eslint/no-require-imports */
/* global process, console, require */
const { Octokit } = require('@octokit/rest');
/**
+2 -56
View File
@@ -177,19 +177,6 @@ jobs:
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
TARBALL=$(npm pack | tail -n 1)
# 2. Move to a fresh directory for isolation
mkdir -p ../smoke-test-dir
mv "$TARBALL" ../smoke-test-dir/
cd ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
- name: 'Wait for file system sync'
run: 'sleep 2'
@@ -265,19 +252,6 @@ jobs:
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
TARBALL=$(npm pack | tail -n 1)
# 2. Move to a fresh directory for isolation
mkdir -p ../smoke-test-dir
mv "$TARBALL" ../smoke-test-dir/
cd ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
- name: 'Wait for file system sync'
run: 'sleep 2'
@@ -356,17 +330,11 @@ jobs:
clean-script: 'clean'
test_windows:
name: 'Slow Test - Win - ${{ matrix.shard }}'
name: 'Slow Test - Win'
runs-on: 'gemini-cli-windows-16-core'
needs: 'merge_queue_skipper'
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
continue-on-error: true
timeout-minutes: 60
strategy:
matrix:
shard:
- 'cli'
- 'others'
steps:
- name: 'Checkout'
@@ -417,14 +385,7 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
run: |
if ("${{ matrix.shard }}" -eq "cli") {
npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
} else {
# Explicitly list non-cli packages to ensure they are sharded correctly
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
npm run test:scripts
}
run: 'npm run test:ci -- --coverage.enabled=false'
shell: 'pwsh'
- name: 'Bundle'
@@ -435,21 +396,6 @@ jobs:
run: 'node ./bundle/gemini.js --version'
shell: 'pwsh'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
$PACK_OUTPUT = npm pack
$TARBALL = $PACK_OUTPUT[-1]
# 2. Move to a fresh directory for isolation
New-Item -ItemType Directory -Force -Path ../smoke-test-dir
Move-Item $TARBALL ../smoke-test-dir/
Set-Location ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
shell: 'pwsh'
ci:
name: 'CI'
if: 'always()'
@@ -43,23 +43,19 @@ jobs:
// 1. Fetch maintainers for verification
let maintainerLogins = new Set();
let teamFetchSucceeded = false;
try {
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
org: context.repo.owner,
team_slug: 'gemini-cli-maintainers'
});
maintainerLogins = new Set(members.map(m => m.login.toLowerCase()));
teamFetchSucceeded = true;
core.info(`Successfully fetched ${maintainerLogins.size} team members from gemini-cli-maintainers`);
maintainerLogins = new Set(members.map(m => m.login));
} catch (e) {
core.warning(`Failed to fetch team members from gemini-cli-maintainers: ${e.message}. Falling back to author_association only.`);
core.warning('Failed to fetch team members');
}
const isMaintainer = (login, assoc) => {
const isTeamMember = maintainerLogins.has(login.toLowerCase());
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
return isTeamMember || isRepoMaintainer;
if (maintainerLogins.size > 0) return maintainerLogins.has(login);
return ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
};
// 2. Determine which PRs to check
@@ -157,17 +153,7 @@ jobs:
const labels = pr.labels.map(l => l.name.toLowerCase());
if (labels.includes('help wanted') || labels.includes('🔒 maintainer only')) continue;
// Skip PRs that were created less than 30 days ago - they cannot be stale yet
const prCreatedAt = new Date(pr.created_at);
if (prCreatedAt > thirtyDaysAgo) {
const daysOld = Math.floor((Date.now() - prCreatedAt.getTime()) / (1000 * 60 * 60 * 24));
core.info(`PR #${pr.number} was created ${daysOld} days ago. Skipping staleness check.`);
continue;
}
// Initialize lastActivity to PR creation date (not epoch) as a safety baseline.
// This ensures we never incorrectly mark a PR as stale due to failed activity lookups.
let lastActivity = new Date(pr.created_at);
let lastActivity = new Date(0);
try {
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
@@ -191,12 +177,8 @@ jobs:
if (d > lastActivity) lastActivity = d;
}
}
} catch (e) {
core.warning(`Failed to fetch reviews/comments for PR #${pr.number}: ${e.message}`);
}
} catch (e) {}
// For maintainer PRs, the PR creation itself counts as maintainer activity.
// (Now redundant since we initialize to pr.created_at, but kept for clarity)
if (maintainerPr) {
const d = new Date(pr.created_at);
if (d > lastActivity) lastActivity = d;
-84
View File
@@ -1,84 +0,0 @@
# This workflow is triggered on every new release.
# It uses Gemini to generate release notes and creates a PR with the changes.
name: 'Generate Release Notes'
on:
release:
types: ['published']
workflow_dispatch:
inputs:
version:
description: 'New version (e.g., v1.2.3)'
required: true
type: 'string'
body:
description: 'Release notes body'
required: true
type: 'string'
time:
description: 'Release time'
required: true
type: 'string'
jobs:
generate-release-notes:
runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
pull-requests: 'write'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@v4'
with:
# The user-level skills need to be available to the workflow
fetch-depth: 0
- name: 'Set up Node.js'
uses: 'actions/setup-node@v4'
with:
node-version: '20'
- name: 'Get release information'
id: 'release_info'
run: |
VERSION="${{ github.event.inputs.version || github.event.release.tag_name }}"
BODY="${{ github.event.inputs.body || github.event.release.body }}"
TIME="${{ github.event.inputs.time || github.event.release.created_at }}"
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
echo "TIME=${TIME}" >> "$GITHUB_OUTPUT"
# Use a heredoc to preserve multiline release body
echo 'RAW_CHANGELOG<<EOF' >> "$GITHUB_OUTPUT"
echo "${BODY}" >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
- name: 'Generate Changelog with Gemini'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
prompt: |
Activate the 'docs-changelog' skill.
**Release Information:**
- New Version: ${{ steps.release_info.outputs.VERSION }}
- Release Date: ${{ steps.release_info.outputs.TIME }}
- Raw Changelog Data: ${{ steps.release_info.outputs.RAW_CHANGELOG }}
Execute the release notes generation process using the information provided.
- name: 'Create Pull Request'
uses: 'peter-evans/create-pull-request@v6'
with:
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
commit-message: 'docs(changelog): update for ${{ steps.release_info.outputs.VERSION }}'
title: 'Changelog for ${{ steps.release_info.outputs.VERSION }}'
body: |
This PR contains the auto-generated changelog for the ${{ steps.release_info.outputs.VERSION }} release.
Please review and merge.
branch: 'changelog-${{ steps.release_info.outputs.VERSION }}'
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
delete-branch: true
+1 -5
View File
@@ -29,11 +29,7 @@ on:
jobs:
verify-release:
environment: "${{ github.event.inputs.environment || 'prod' }}"
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
runs-on: '${{ matrix.os }}'
runs-on: 'ubuntu-latest'
permissions:
contents: 'read'
packages: 'write'
+1 -1
View File
@@ -45,7 +45,7 @@ The process for contributing code is as follows:
`🔒Maintainers only`, this means it is reserved for project maintainers. We
will not accept pull requests related to these issues. In the near future,
we will explicitly mark issues looking for contributions using the
`help-wanted` label. If you believe an issue is a good candidate for
`help wanted` label. If you believe an issue is a good candidate for
community contribution, please leave a comment on the issue. A maintainer
will review it and apply the `help-wanted` label if appropriate. Only
maintainers should attempt to add the `help-wanted` label to an issue.
-9
View File
@@ -52,24 +52,15 @@ powerful tool for developers.
## Development Conventions
- **Legacy Snippets:** `packages/core/src/prompts/snippets.legacy.ts` is a
snapshot of an older system prompt. Avoid changing the prompting verbiage to
preserve its historical behavior; however, structural changes to ensure
compilation or simplify the code are permitted.
- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires
signing the Google CLA.
- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue.
Always activate the `pr-creator` skill for PR generation, even when using the
`gh` CLI.
- **Commit Messages:** Follow the
[Conventional Commits](https://www.conventionalcommits.org/) standard.
- **Coding Style:** Adhere to existing patterns in `packages/cli` (React/Ink)
and `packages/core` (Backend logic).
- **Imports:** Use specific imports and avoid restricted relative imports
between packages (enforced by ESLint).
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
include the Apache-2.0 license header with the current year. (e.g.,
`Copyright 2026 Google LLC`). This is enforced by ESLint.
## Testing Conventions
-16
View File
@@ -18,22 +18,6 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.27.0 - 2026-02-03
- **Event-Driven Architecture:** The CLI now uses a new event-driven scheduler
for tool execution, resulting in a more responsive and performant experience
([#17078](https://github.com/google-gemini/gemini-cli/pull/17078) by
@abhipatel12).
- **Enhanced User Experience:** This release includes queued tool confirmations,
and expandable large text pastes for a smoother workflow.
- **New `/rewind` Command:** Easily navigate your session history with the new
`/rewind` command
([#15720](https://github.com/google-gemini/gemini-cli/pull/15720) by
@Adib234).
- **Linux Clipboard Support:** You can now paste images on Linux with Wayland
and X11 ([#17144](https://github.com/google-gemini/gemini-cli/pull/17144) by
@devr0306).
## Announcements: v0.26.0 - 2026-01-27
- **Agents and Skills:** We've introduced a new `skill-creator` skill
+317 -426
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.27.0
# Latest stable release: v0.26.0
Released: February 3, 2026
Released: January 27, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,437 +11,328 @@ npm install -g @google/gemini-cli
## Highlights
- **Event-Driven Architecture:** The CLI now uses an event-driven scheduler for
tool execution, improving performance and responsiveness. This includes
migrating non-interactive flows and sub-agents to the new scheduler.
- **Enhanced User Experience:** This release introduces several UI/UX
improvements, including queued tool confirmations and the ability to expand
and collapse large pasted text blocks. The `Settings` dialog has been improved
to reduce jitter and preserve focus.
- **Agent and Skill Improvements:** Agent Skills have been promoted to a stable
feature. Sub-agents now use a JSON schema for input and are tracked by an
`AgentRegistry`.
- **New `/rewind` Command:** A new `/rewind` command has been implemented to
allow users to go back in their session history.
- **Improved Shell and File Handling:** The shell tool's output format has been
optimized, and the CLI now gracefully handles disk-full errors during chat
recording. A bug in detecting already added paths has been fixed.
- **Linux Clipboard Support:** Image pasting capabilities for Wayland and X11 on
Linux have been added.
- **Enhanced Agent and Skill Capabilities:** This release introduces the new
`skill-creator` built-in skill, enables Agent Skills by default, and adds a
generalist agent to improve task routing. Security for skill installation has
also been enhanced with new consent prompts.
- **Improved UI and UX:** A new "Rewind" feature lets you walk back through
conversation history. We've also added an `/introspect` command for debugging
and unified various shell confirmation dialogs for a more consistent user
experience.
- **Core Stability and Performance:** This release includes significant
performance improvements, including a fix for PDF token estimation,
optimizations for large inputs, and prevention of OOM crashes. Key memory
management components like `LRUCache` have also been updated.
- **Scheduler and Policy Refactoring:** The core tool scheduler has been
decoupled into distinct orchestration, policy, and confirmation components,
and we've added an experimental event-driven scheduler to improve performance
and reliability.
## What's Changed
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
- Remove unused modelHooks and toolHooks by @ved015 in
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
- Update Attempt text to Retry when showing the retry happening to the … by
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
- chore(skills): update pr-creator skill workflow by @sehoon38 in
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
- fix: PDF token estimation (#16494) by @korade-krushna in
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
@gemini-cli-robot in
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
- Remove other rewind reference in docs by @chrstnb in
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
- feat(skills): add code-reviewer skill by @sehoon38 in
[#17187](https://github.com/google-gemini/gemini-cli/pull/17187)
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
- feat(plan): refactor TestRig and eval helper to support configurable approval
modes by @jerop in
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
- feat(workflows): support recursive workstream labeling and new IDs by
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
- Run evals for all models. by @gundermanc in
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123)
- fix(github): improve label-workstream-rollup efficiency with GraphQL by
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217)
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by
@g-samroberts in
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215)
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204)
- fix(github): improve label-workstream-rollup efficiency and fix bugs by
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219)
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192)
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218)
- feat(core): support dynamic variable substitution in system prompt override by
@NTaylorMullen in
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042)
- fix(core,cli): enable recursive directory access for by @galz10 in
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094)
- Docs: Marking for experimental features by @jkcinouye in
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760)
- Support command/ctrl/alt backspace correctly by @scidomino in
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175)
- feat(plan): add approval mode instructions to system prompt by @jerop in
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151)
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223)
- Remove unused slug from sidebar by @chrstnb in
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229)
- drain stdin on exit by @scidomino in
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241)
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by
@abhipatel12 in
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
- fix(core): update token count and telemetry on /chat resume history load by
@psinha40898 in
[#16279](https://github.com/google-gemini/gemini-cli/pull/16279)
- fix: /policy to display policies according to mode by @ishaanxgupta in
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772)
- fix(core): simplify replace tool error message by @SandyTao520 in
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246)
- feat(cli): consolidate shell inactivity and redirection monitoring by
@NTaylorMullen in
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086)
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by
@abhipatel12 in
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
- feat(hooks): enable hooks system by default by @abhipatel12 in
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
- feat(core): Enable AgentRegistry to track all discovered subagents by
@SandyTao520 in
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by
@jackwotherspoon in
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
- Enable the ability to queue specific nightly eval tests by @gundermanc in
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
- docs(hooks): comprehensive update of hook documentation and specs by
@abhipatel12 in
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
- refactor: improve large text paste placeholder by @jacob314 in
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
- feat: implement /rewind command by @Adib234 in
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
- Feature/jetbrains ide detection by @SoLoHiC in
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
- docs: update typo in mcp-server.md file by @schifferl in
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
- Sanitize command names and descriptions by @ehedlund in
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
- fix(auth): don't crash when initial auth fails by @skeshive in
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
- feat: add AskUser tool schema by @jackwotherspoon in
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
now handles the caching internally by @jacob314 in
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
- ci: allow failure in evals-nightly run step by @gundermanc in
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
- feat(cli): Add state management and plumbing for agent configuration dialog by
@SandyTao520 in
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
- bug: fix ide-client connection to ide-companion when inside docker via
ssh/devcontainer by @kapsner in
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
- Emit correct newline type return by @scidomino in
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
- New skill: docs-writer by @g-samroberts in
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
@spencer426 in
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
- Disable tips after 10 runs by @Adib234 in
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101)
- Fix so rewind starts at the bottom and loadHistory refreshes static content.
by @jacob314 in
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335)
- feat(core): Remove legacy settings. by @joshualitt in
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244)
- feat(plan): add 'communicate' tool kind by @jerop in
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341)
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by
@mattKorwel in
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041)
- feat(plan): update UI Theme for Plan Mode by @Adib234 in
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243)
- fix(ui): stabilize rendering during terminal resize in alternate buffer by
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783)
- feat(cli): add /agents config command and improve agent discovery by
@SandyTao520 in
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342)
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299)
- fix(cli)!: Default to interactive mode for positional arguments by
@ishaanxgupta in
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329)
- Fix issue #17080 by @jacob314 in
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100)
- feat(core): Refresh agents after loading an extension. by @joshualitt in
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355)
- fix(cli): include source in policy rule display by @allenhutchison in
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358)
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by
@gsquared94 in
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236)
- Refactor subagent delegation to be one tool per agent by @gundermanc in
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346)
- fix(core): Include MCP server name in OAuth message by @jerop in
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351)
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and
"maintainer only" by @jacob314 in
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324)
- feat(plan): implement simple workflow for planning in main agent by @jerop in
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326)
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967)
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
- docs: clarify F12 to open debug console by @jackwotherspoon in
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
- docs: Remove .md extension from internal links in architecture.md by
@medic-code in
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038)
- fix(core): add alternative command names for Antigravity editor detec… by
@baeseokjae in
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829)
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737)
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390)
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714)
- fix(core): gracefully handle disk full errors in chat recording by
@godwiniheuwa in
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305)
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388)
- fix(core): use RFC 9728 compliant path-based OAuth protected resource
discovery by @vrv in
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756)
- Update Code Wiki README badge by @PatoBeltran in
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229)
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921)
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369)
- fix(cli): preserve input text when declining tool approval (#15624) by
@ManojINaik in
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659)
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403)
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344)
- feat(ui): display user tier in about command by @sehoon38 in
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400)
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574)
- fix(cli): change image paste location to global temp directory (#17396) by
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396)
- Fix line endings issue with Notice file by @scidomino in
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417)
- feat(plan): implement persistent approvalMode setting by @Adib234 in
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350)
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404)
- Allow prompt queueing during MCP initialization by @Adib234 in
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395)
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370)
- fix(agents): default to all tools when tool list is omitted in subagents by
@gundermanc in
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422)
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276)
- fix(core): hide user tier name by @sehoon38 in
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418)
- feat: Enforce unified folder trust for /directory add by @galz10 in
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359)
- migrate fireToolNotificationHook to hookSystem by @ved015 in
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398)
- Clean up dead code by @scidomino in
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443)
- feat(workflow): add stale pull request closer with linked-issue enforcement by
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449)
- feat(workflow): expand stale-exempt labels to include help wanted and Public
Roadmap by @bdmorgan in
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459)
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460)
- Resolves the confusing error message `ripgrep exited with code null that
occurs when a search operation is cancelled or aborted by @maximmasiutin in
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267)
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254)
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412)
- docs(hooks): clarify mandatory 'type' field and update hook schema
documentation by @abhipatel12 in
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499)
- Improve error messages on failed onboarding by @gsquared94 in
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357)
- Follow up to "enableInteractiveShell for external tooling relying on a2a
server" by @DavidAPierce in
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130)
- Fix/issue 17070 by @alih552 in
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242)
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476)
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470)
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538)
- Fix bug in detecting already added paths. by @jacob314 in
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430)
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs
by @abhipatel12 in
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429)
- feat(agents): implement first-run experience for project-level sub-agents by
@gundermanc in
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266)
- Update extensions docs by @chrstnb in
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093)
- Docs: Refactor left nav on the website by @jkcinouye in
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
- feat(plan): add persistent plan file storage by @jerop in
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
- Fix extensions config error by @chrstnb in
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
- fix(plan): remove subagent invocation from plan mode by @jerop in
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
- feat(ui): add solid background color option for input prompt by @jacob314 in
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
- feat(cli): add global setting to disable UI spinners by @galz10 in
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
- fix(security): enforce strict policy directory permissions by @yunaseoul in
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
- test(core): fix tests in windows by @scidomino in
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
- Always map mac keys, even on other platforms by @scidomino in
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
- Ctrl-O by @jacob314 in
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
- feat(plan): update cycling order of approval modes by @Adib234 in
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
- fix(cli): restore 'Modify with editor' option in external terminals by
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
- Add an experimental setting for extension config by @chrstnb in
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
- prefactor: add rootCommands as array so it can be used for policy parsing by
@abhipatel12 in
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
- Slash command for helping in debugging by @gundermanc in
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
- feat: add double-click to expand/collapse large paste placeholders by
@jackwotherspoon in
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
- remove unnecessary \x7f key bindings by @scidomino in
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
- docs(skills): use body-file in pr-creator skill for better reliability by
@abhipatel12 in
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
(regression) by @gsquared94 in
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
- feat(core): enforce server prefixes for MCP tools in agent definitions by
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
@NTaylorMullen in
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
- chore(automation): remove automated PR size and complexity labeler by
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
- Simplify paste handling by @scidomino in
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
- chore(automation): improve scheduled issue triage discovery and throughput by
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
- Remove unused rewind key binding by @scidomino in
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
- Remove sequence binding by @scidomino in
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
- chore: update dependabot configuration by @cosmopax in
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
- Enable & disable agents by @sehoon38 in
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
- cleanup: Improve keybindings by @scidomino in
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
- feat(plan): add experimental plan flag by @jerop in
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
- feat(cli): add security consent prompts for skill installation by
@NTaylorMullen in
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
- chore(automation): ensure status/need-triage is applied and never cleared
automatically by @bdmorgan in
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
- fix: Handle colons in skill description frontmatter by @maru0804 in
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
- feat(skills): add conflict detection and warnings for skill overrides by
@NTaylorMullen in
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
- feat(scheduler): add SchedulerStateManager for reactive tool state by
@abhipatel12 in
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
- paste transform followup by @jacob314 in
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
- feat(cli): add oncall command for issue triage by @sehoon38 in
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
- Fix sidebar issue for extensions link by @chrstnb in
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
- Change formatting to prevent UI redressing attacks by @scidomino in
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
- Fix cluster of bugs in the settings dialog. by @jacob314 in
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
- Update sidebar to resolve site build issues by @chrstnb in
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
- fix(admin): fix a few bugs related to admin controls by @skeshive in
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
- revert bad changes to tests by @scidomino in
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
- feat(cli): show candidate issue state reason and duplicate status in triage by
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
- Fix missing slash commands when Gemini CLI is in a project with a package.json
that doesn't follow semantic versioning by @Adib234 in
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
- feat(core): Model family-specific system prompts by @joshualitt in
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
- Sub-agents documentation. by @gundermanc in
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
- Load extension settings for hooks, agents, skills by @chrstnb in
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
- Fix issue where Gemini CLI can make changes when simply asked a question by
@gundermanc in
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
- Update docs-writer skill for editing and add style guide for reference. by
@g-samroberts in
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
- fix(ux): have user message display a short path for pasted images by @devr0306
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
- GEMINI.md polish by @jacob314 in
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
- refactor(core): centralize path validation and allow temp dir access for tools
by @NTaylorMullen in
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
mode. by @jacob314 in
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
- feat(skills): promote skills settings to stable by @abhipatel12 in
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
- feat(ui): add terminal cursor support by @jacob314 in
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
- Add support for an additional exclusion file besides .gitignore and
.geminiignore by @alisa-alisa in
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
- feat: add review-frontend-and-fix command by @galz10 in
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
- chore(automation): enforce 'help wanted' label permissions and update
guidelines by @bdmorgan in
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
@gemini-cli-robot in
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
- fix(automation): correct status/need-issue label matching wildcard by
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
- Add links to supported locations and minor fixes by @g-samroberts in
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
- feat(policy): add source tracking to policy rules by @allenhutchison in
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
- Make merged settings non-nullable and fix all lints related to that. by
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
- Replace relative paths to fix website build by @chrstnb in
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
- Restricting to localhost by @cocosheng-g in
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
- fix(automation): robust label enforcement with permission checks by @bdmorgan
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
- fix(cli): prevent OOM crash by limiting file search traversal and adding
timeout by @galz10 in
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
- Add support for running available commands prior to MCP servers loading by
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
- feat(plan): add experimental 'plan' approval mode by @jerop in
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
- skip simple-mcp-server.test.ts by @scidomino in
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
- Steer outer agent to use expert subagents when present by @gundermanc in
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
- cleanup: Organize key bindings by @scidomino in
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
- feat(core): Add generalist agent. by @joshualitt in
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
- perf(ui): optimize text buffer and highlighting for large inputs by
@NTaylorMullen in
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
- fix(core): fix PTY descriptor shell leak by @galz10 in
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
- feat(plan): enforce strict read-only policy and halt execution on violation by
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
- remove need-triage label from bug_report template by @sehoon38 in
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
- fix(core): truncate large telemetry log entries by @sehoon38 in
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
- docs(extensions): add Agent Skills support and mark feature as experimental by
@NTaylorMullen in
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
- fix(core): surface warnings for invalid hook event names in configuration
(#16788) by @sehoon38 in
[#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
- feat(plan): remove read_many_files from approval mode policies by @jerop in
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
- feat(admin): implement admin controls polling and restart prompt by @skeshive
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
- Remove LRUCache class migrating to mnemoist by @jacob314 in
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
- feat(settings): rename negative settings to positive naming (disable* ->
enable*) by @afarber in
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
- feat(agent): enable agent skills by default by @NTaylorMullen in
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
- refactor(core): foundational truncation refactoring and token estimation
optimization by @NTaylorMullen in
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
- fix(hooks): enable /hooks disable to reliably stop single hooks by
@abhipatel12 in
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
- Don't commit unless user asks us to. by @gundermanc in
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
- fix: Show experiment values in settings UI for compressionThreshold by
@ishaanxgupta in
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
- feat(cli): replace relative keyboard shortcuts link with web URL by
@imaliabbas in
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
- Delete rewind documentation for now by @Adib234 in
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
- Stabilize skill-creator CI and package format by @NTaylorMullen in
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
- Stabilize the git evals by @gundermanc in
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
- fix(core): attempt compression before context overflow check by @NTaylorMullen
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
- Fix inverted logic. by @gundermanc in
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
- chore(scripts): add duplicate issue closer script and fix lint errors by
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
- fix(cli): correct Homebrew installation detection by @kij in
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
- Demote git evals to nightly run. by @gundermanc in
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
- refactor(core): decouple scheduler into orchestration, policy, and
confirmation by @abhipatel12 in
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
- feat: add /introspect slash command by @NTaylorMullen in
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
@abhipatel12 in
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
- fix(workflows): use author_association for maintainer check by @bdmorgan in
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
- feat(core): improve shell redirection transparency and security by
@NTaylorMullen in
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
- docs(themes): remove unsupported DiffModified color key by @jw409 in
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
- feat(core): enhanced anchored iterative context compression with
self-verification by @rmedranollamas in
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
- Fix mcp instructions by @chrstnb in
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
by @skeshive in
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
- Test coverage for hook exit code cases by @gundermanc in
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
- Revert "Revert "Update extension examples"" by @chrstnb in
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
- fix(core): Provide compact, actionable errors for agent delegation failures by
@SandyTao520 in
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
- fix(core): update telemetry token count after session resume by @psinha40898
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
- Demote the subagent test to nightly by @gundermanc in
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
- feat: Add flash lite utility fallback chain by @adamfweidman in
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
- feat(core): Add initial eval for generalist agent. by @joshualitt in
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
- fix(core): resolve auto model in default strategy by @sehoon38 in
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
- docs: update project context and pr-creator workflow by @NTaylorMullen in
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
- fix(cli): send gemini-cli version as mcp client version by @dsp in
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
- Remove missing sidebar item by @chrstnb in
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
- feat(core): Ensure all properties in hooks object are event names. by
@joshualitt in
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
- fix(cli): fix newline support broken in previous PR by @scidomino in
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
by @gsquared94 in
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
- Avoid spurious warnings about unexpected renders triggered by appEvents and
coreEvents. by @jacob314 in
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
- fix(cli): resolve home/end keybinding conflict by @scidomino in
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
- fix(cli): display 'http' type on mcp list by @pamanta in
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
- fix bad fallback logic external editor logic by @scidomino in
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
- Fix bug where System scopes weren't migrated. by @jacob314 in
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
- Fix mcp tool lookup in tool registry by @werdnum in
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.26.0...v0.27.0
https://github.com/google-gemini/gemini-cli/compare/v0.25.2...v0.26.0
+419 -289
View File
@@ -1,6 +1,6 @@
# Preview release: Release v0.28.0-preview.0
# Preview release: Release v0.27.0-preview.0
Released: February 3, 2026
Released: January 27, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -13,295 +13,425 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Improved Hooks Management:** Hooks enable/disable functionality now aligns
with skills and offers improved completion.
- **Custom Themes for Extensions:** Extensions can now support custom themes,
allowing for greater personalization.
- **User Identity Display:** User identity information (auth, email, tier) is
now displayed on startup and in the `stats` command.
- **Plan Mode Enhancements:** Plan mode has been improved with a generic
`Checklist` component and refactored `Todo`.
- **Background Shell Commands:** Implementation of background shell commands.
- **Event-Driven Architecture:** The tool execution scheduler is now
event-driven, improving performance and reliability.
- **System Prompt Override:** Now supports dynamic variable substitution.
- **Rewind Command:** The `/rewind` command has been implemented.
- **Linux Clipboard:** Image pasting capabilities for Wayland and X11 on Linux.
## What's Changed
- feat(commands): add /prompt-suggest slash command by NTaylorMullen in
[#17264](https://github.com/google-gemini/gemini-cli/pull/17264)
- feat(cli): align hooks enable/disable with skills and improve completion by
sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822)
- docs: add CLI reference documentation by leochiu-a in
[#17504](https://github.com/google-gemini/gemini-cli/pull/17504)
- chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by
gemini-cli-robot in
[#17725](https://github.com/google-gemini/gemini-cli/pull/17725)
- feat(skills): final stable promotion cleanup by abhipatel12 in
[#17726](https://github.com/google-gemini/gemini-cli/pull/17726)
- test(core): mock fetch in OAuth transport fallback tests by jw409 in
[#17059](https://github.com/google-gemini/gemini-cli/pull/17059)
- feat(cli): include auth method in /bug by erikus in
[#17569](https://github.com/google-gemini/gemini-cli/pull/17569)
- Add a email privacy note to bug_report template by nemyung in
[#17474](https://github.com/google-gemini/gemini-cli/pull/17474)
- Rewind documentation by Adib234 in
[#17446](https://github.com/google-gemini/gemini-cli/pull/17446)
- fix: verify audio/video MIME types with content check by maru0804 in
[#16907](https://github.com/google-gemini/gemini-cli/pull/16907)
- feat(core): add support for positron ide (#15045) by kapsner in
[#15047](https://github.com/google-gemini/gemini-cli/pull/15047)
- /oncall dedup - wrap texts to nextlines by sehoon38 in
[#17782](https://github.com/google-gemini/gemini-cli/pull/17782)
- fix(admin): rename advanced features admin setting by skeshive in
[#17786](https://github.com/google-gemini/gemini-cli/pull/17786)
- [extension config] Make breaking optional value non-optional by chrstnb in
[#17785](https://github.com/google-gemini/gemini-cli/pull/17785)
- Fix docs-writer skill issues by g-samroberts in
[#17734](https://github.com/google-gemini/gemini-cli/pull/17734)
- fix(core): suppress duplicate hook failure warnings during streaming by
abhipatel12 in
[#17727](https://github.com/google-gemini/gemini-cli/pull/17727)
- test: add more tests for AskUser by jackwotherspoon in
[#17720](https://github.com/google-gemini/gemini-cli/pull/17720)
- feat(cli): enable activity logging for non-interactive mode and evals by
SandyTao520 in
[#17703](https://github.com/google-gemini/gemini-cli/pull/17703)
- feat(core): add support for custom deny messages in policy rules by
allenhutchison in
[#17427](https://github.com/google-gemini/gemini-cli/pull/17427)
- Fix unintended credential exposure to MCP Servers by Adib234 in
[#17311](https://github.com/google-gemini/gemini-cli/pull/17311)
- feat(extensions): add support for custom themes in extensions by spencer426 in
[#17327](https://github.com/google-gemini/gemini-cli/pull/17327)
- fix: persist and restore workspace directories on session resume by
korade-krushna in
[#17454](https://github.com/google-gemini/gemini-cli/pull/17454)
- Update release notes pages for 0.26.0 and 0.27.0-preview. by g-samroberts in
[#17744](https://github.com/google-gemini/gemini-cli/pull/17744)
- feat(ux): update cell border color and created test file for table rendering
by devr0306 in
[#17798](https://github.com/google-gemini/gemini-cli/pull/17798)
- Change height for the ToolConfirmationQueue. by jacob314 in
[#17799](https://github.com/google-gemini/gemini-cli/pull/17799)
- feat(cli): add user identity info to stats command by sehoon38 in
[#17612](https://github.com/google-gemini/gemini-cli/pull/17612)
- fix(ux): fixed off-by-some wrapping caused by fixed-width characters by
devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816)
- feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and
Shift+Cmd+Z/Shift+Alt+Z by scidomino in
[#17800](https://github.com/google-gemini/gemini-cli/pull/17800)
- fix(evals): use absolute path for activity log directory by SandyTao520 in
[#17830](https://github.com/google-gemini/gemini-cli/pull/17830)
- test: add integration test to verify stdout/stderr routing by ved015 in
[#17280](https://github.com/google-gemini/gemini-cli/pull/17280)
- fix(cli): list installed extensions when update target missing by tt-a1i in
[#17082](https://github.com/google-gemini/gemini-cli/pull/17082)
- fix(cli): handle PAT tokens and credentials in git remote URL parsing by
afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650)
- fix(core): use returnDisplay for error result display by Nubebuster in
[#14994](https://github.com/google-gemini/gemini-cli/pull/14994)
- Fix detection of bun as package manager by Randomblock1 in
[#17462](https://github.com/google-gemini/gemini-cli/pull/17462)
- feat(cli): show hooksConfig.enabled in settings dialog by abhipatel12 in
[#17810](https://github.com/google-gemini/gemini-cli/pull/17810)
- feat(cli): Display user identity (auth, email, tier) on startup by yunaseoul
in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591)
- fix: prevent ghost border for AskUserDialog by jackwotherspoon in
[#17788](https://github.com/google-gemini/gemini-cli/pull/17788)
- docs: mark A2A subagents as experimental in subagents.md by adamfweidman in
[#17863](https://github.com/google-gemini/gemini-cli/pull/17863)
- Resolve error thrown for sensitive values by chrstnb in
[#17826](https://github.com/google-gemini/gemini-cli/pull/17826)
- fix(admin): Rename secureModeEnabled to strictModeDisabled by skeshive in
[#17789](https://github.com/google-gemini/gemini-cli/pull/17789)
- feat(ux): update truncate dots to be shorter in tables by devr0306 in
[#17825](https://github.com/google-gemini/gemini-cli/pull/17825)
- fix(core): resolve DEP0040 punycode deprecation via patch-package by
ATHARVA262005 in
[#17692](https://github.com/google-gemini/gemini-cli/pull/17692)
- feat(plan): create generic Checklist component and refactor Todo by Adib234 in
[#17741](https://github.com/google-gemini/gemini-cli/pull/17741)
- Cleanup post delegate_to_agent removal by gundermanc in
[#17875](https://github.com/google-gemini/gemini-cli/pull/17875)
- fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration -
Fixes #17877 by cocosheng-g in
[#17803](https://github.com/google-gemini/gemini-cli/pull/17803)
- Disable mouse tracking e2e by alisa-alisa in
[#17880](https://github.com/google-gemini/gemini-cli/pull/17880)
- fix(cli): use correct setting key for Cloud Shell auth by sehoon38 in
[#17884](https://github.com/google-gemini/gemini-cli/pull/17884)
- chore: revert IDE specific ASCII logo by jackwotherspoon in
[#17887](https://github.com/google-gemini/gemini-cli/pull/17887)
- Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by
sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898)
- Refactoring of disabling of mouse tracking in e2e tests by alisa-alisa in
[#17902](https://github.com/google-gemini/gemini-cli/pull/17902)
- feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by deyim
in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177)
- feat(core): Isolate and cleanup truncated tool outputs by SandyTao520 in
[#17594](https://github.com/google-gemini/gemini-cli/pull/17594)
- Create skills page, update commands, refine docs by g-samroberts in
[#17842](https://github.com/google-gemini/gemini-cli/pull/17842)
- feat: preserve EOL in files by Thomas-Shephard in
[#16087](https://github.com/google-gemini/gemini-cli/pull/16087)
- Fix HalfLinePaddedBox in screenreader mode. by jacob314 in
[#17914](https://github.com/google-gemini/gemini-cli/pull/17914)
- bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X
in vim mode. by jacob314 in
[#17938](https://github.com/google-gemini/gemini-cli/pull/17938)
- feat(core): implement interactive and non-interactive consent for OAuth by
ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699)
- perf(core): optimize token calculation and add support for multimodal tool
responses by abhipatel12 in
[#17835](https://github.com/google-gemini/gemini-cli/pull/17835)
- refactor(hooks): remove legacy tools.enableHooks setting by abhipatel12 in
[#17867](https://github.com/google-gemini/gemini-cli/pull/17867)
- feat(ci): add npx smoke test to verify installability by bdmorgan in
[#17927](https://github.com/google-gemini/gemini-cli/pull/17927)
- feat(core): implement dynamic policy registration for subagents by abhipatel12
in [#17838](https://github.com/google-gemini/gemini-cli/pull/17838)
- feat: Implement background shell commands by galz10 in
[#14849](https://github.com/google-gemini/gemini-cli/pull/14849)
- feat(admin): provide actionable error messages for disabled features by
skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815)
- Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by
jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940)
- Fix broken link in docs by chrstnb in
[#17959](https://github.com/google-gemini/gemini-cli/pull/17959)
- feat(plan): reuse standard tool confirmation for AskUser tool by jerop in
[#17864](https://github.com/google-gemini/gemini-cli/pull/17864)
- feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by
lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942)
- run npx pointing to the specific commit SHA by sehoon38 in
[#17970](https://github.com/google-gemini/gemini-cli/pull/17970)
- Add allowedExtensions setting by kevinjwang1 in
[#17695](https://github.com/google-gemini/gemini-cli/pull/17695)
- feat(plan): refactor ToolConfirmationPayload to union type by jerop in
[#17980](https://github.com/google-gemini/gemini-cli/pull/17980)
- lower the default max retries to reduce contention by sehoon38 in
[#17975](https://github.com/google-gemini/gemini-cli/pull/17975)
- fix(core): ensure YOLO mode auto-approves complex shell commands when parsing
fails by abhipatel12 in
[#17920](https://github.com/google-gemini/gemini-cli/pull/17920)
- Fix broken link. by g-samroberts in
[#17972](https://github.com/google-gemini/gemini-cli/pull/17972)
- Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over
AppContainer for input handling. by jacob314 in
[#17993](https://github.com/google-gemini/gemini-cli/pull/17993)
- Fix truncation for AskQuestion by jacob314 in
[#18001](https://github.com/google-gemini/gemini-cli/pull/18001)
- fix(workflow): update maintainer check logic to be inclusive and
case-insensitive by bdmorgan in
[#18009](https://github.com/google-gemini/gemini-cli/pull/18009)
- Fix Esc cancel during streaming by LyalinDotCom in
[#18039](https://github.com/google-gemini/gemini-cli/pull/18039)
- feat(acp): add session resume support by bdmorgan in
[#18043](https://github.com/google-gemini/gemini-cli/pull/18043)
- fix(ci): prevent stale PR closer from incorrectly closing new PRs by bdmorgan
in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069)
- chore: delete autoAccept setting unused in production by victorvianna in
[#17862](https://github.com/google-gemini/gemini-cli/pull/17862)
- feat(plan): use placeholder for choice question "Other" option by jerop in
[#18101](https://github.com/google-gemini/gemini-cli/pull/18101)
- docs: update clearContext to hookSpecificOutput by jackwotherspoon in
[#18024](https://github.com/google-gemini/gemini-cli/pull/18024)
- docs-writer skill: Update docs writer skill by jkcinouye in
[#17928](https://github.com/google-gemini/gemini-cli/pull/17928)
- Sehoon/oncall filter by sehoon38 in
[#18105](https://github.com/google-gemini/gemini-cli/pull/18105)
- feat(core): add setting to disable loop detection by SandyTao520 in
[#18008](https://github.com/google-gemini/gemini-cli/pull/18008)
- Docs: Revise docs/index.md by jkcinouye in
[#17879](https://github.com/google-gemini/gemini-cli/pull/17879)
- Fix up/down arrow regression and add test. by jacob314 in
[#18108](https://github.com/google-gemini/gemini-cli/pull/18108)
- fix(ui): prevent content leak in MaxSizedBox bottom overflow by jerop in
[#17991](https://github.com/google-gemini/gemini-cli/pull/17991)
- refactor: migrate checks.ts utility to core and deduplicate by jerop in
[#18139](https://github.com/google-gemini/gemini-cli/pull/18139)
- feat(core): implement tool name aliasing for backward compatibility by
SandyTao520 in
[#17974](https://github.com/google-gemini/gemini-cli/pull/17974)
- docs: fix help-wanted label spelling by pavan-sh in
[#18114](https://github.com/google-gemini/gemini-cli/pull/18114)
- feat(cli): implement automatic theme switching based on terminal background by
Abhijit-2592 in
[#17976](https://github.com/google-gemini/gemini-cli/pull/17976)
- fix(ide): no-op refactoring that moves the connection logic to helper
functions by skeshive in
[#18118](https://github.com/google-gemini/gemini-cli/pull/18118)
- feat: update review-frontend-and-fix slash command to review-and-fix by galz10
in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146)
- fix: improve Ctrl+R reverse search by jackwotherspoon in
[#18075](https://github.com/google-gemini/gemini-cli/pull/18075)
- feat(plan): handle inconsistency in schedulers by Adib234 in
[#17813](https://github.com/google-gemini/gemini-cli/pull/17813)
- feat(plan): add core logic and exit_plan_mode tool definition by jerop in
[#18110](https://github.com/google-gemini/gemini-cli/pull/18110)
- feat(core): rename search_file_content tool to grep_search and add legacy
alias by SandyTao520 in
[#18003](https://github.com/google-gemini/gemini-cli/pull/18003)
- fix(core): prioritize detailed error messages for code assist setup by
gsquared94 in [#17852](https://github.com/google-gemini/gemini-cli/pull/17852)
- fix(cli): resolve environment loading and auth validation issues in ACP mode
by bdmorgan in
[#18025](https://github.com/google-gemini/gemini-cli/pull/18025)
- feat(core): add .agents/skills directory alias for skill discovery by
NTaylorMullen in
[#18151](https://github.com/google-gemini/gemini-cli/pull/18151)
- chore(core): reassign telemetry keys to avoid server conflict by mattKorwel in
[#18161](https://github.com/google-gemini/gemini-cli/pull/18161)
- Add link to rewind doc in commands.md by Adib234 in
[#17961](https://github.com/google-gemini/gemini-cli/pull/17961)
- feat(core): add draft-2020-12 JSON Schema support with lenient fallback by
afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060)
- refactor(core): robust trimPreservingTrailingNewline and regression test by
adamfweidman in
[#18196](https://github.com/google-gemini/gemini-cli/pull/18196)
- Remove MCP servers on extension uninstall by chrstnb in
[#18121](https://github.com/google-gemini/gemini-cli/pull/18121)
- refactor: localize ACP error parsing logic to cli package by bdmorgan in
[#18193](https://github.com/google-gemini/gemini-cli/pull/18193)
- feat(core): Add A2A auth config types by adamfweidman in
[#18205](https://github.com/google-gemini/gemini-cli/pull/18205)
- Set default max attempts to 3 and use the common variable by sehoon38 in
[#18209](https://github.com/google-gemini/gemini-cli/pull/18209)
- feat(plan): add exit_plan_mode ui and prompt by jerop in
[#18162](https://github.com/google-gemini/gemini-cli/pull/18162)
- fix(test): improve test isolation and enable subagent evaluations by
cocosheng-g in
[#18138](https://github.com/google-gemini/gemini-cli/pull/18138)
- feat(plan): use custom deny messages in plan mode policies by Adib234 in
[#18195](https://github.com/google-gemini/gemini-cli/pull/18195)
- Match on extension ID when stopping extensions by chrstnb in
[#18218](https://github.com/google-gemini/gemini-cli/pull/18218)
- fix(core): Respect user's .gitignore preference by xyrolle in
[#15482](https://github.com/google-gemini/gemini-cli/pull/15482)
- docs: document GEMINI_CLI_HOME environment variable by adamfweidman in
[#18219](https://github.com/google-gemini/gemini-cli/pull/18219)
- chore(core): explicitly state plan storage path in prompt by jerop in
[#18222](https://github.com/google-gemini/gemini-cli/pull/18222)
- A2a admin setting by DavidAPierce in
[#17868](https://github.com/google-gemini/gemini-cli/pull/17868)
- feat(a2a): Add pluggable auth provider infrastructure by adamfweidman in
[#17934](https://github.com/google-gemini/gemini-cli/pull/17934)
- Fix handling of empty settings by chrstnb in
[#18131](https://github.com/google-gemini/gemini-cli/pull/18131)
- Reload skills when extensions change by chrstnb in
[#18225](https://github.com/google-gemini/gemini-cli/pull/18225)
- feat: Add markdown rendering to ask_user tool by jackwotherspoon in
[#18211](https://github.com/google-gemini/gemini-cli/pull/18211)
- Add telemetry to rewind by Adib234 in
[#18122](https://github.com/google-gemini/gemini-cli/pull/18122)
- feat(admin): add support for MCP configuration via admin controls (pt1) by
skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223)
- feat(core): require user consent before MCP server OAuth by ehedlund in
[#18132](https://github.com/google-gemini/gemini-cli/pull/18132)
- fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars
by skeshive in
[#18231](https://github.com/google-gemini/gemini-cli/pull/18231)
- feat(ui): move user identity display to header by sehoon38 in
[#18216](https://github.com/google-gemini/gemini-cli/pull/18216)
- fix: enforce folder trust for workspace settings, skills, and context by
galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596)
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
- Remove unused modelHooks and toolHooks by @ved015 in
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
- Update Attempt text to Retry when showing the retry happening to the … by
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
- chore(skills): update pr-creator skill workflow by @sehoon38 in
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
@gemini-cli-robot in
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
- Remove other rewind reference in docs by @chrstnb in
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
- feat(skills): add code-reviewer skill by @sehoon38 in
[#17187](httpshttps://github.com/google-gemini/gemini-cli/pull/17187)
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
- feat(plan): refactor TestRig and eval helper to support configurable approval
modes by @jerop in
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
- feat(workflows): support recursive workstream labeling and new IDs by
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
- Run evals for all models. by @gundermanc in
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123)
- fix(github): improve label-workstream-rollup efficiency with GraphQL by
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217)
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by
@g-samroberts in
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215)
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204)
- fix(github): improve label-workstream-rollup efficiency and fix bugs by
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219)
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192)
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218)
- feat(core): support dynamic variable substitution in system prompt override by
@NTaylorMullen in
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042)
- fix(core,cli): enable recursive directory access for by @galz10 in
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094)
- Docs: Marking for experimental features by @jkcinouye in
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760)
- Support command/ctrl/alt backspace correctly by @scidomino in
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175)
- feat(plan): add approval mode instructions to system prompt by @jerop in
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151)
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223)
- Remove unused slug from sidebar by @chrstnb in
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229)
- drain stdin on exit by @scidomino in
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241)
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by
@abhipatel12 in
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
- fix(core): update token count and telemetry on /chat resume history load by
@psinha40898 in
[#16279](https://github.com/google-gemini/gemini-cli/pull/16279)
- fix: /policy to display policies according to mode by @ishaanxgupta in
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772)
- fix(core): simplify replace tool error message by @SandyTao520 in
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246)
- feat(cli): consolidate shell inactivity and redirection monitoring by
@NTaylorMullen in
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086)
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by
@abhipatel12 in
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
- feat(hooks): enable hooks system by default by @abhipatel12 in
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
- feat(core): Enable AgentRegistry to track all discovered subagents by
@SandyTao520 in
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by
@jackwotherspoon in
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
- Enable the ability to queue specific nightly eval tests by @gundermanc in
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
- docs(hooks): comprehensive update of hook documentation and specs by
@abhipatel12 in
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
- refactor: improve large text paste placeholder by @jacob314 in
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
- feat: implement /rewind command by @Adib234 in
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
- Feature/jetbrains ide detection by @SoLoHiC in
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
- docs: update typo in mcp-server.md file by @schifferl in
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
- Sanitize command names and descriptions by @ehedlund in
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
- fix(auth): don't crash when initial auth fails by @skeshive in
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
- feat: add AskUser tool schema by @jackwotherspoon in
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
now handles the caching internally by @jacob314 in
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
- ci: allow failure in evals-nightly run step by @gundermanc in
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
- feat(cli): Add state management and plumbing for agent configuration dialog by
@SandyTao520 in
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
- bug: fix ide-client connection to ide-companion when inside docker via
ssh/devcontainer by @kapsner in
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
- Emit correct newline type return by @scidomino in
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
- New skill: docs-writer by @g-samroberts in
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
@spencer426 in
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
- Disable tips after 10 runs by @Adib234 in
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101)
- Fix so rewind starts at the bottom and loadHistory refreshes static content.
by @jacob314 in
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335)
- feat(core): Remove legacy settings. by @joshualitt in
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244)
- feat(plan): add 'communicate' tool kind by @jerop in
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341)
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by
@mattKorwel in
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041)
- feat(plan): update UI Theme for Plan Mode by @Adib234 in
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243)
- fix(ui): stabilize rendering during terminal resize in alternate buffer by
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783)
- feat(cli): add /agents config command and improve agent discovery by
@SandyTao520 in
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342)
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299)
- fix(cli)!: Default to interactive mode for positional arguments by
@ishaanxgupta in
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329)
- Fix issue #17080 by @jacob314 in
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100)
- feat(core): Refresh agents after loading an extension. by @joshualitt in
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355)
- fix(cli): include source in policy rule display by @allenhutchison in
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358)
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by
@gsquared94 in
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236)
- Refactor subagent delegation to be one tool per agent by @gundermanc in
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346)
- fix(core): Include MCP server name in OAuth message by @jerop in
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351)
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and
"maintainer only" by @jacob314 in
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324)
- feat(plan): implement simple workflow for planning in main agent by @jerop in
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326)
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967)
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by
@medic-code in
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038)
- fix(core): add alternative command names for Antigravity editor detec… by
@BaeSeokJae in
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829)
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737)
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390)
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714)
- fix(core): gracefully handle disk full errors in chat recording by
@godwiniheuwa in
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305)
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388)
- fix(core): use RFC 9728 compliant path-based OAuth protected resource
discovery by @vrv in
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756)
- Update Code Wiki README badge by @PatoBeltran in
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229)
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921)
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369)
- fix(cli): preserve input text when declining tool approval (#15624) by
@ManojINaik in
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659)
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403)
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344)
- feat(ui): display user tier in about command by @sehoon38 in
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400)
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574)
- fix(cli): change image paste location to global temp directory (#17396) by
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396)
- Fix line endings issue with Notice file by @scidomino in
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417)
- feat(plan): implement persistent approvalMode setting by @Adib234 in
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350)
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404)
- Allow prompt queueing during MCP initialization by @Adib234 in
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395)
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370)
- fix(agents): default to all tools when tool list is omitted in subagents by
@gundermanc in
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422)
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276)
- fix(core): hide user tier name by @sehoon38 in
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418)
- feat: Enforce unified folder trust for /directory add by @galz10 in
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359)
- migrate fireToolNotificationHook to hookSystem by @ved015 in
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398)
- Clean up dead code by @scidomino in
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443)
- feat(workflow): add stale pull request closer with linked-issue enforcement by
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449)
- feat(workflow): expand stale-exempt labels to include help wanted and Public
Roadmap by @bdmorgan in
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459)
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460)
- Resolves the confusing error message `ripgrep exited with code null that
occurs when a search operation is cancelled or aborted by @maximmasiutin in
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267)
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254)
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412)
- docs(hooks): clarify mandatory 'type' field and update hook schema
documentation by @abhipatel12 in
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499)
- Improve error messages on failed onboarding by @gsquared94 in
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357)
- Follow up to "enableInteractiveShell for external tooling relying on a2a
server" by @DavidAPierce in
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130)
- Fix/issue 17070 by @alih552 in
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242)
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476)
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470)
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538)
- Fix bug in detecting already added paths. by @jacob314 in
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430)
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs
by @abhipatel12 in
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429)
- feat(agents): implement first-run experience for project-level sub-agents by
@gundermanc in
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266)
- Update extensions docs by @chrstnb in
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093)
- Docs: Refactor left nav on the website by @jkcinouye in
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
- feat(plan): add persistent plan file storage by @jerop in
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
- Fix extensions config error by @chrstnb in
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
- fix(plan): remove subagent invocation from plan mode by @jerop in
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
- feat(ui): add solid background color option for input prompt by @jacob314 in
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
- feat(cli): add global setting to disable UI spinners by @galz10 in
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
- fix(security): enforce strict policy directory permissions by @yunaseoul in
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
- test(core): fix tests in windows by @scidomino in
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
- Always map mac keys, even on other platforms by @scidomino in
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
- Ctrl-O by @jacob314 in
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
- feat(plan): update cycling order of approval modes by @Adib234 in
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
- fix(cli): restore 'Modify with editor' option in external terminals by
@abhipatel12 in
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
- Slash command for helping in debugging by @gundermanc in
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
- feat: add double-click to expand/collapse large paste placeholders by
@jackwotherspoon in
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
@abhipatel12 in
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
(regression) by @gsquared94 in
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
- feat(core): enforce server prefixes for MCP tools in agent definitions by
@abhipatel12 in
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
- paste transform followup by @jacob314 in
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
- feat(cli): add oncall command for issue triage by @sehoon38 in
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
- Fix sidebar issue for extensions link by @chrstnb in
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
- Change formatting to prevent UI redressing attacks by @scidomino in
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
- Fix cluster of bugs in the settings dialog. by @jacob314 in
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
- Update sidebar to resolve site build issues by @chrstnb in
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
- fix(admin): fix a few bugs related to admin controls by @skeshive in
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
- revert bad changes to tests by @scidomino in
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
- feat(cli): show candidate issue state reason and duplicate status in triage by
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
- Fix missing slash commands when Gemini CLI is in a project with a package.json
that doesn't follow semantic versioning by @Adib234 in
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
- feat(core): Model family-specific system prompts by @joshualitt in
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
- Sub-agents documentation. by @gundermanc in
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
- Load extension settings for hooks, agents, skills by @chrstnb in
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
- Fix issue where Gemini CLI can make changes when simply asked a question by
@gundermanc in
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
- Update docs-writer skill for editing and add style guide for reference. by
@g-samroberts in
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
- fix(ux): have user message display a short path for pasted images by @devr0306
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
- GEMINI.md polish by @jacob314 in
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
- refactor(core): centralize path validation and allow temp dir access for tools
by @NTaylorMullen in
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
mode. by @jacob314 in
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
- feat(skills): promote skills settings to stable by @abhipatel12 in
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
- feat(ui): add terminal cursor support by @jacob314 in
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
- Add support for an additional exclusion file besides .gitignore and
.geminiignore by @alisa-alisa in
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
- feat: add review-frontend-and-fix command by @galz10 in
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.27.0-preview.8...v0.28.0-preview.0
https://github.com/google-gemini/gemini-cli/compare/v0.26.0-preview.5...v0.27.0-preview.0
-15
View File
@@ -99,18 +99,3 @@ See [Extensions Documentation](../extensions/index.md) for more details.
| `gemini mcp list` | List all configured MCP servers | `gemini mcp list` |
See [MCP Server Integration](../tools/mcp-server.md) for more details.
## Skills management
| Command | Description | Example |
| -------------------------------- | ------------------------------------- | ------------------------------------------------- |
| `gemini skills list` | List all discovered agent skills | `gemini skills list` |
| `gemini skills install <source>` | Install skill from Git, path, or file | `gemini skills install https://github.com/u/repo` |
| `gemini skills link <path>` | Link local agent skills via symlink | `gemini skills link /path/to/my-skills` |
| `gemini skills uninstall <name>` | Uninstall an agent skill | `gemini skills uninstall my-skill` |
| `gemini skills enable <name>` | Enable an agent skill | `gemini skills enable my-skill` |
| `gemini skills disable <name>` | Disable an agent skill | `gemini skills disable my-skill` |
| `gemini skills enable --all` | Enable all skills | `gemini skills enable --all` |
| `gemini skills disable --all` | Disable all skills | `gemini skills disable --all` |
See [Agent Skills Documentation](./skills.md) for more details.
+82 -141
View File
@@ -10,14 +10,6 @@ Slash commands provide meta-level control over the CLI itself.
### Built-in Commands
- **`/about`**
- **Description:** Show version info. Please share this information when
filing issues.
- **`/auth`**
- **Description:** Open a dialog that lets you change the authentication
method.
- **`/bug`**
- **Description:** File an issue about Gemini CLI. By default, the issue is
filed within the GitHub repository for Gemini CLI. The string you enter
@@ -30,21 +22,10 @@ Slash commands provide meta-level control over the CLI itself.
conversation state interactively, or resuming a previous state from a later
session.
- **Sub-commands:**
- **`delete <tag>`**
- **Description:** Deletes a saved conversation checkpoint.
- **`list`**
- **Description:** Lists available tags for chat state resumption.
- **Note:** This command only lists chats saved within the current
project. Because chat history is project-scoped, chats saved in other
project directories will not be displayed.
- **`resume <tag>`**
- **Description:** Resumes a conversation from a previous save.
- **Note:** You can only resume chats that were saved within the current
project. To resume a chat from a different project, you must run the
Gemini CLI from that project's directory.
- **`save <tag>`**
- **`save`**
- **Description:** Saves the current conversation history. You must add a
`<tag>` for identifying the conversation state.
- **Usage:** `/chat save <tag>`
- **Details on checkpoint location:** The default locations for saved chat
checkpoints are:
- Linux/macOS: `~/.gemini/tmp/<project_hash>/`
@@ -56,11 +37,25 @@ Slash commands provide meta-level control over the CLI itself.
conversation states. For automatic checkpoints created before file
modifications, see the
[Checkpointing documentation](../cli/checkpointing.md).
- **`share [filename]`**
- **`resume`**
- **Description:** Resumes a conversation from a previous save.
- **Usage:** `/chat resume <tag>`
- **Note:** You can only resume chats that were saved within the current
project. To resume a chat from a different project, you must run the
Gemini CLI from that project's directory.
- **`list`**
- **Description:** Lists available tags for chat state resumption.
- **Note:** This command only lists chats saved within the current
project. Because chat history is project-scoped, chats saved in other
project directories will not be displayed.
- **`delete`**
- **Description:** Deletes a saved conversation checkpoint.
- **Usage:** `/chat delete <tag>`
- **`share`**
- **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`.
or JSON file.
- **Usage** `/chat share file.md` or `/chat share file.json`. If no
filename is provided, then the CLI will generate one.
- **`/clear`**
- **Description:** Clear the terminal screen, including the visible session
@@ -103,9 +98,6 @@ Slash commands provide meta-level control over the CLI itself.
`--include-directories`.
- **Usage:** `/directory show`
- **`/docs`**
- **Description:** Open the Gemini CLI documentation in your browser.
- **`/editor`**
- **Description:** Open a dialog for selecting supported editors.
@@ -113,73 +105,34 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Lists all active extensions in the current Gemini CLI
session. See [Gemini CLI Extensions](../extensions/index.md).
- **`/help`**
- **`/help`** (or **`/?`**)
- **Description:** Display help information about Gemini CLI, including
available commands and their usage.
- **`/shortcuts`**
- **Description:** Toggle the shortcuts panel above the input.
- **Shortcut:** Press `?` when the prompt is empty.
- **`/hooks`**
- **Description:** Manage hooks, which allow you to intercept and customize
Gemini CLI behavior at specific lifecycle events.
- **Sub-commands:**
- **`disable-all`**:
- **Description:** Disable all enabled hooks.
- **`disable <hook-name>`**:
- **Description:** Disable a hook by name.
- **`enable-all`**:
- **Description:** Enable all disabled hooks.
- **`enable <hook-name>`**:
- **Description:** Enable a hook by name.
- **`list`** (or `show`, `panel`):
- **Description:** Display all registered hooks with their status.
- **`/ide`**
- **Description:** Manage IDE integration.
- **Sub-commands:**
- **`disable`**:
- **Description:** Disable IDE integration.
- **`enable`**:
- **Description:** Enable IDE integration.
- **`install`**:
- **Description:** Install required IDE companion.
- **`status`**:
- **Description:** Check status of IDE integration.
- **`/init`**
- **Description:** To help users easily create a `GEMINI.md` file, this
command analyzes the current directory and generates a tailored context
file, making it simpler for them to provide project-specific instructions to
the Gemini agent.
- **`/introspect`**
- **Description:** Provide debugging information about the current Gemini CLI
session, including the state of loaded sub-agents and active hooks. This
command is primarily for advanced users and developers.
- **`/mcp`**
- **Description:** Manage configured Model Context Protocol (MCP) servers.
- **Sub-commands:**
- **`list`** or **`ls`**:
- **Description:** List configured MCP servers and tools. This is the
default action if no subcommand is specified.
- **`desc`**
- **Description:** List configured MCP servers and tools with
descriptions.
- **`schema`**:
- **Description:** List configured MCP servers and tools with descriptions
and schemas.
- **`auth`**:
- **Description:** Authenticate with an OAuth-enabled MCP server.
- **Usage:** `/mcp auth <server-name>`
- **Details:** If `<server-name>` is provided, it initiates the OAuth flow
for that server. If no server name is provided, it lists all configured
servers that support OAuth authentication.
- **`desc`**
- **Description:** List configured MCP servers and tools with
descriptions.
- **`list`** or **`ls`**:
- **Description:** List configured MCP servers and tools. This is the
default action if no subcommand is specified.
- **`refresh`**:
- **Description:** Restarts all MCP servers and re-discovers their
available tools.
- **`schema`**:
- **Description:** List configured MCP servers and tools with descriptions
and schemas.
- [**`/model`**](./model.md)
- **Description:** Opens a dialog to choose your Gemini model.
- **`/memory`**
- **Description:** Manage the AI's instructional context (hierarchical memory
@@ -188,40 +141,23 @@ Slash commands provide meta-level control over the CLI itself.
- **`add`**:
- **Description:** Adds the following text to the AI's memory. Usage:
`/memory add <text to remember>`
- **`list`**:
- **Description:** Lists the paths of the GEMINI.md files in use for
hierarchical memory.
- **`refresh`**:
- **Description:** Reload the hierarchical instructional memory from all
`GEMINI.md` files found in the configured locations (global,
project/ancestors, and sub-directories). This command updates the model
with the latest `GEMINI.md` content.
- **`show`**:
- **Description:** Display the full, concatenated content of the current
hierarchical memory that has been loaded from all `GEMINI.md` files.
This lets you inspect the instructional context being provided to the
Gemini model.
- **`refresh`**:
- **Description:** Reload the hierarchical instructional memory from all
`GEMINI.md` files found in the configured locations (global,
project/ancestors, and sub-directories). This command updates the model
with the latest `GEMINI.md` content.
- **`list`**:
- **Description:** Lists the paths of the GEMINI.md files in use for
hierarchical memory.
- **Note:** For more details on how `GEMINI.md` files contribute to
hierarchical memory, see the
[CLI Configuration documentation](../get-started/configuration.md).
- [**`/model`**](./model.md)
- **Description:** Opens a dialog to choose your Gemini model.
- **`/policies`**
- **Description:** Manage policies.
- **Sub-commands:**
- **`list`**:
- **Description:** List all active policies grouped by mode.
- **`/privacy`**
- **Description:** Display the Privacy Notice and allow users to select
whether they consent to the collection of their data for service improvement
purposes.
- **`/quit`** (or **`/exit`**)
- **Description:** Exit Gemini CLI.
- **`/restore`**
- **Description:** Restores the project files to the state they were in just
before a tool was executed. This is particularly useful for undoing file
@@ -232,24 +168,18 @@ Slash commands provide meta-level control over the CLI itself.
[settings](../get-started/configuration.md). See
[Checkpointing documentation](../cli/checkpointing.md) for more details.
- [**`/rewind`**](./rewind.md)
- **Description:** Navigates backward through the conversation history,
allowing you to review past interactions and potentially revert to a
previous state. This feature helps in managing complex or branched
conversations.
- **`/resume`**
- **Description:** Browse and resume previous conversation sessions. Opens an
interactive session browser where you can search, filter, and select from
automatically saved conversations.
- **Features:**
- **Management:** Delete unwanted sessions directly from the browser
- **Resume:** Select any session to resume and continue the conversation
- **Search:** Use `/` to search through conversation content across all
sessions
- **Session Browser:** Interactive interface showing all saved sessions with
timestamps, message counts, and first user message for context
- **Search:** Use `/` to search through conversation content across all
sessions
- **Sorting:** Sort sessions by date or message count
- **Management:** Delete unwanted sessions directly from the browser
- **Resume:** Select any session to resume and continue the conversation
- **Note:** All conversations are automatically saved as you chat - no manual
saving required. See [Session Management](../cli/session-management.md) for
complete details.
@@ -268,26 +198,19 @@ Slash commands provide meta-level control over the CLI itself.
modify them as desired. Changes to some settings are applied immediately,
while others require a restart.
- **`/shells`** (or **`/bashes`**)
- **Description:** Toggle the background shells view. This allows you to view
and manage long-running processes that you've sent to the background.
- **`/setup-github`**
- **Description:** Set up GitHub Actions to triage issues and review PRs with
Gemini.
- [**`/skills`**](./skills.md)
- **Description:** Manage Agent Skills, which provide on-demand expertise and
specialized workflows.
- **Sub-commands:**
- **`disable <name>`**:
- **Description:** Disable a specific skill by name.
- **Usage:** `/skills disable <name>`
- **`enable <name>`**:
- **Description:** Enable a specific skill by name.
- **Usage:** `/skills enable <name>`
- **`list`**:
- **Description:** List all discovered skills and their current status
(enabled/disabled).
- **`enable`**:
- **Description:** Enable a specific skill by name.
- **Usage:** `/skills enable <name>`
- **`disable`**:
- **Description:** Disable a specific skill by name.
- **Usage:** `/skills disable <name>`
- **`reload`**:
- **Description:** Refresh the list of discovered skills from all tiers
(workspace, user, and extensions).
@@ -299,14 +222,18 @@ Slash commands provide meta-level control over the CLI itself.
cached tokens are being used, which occurs with API key authentication but
not with OAuth authentication at this time.
- **`/terminal-setup`**
- **Description:** Configure terminal keybindings for multiline input (VS
Code, Cursor, Windsurf).
- [**`/theme`**](./themes.md)
- **Description:** Open a dialog that lets you change the visual theme of
Gemini CLI.
- **`/auth`**
- **Description:** Open a dialog that lets you change the authentication
method.
- **`/about`**
- **Description:** Show version info. Please share this information when
filing issues.
- [**`/tools`**](../tools/index.md)
- **Description:** Display a list of tools that are currently available within
Gemini CLI.
@@ -318,23 +245,37 @@ Slash commands provide meta-level control over the CLI itself.
- **`nodesc`** or **`nodescriptions`**:
- **Description:** Hide tool descriptions, showing only the tool names.
- **`/privacy`**
- **Description:** Display the Privacy Notice and allow users to select
whether they consent to the collection of their data for service improvement
purposes.
- **`/quit`** (or **`/exit`**)
- **Description:** Exit Gemini CLI.
- **`/vim`**
- **Description:** Toggle vim mode on or off. When vim mode is enabled, the
input area supports vim-style navigation and editing commands in both NORMAL
and INSERT modes.
- **Features:**
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
- **NORMAL mode:** Navigate with `h`, `j`, `k`, `l`; jump by words with `w`,
`b`, `e`; go to line start/end with `0`, `$`, `^`; go to specific lines
with `G` (or `gg` for first line)
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
- **Repeat last command:** Use `.` to repeat the last editing operation
- **Persistent setting:** Vim mode preference is saved to
`~/.gemini/settings.json` and restored between sessions
- **Repeat last command:** Use `.` to repeat the last editing operation
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
footer
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
footer
- **`/init`**
- **Description:** To help users easily create a `GEMINI.md` file, this
command analyzes the current directory and generates a tailored context
file, making it simpler for them to provide project-specific instructions to
the Gemini agent.
### Custom commands
@@ -347,11 +288,11 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
These shortcuts apply directly to the input prompt for text manipulation.
- **Undo:**
- **Keyboard shortcut:** Press **Alt+z** or **Cmd+z** to undo the last action
- **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
in the input prompt.
- **Redo:**
- **Keyboard shortcut:** Press **Shift+Alt+Z** or **Shift+Cmd+Z** to redo the
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
last undone action in the input prompt.
## At commands (`@`)
-80
View File
@@ -1,80 +0,0 @@
# Creating Agent Skills
This guide provides an overview of how to create your own Agent Skills to extend
the capabilities of Gemini CLI.
## Getting started: The `skill-creator` skill
The recommended way to create a new skill is to use the built-in `skill-creator`
skill. To use it, ask Gemini CLI to create a new skill for you.
**Example prompt:**
> "create a new skill called 'code-reviewer'"
Gemini CLI will then use the `skill-creator` to generate the skill:
1. Generate a new directory for your skill (e.g., `my-new-skill/`).
2. Create a `SKILL.md` file with the necessary YAML frontmatter (`name` and
`description`).
3. Create the standard resource directories: `scripts/`, `references/`, and
`assets/`.
## Manual skill creation
If you prefer to create skills manually:
1. **Create a directory** for your skill (e.g., `my-new-skill/`).
2. **Create a `SKILL.md` file** inside the new directory.
To add additional resources that support the skill, refer to the skill
structure.
## Skill structure
A skill is a directory containing a `SKILL.md` file at its root.
### Folder structure
While a `SKILL.md` file is the only required component, we recommend the
following structure for organizing your skill's resources:
```text
my-skill/
├── SKILL.md (Required) Instructions and metadata
├── scripts/ (Optional) Executable scripts
├── references/ (Optional) Static documentation
└── assets/ (Optional) Templates and other resources
```
### `SKILL.md` file
The `SKILL.md` file is the core of your skill. This file uses YAML frontmatter
for metadata and Markdown for instructions. For example:
```markdown
---
name: code-reviewer
description:
Use this skill to review code. It supports both local changes and remote Pull
Requests.
---
# Code Reviewer
This skill guides the agent in conducting thorough code reviews.
## Workflow
### 1. Determine Review Target
- **Remote PR**: If the user gives a PR number or URL, target that remote PR.
- **Local Changes**: If changes are local... ...
```
- **`name`**: A unique identifier for the skill. This should match the directory
name.
- **`description`**: A description of what the skill does and when Gemini should
use it.
- **Body**: The Markdown body of the file contains the instructions that guide
the agent's behavior when the skill is active.
-17
View File
@@ -203,23 +203,6 @@ with the actual Gemini CLI process, which inherits the environment variable.
This makes it significantly more difficult for a user to bypass the enforced
settings.
## User isolation in shared environments
In shared compute environments (like ML experiment runners or shared build
servers), you can isolate Gemini CLI state by overriding the user's home
directory.
By default, Gemini CLI stores configuration and history in `~/.gemini`. You can
use the `GEMINI_CLI_HOME` environment variable to point to a unique directory
for a specific user or job. The CLI will create a `.gemini` folder inside the
specified path.
```bash
# Isolate state for a specific job
export GEMINI_CLI_HOME="/tmp/gemini-job-123"
gemini
```
## Restricting tool access
You can significantly enhance security by controlling which tools the Gemini
-2
View File
@@ -23,8 +23,6 @@ overview of Gemini CLI, see the [main documentation page](../index.md).
## Advanced features
- **[Plan mode (experimental)](./plan-mode.md):** Use a safe, read-only mode for
planning complex changes.
- **[Checkpointing](./checkpointing.md):** Automatically save and restore
snapshots of your session and files.
- **[Enterprise configuration](./enterprise.md):** Deploy and manage Gemini CLI
+6 -20
View File
@@ -23,7 +23,7 @@ available combinations.
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Shift, Ctrl)` |
| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + B` |
| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + F` |
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
@@ -106,18 +106,8 @@ available combinations.
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`<br />`Ctrl + S` |
| 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 (no Shift)` |
| Show warning when trying to unfocus background shell via Tab. | `Tab (no Shift)` |
| Show warning when trying to unfocus shell input via Tab. | `Tab (no Shift)` |
| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
| Move focus from the shell back to Gemini. | `Shift + Tab` |
| Focus the shell input from the gemini input. | `Tab (no Shift)` |
| Focus the Gemini input from the shell input. | `Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Suspend the application (not yet implemented). | `Ctrl + Z` |
@@ -129,9 +119,6 @@ available combinations.
- `Option+B/F/M` (macOS only): Are interpreted as `Cmd+B/F/M` even if your
terminal isn't configured to send Meta with Option.
- `!` on an empty prompt: Enter or exit shell mode.
- `?` on an empty prompt: Toggle the shortcuts panel above the input. Press
`Esc`, `Backspace`, or any printable key to close it. Press `?` again to close
the panel and insert a `?` into the prompt.
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
@@ -140,7 +127,6 @@ available combinations.
single-line input, navigate backward or forward through prompt history.
- `Number keys (1-9, multi-digit)` inside selection dialogs: Jump directly to
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.
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
view full content inline. Double-click again to collapse.
- `Double-click` on a paste placeholder (`[Pasted Text: X lines]`) in alternate
buffer mode: Expand to view full content inline. Double-click again to
collapse.
-106
View File
@@ -1,106 +0,0 @@
# Plan Mode (experimental)
Plan Mode is a safe, read-only mode for researching and designing complex
changes. It prevents modifications while you research, design and plan an
implementation strategy.
> **Note: Plan Mode is currently an experimental feature.**
>
> Experimental features are subject to change. To use Plan Mode, enable it via
> `/settings` (search for `Plan`) or add the following to your `settings.json`:
>
> ```json
> {
> "experimental": {
> "plan": true
> }
> }
> ```
>
> Your feedback is invaluable as we refine this feature. If you have ideas,
> suggestions, or encounter issues:
>
> - Use the `/bug` command within the CLI to file an issue.
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues) on
> GitHub.
- [Starting in Plan Mode](#starting-in-plan-mode)
- [How to use Plan Mode](#how-to-use-plan-mode)
- [Entering Plan Mode](#entering-plan-mode)
- [The Planning Workflow](#the-planning-workflow)
- [Exiting Plan Mode](#exiting-plan-mode)
- [Tool Restrictions](#tool-restrictions)
## Starting in Plan Mode
You can configure Gemini CLI to start directly in Plan Mode by default:
1. Type `/settings` in the CLI.
2. Search for `Default Approval Mode`.
3. Set the value to `Plan`.
Other ways to start in Plan Mode:
- **CLI Flag:** `gemini --approval-mode=plan`
- **Manual Settings:** Manually update your `settings.json`:
```json
{
"general": {
"defaultApprovalMode": "plan"
}
}
```
## How to use Plan Mode
### Entering Plan Mode
You can enter Plan Mode in three ways:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Plan` -> `Auto-Edit`).
2. **Command:** Type `/plan` in the input box.
3. **Natural Language:** Ask the agent to "start a plan for...".
### The Planning Workflow
1. **Requirements:** The agent clarifies goals using `ask_user`.
2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map
the codebase and validate assumptions.
3. **Planning:** A detailed plan is written to a temporary Markdown file.
4. **Review:** You review the plan.
- **Approve:** Exit Plan Mode and start implementation (switching to
Auto-Edit or Default approval mode).
- **Iterate:** Provide feedback to refine the plan.
### Exiting Plan Mode
To exit Plan Mode:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
1. **Tool:** The agent calls the `exit_plan_mode` tool to present the finalized
plan for your approval.
## Tool Restrictions
Plan Mode enforces strict safety policies to prevent accidental changes.
These are the only allowed tools:
- **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`]
- **Search:** [`grep_search`], [`google_web_search`]
- **Interaction:** `ask_user`
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/plans/` directory.
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
[`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext
[`write_file`]: /docs/tools/file-system.md#3-write_file-writefile
[`glob`]: /docs/tools/file-system.md#4-glob-findfiles
[`google_web_search`]: /docs/tools/web-search.md
[`replace`]: /docs/tools/file-system.md#6-replace-edit
[MCP tools]: /docs/tools/mcp-server.md
+51 -52
View File
@@ -22,14 +22,14 @@ 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 Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------- | ---------------------------------- | ------------------------------------------------------------- | ------- |
| Preview Features (e.g., models) | `general.previewFeatures` | Enable preview features (e.g., preview models). | `false` |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
### Output
@@ -39,34 +39,31 @@ they appear in the UI.
### UI
| UI Label | Setting | Description | Default |
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
### IDE
@@ -80,7 +77,6 @@ they appear in the UI.
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
### Context
@@ -97,24 +93,27 @@ they appear in the UI.
### Tools
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation. | `40000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Auto Accept | `tools.autoAccept` | Automatically accept and execute tool calls that are considered safe (e.g., read-only operations). | `false` |
| Approval Mode | `tools.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` |
| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
### Security
| UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| 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` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
### Experimental
+79 -21
View File
@@ -52,7 +52,6 @@ locations override lower ones: **Workspace > User > Extension**.
Use the `/skills` slash command to view and manage available expertise:
- `/skills list` (default): Shows all discovered skills and their status.
- `/skills link <path>`: Links agent skills from a local directory via symlink.
- `/skills disable <name>`: Prevents a specific skill from being used.
- `/skills enable <name>`: Re-enables a disabled skill.
- `/skills reload`: Refreshes the list of discovered skills from all tiers.
@@ -68,13 +67,6 @@ The `gemini skills` command provides management utilities:
# List all discovered skills
gemini skills list
# Link agent skills from a local directory via symlink
# Discovers skills (SKILL.md or */SKILL.md) and creates symlinks in ~/.gemini/skills (user)
gemini skills link /path/to/my-skills-repo
# Link to the workspace scope (.gemini/skills)
gemini skills link /path/to/my-skills-repo --scope workspace
# Install a skill from a Git repository, local directory, or zipped skill file (.skill)
# Uses the user scope by default (~/.gemini/skills)
gemini skills install https://github.com/user/repo.git
@@ -97,7 +89,85 @@ gemini skills enable my-expertise
gemini skills disable my-expertise --scope workspace
```
## How it Works
## Creating a Skill
A skill is a directory containing a `SKILL.md` file at its root. This file
serves as the entry point for your skill and uses YAML frontmatter for metadata
and Markdown for instructions.
### Folder Structure
Skills are self-contained directories. At a minimum, a skill requires a
`SKILL.md` file, but can include other resources:
```text
my-skill/
├── SKILL.md (Required) Instructions and metadata
├── scripts/ (Optional) Executable scripts/tools
├── references/ (Optional) Static documentation and examples
└── assets/ (Optional) Templates and binary resources
```
### Basic Structure (SKILL.md)
```markdown
---
name: <unique-name>
description: <what the skill does and when Gemini should use it>
---
<your instructions for how the agent should behave / use the skill>
```
- **`name`**: A unique identifier (lowercase, alphanumeric, and dashes).
- **`description`**: The most critical field. Gemini uses this to decide when
the skill is relevant. Be specific about the expertise provided.
- **Body**: Everything below the second `---` is injected as expert procedural
guidance for the model.
### Example: Team Code Reviewer
Create `~/.gemini/skills/code-reviewer/SKILL.md`:
```markdown
---
name: code-reviewer
description:
Expertise in reviewing code for style, security, and performance. Use when the
user asks for "feedback," a "review," or to "check" their changes.
---
# Code Reviewer
You are an expert code reviewer. When reviewing code, follow this workflow:
1. **Analyze**: Review the staged changes or specific files provided. Ensure
that the changes are scoped properly and represent minimal changes required
to address the issue.
2. **Style**: Ensure code follows the workspace's conventions and idiomatic
patterns as described in the `GEMINI.md` file.
3. **Security**: Flag any potential security vulnerabilities.
4. **Tests**: Verify that new logic has corresponding test coverage and that
the test coverage adequately validates the changes.
Provide your feedback as a concise bulleted list of "Strengths" and
"Opportunities."
```
### Resource Conventions
While you can structure your skill directory however you like, the Agent Skills
standard encourages these conventions:
- **`scripts/`**: Executable scripts (bash, python, node) the agent can run.
- **`references/`**: Static documentation, schemas, or example data for the
agent to consult.
- **`assets/`**: Code templates, boilerplate, or binary resources.
When a skill is activated, Gemini CLI provides the model with a tree view of the
entire skill directory, allowing it to discover and utilize these assets.
## How it Works (Security & Privacy)
1. **Discovery**: At the start of a session, Gemini CLI scans the discovery
tiers and injects the name and description of all enabled skills into the
@@ -113,15 +183,3 @@ gemini skills disable my-expertise --scope workspace
it permission to read any bundled assets.
5. **Execution**: The model proceeds with the specialized expertise active. It
is instructed to prioritize the skill's procedural guidance within reason.
### Skill activation
Once a skill is activated (typically by Gemini identifying a task that matches
the skill's description and your approval), its specialized instructions and
resources are loaded into the agent's context. A skill remains active and its
guidance is prioritized for the duration of the session.
## Creating your own skills
To create your own skills, see the [Create Agent Skills](./creating-skills.md)
guide.
-22
View File
@@ -320,8 +320,6 @@ Captures startup configuration and user prompt submissions.
Tracks changes and duration of approval modes.
##### Lifecycle
- `approval_mode_switch`: Approval mode was changed.
- **Attributes**:
- `from_mode` (string)
@@ -332,15 +330,6 @@ Tracks changes and duration of approval modes.
- `mode` (string)
- `duration_ms` (int)
##### Execution
These events track the execution of an approval mode, such as Plan Mode.
- `plan_execution`: A plan was executed and the session switched from plan mode
to active execution.
- **Attributes**:
- `approval_mode` (string)
#### Tools
Captures tool executions, output truncation, and Edit behavior.
@@ -721,17 +710,6 @@ Agent lifecycle metrics: runs, durations, and turns.
- **Attributes**:
- `agent_name` (string)
##### Approval Mode
###### Execution
These metrics track the adoption and usage of specific approval workflows, such
as Plan Mode.
- `gemini_cli.plan.execution.count` (Counter, Int): Counts plan executions.
- **Attributes**:
- `approval_mode` (string)
##### UI
UI stability signals such as flicker count.
+3 -3
View File
@@ -295,9 +295,9 @@ The Gemini CLI ships with a set of default policies to provide a safe
out-of-the-box experience.
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
- **Agent delegation** defaults to **`ask_user`** to ensure remote agents can
prompt for confirmation, but local sub-agent actions are executed silently and
checked individually.
- **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to
ensure remote agents can prompt for confirmation, but local sub-agent actions
are executed silently and checked individually.
- **Write tools** (like `write_file`, `run_shell_command`) default to
**`ask_user`**.
- In **`yolo`** mode, a high-priority rule allows all tools.
+31 -34
View File
@@ -24,29 +24,30 @@ the main agent's context or toolset.
## What are sub-agents?
Sub-agents are "specialists" that the main Gemini agent can hire for a specific
job.
Think of sub-agents as "specialists" that the main Gemini agent can hire for a
specific job.
- **Focused context:** Each sub-agent has its own system prompt and persona.
- **Specialized tools:** Sub-agents can have a restricted or specialized set of
tools.
- **Independent context window:** Interactions with a sub-agent happen in a
separate context loop, which saves tokens in your main conversation history.
separate context loop. The main agent only sees the final result, saving
tokens in your main conversation history.
Sub-agents are exposed to the main agent as a tool of the same name. When the
main agent calls the tool, it delegates the task to the sub-agent. Once the
sub-agent completes its task, it reports back to the main agent with its
findings.
Sub-agents are exposed to the main agent as a tool of the same name which
delegates to the sub-agent, when called. Once the sub-agent completes its task
(or fails), it reports back to the main agent with its findings (usually as a
text summary or structured report returned by the tool).
## Built-in sub-agents
Gemini CLI comes with the following built-in sub-agents:
Gemini CLI comes with powerful built-in sub-agents.
### Codebase Investigator
- **Name:** `codebase_investigator`
- **Purpose:** Analyze the codebase, reverse engineer, and understand complex
dependencies.
- **Purpose:** Deep analysis of the codebase, reverse engineering, and
understanding complex dependencies.
- **When to use:** "How does the authentication system work?", "Map out the
dependencies of the `AgentRegistry` class."
- **Configuration:** Enabled by default. You can configure it in
@@ -66,25 +67,20 @@ Gemini CLI comes with the following built-in sub-agents:
### CLI Help Agent
- **Name:** `cli_help`
- **Purpose:** Get expert knowledge about Gemini CLI itself, its commands,
- **Purpose:** Expert knowledge about Gemini CLI itself, its commands,
configuration, and documentation.
- **When to use:** "How do I configure a proxy?", "What does the `/rewind`
command do?"
- **Configuration:** Enabled by default.
### Generalist Agent
- **Name:** `generalist_agent`
- **Purpose:** Route tasks to the appropriate specialized sub-agent.
- **When to use:** Implicitly used by the main agent for routing. Not directly
invoked by the user.
- **Configuration:** Enabled by default. No specific configuration options.
## Creating custom sub-agents
You can create your own sub-agents to automate specific workflows or enforce
specific personas. To use custom sub-agents, you must enable them in your
`settings.json`:
specific personas.
### Prerequisites
To use custom sub-agents, you must enable them in your `settings.json`:
```json
{
@@ -116,7 +112,7 @@ description: Specialized in finding security vulnerabilities in code.
kind: local
tools:
- read_file
- grep_search
- search_file_content
model: gemini-2.5-pro
temperature: 0.2
max_turns: 10
@@ -127,10 +123,10 @@ vulnerabilities.
Focus on:
1. SQL Injection
2. XSS (Cross-Site Scripting)
3. Hardcoded credentials
4. Unsafe file operations
1. SQL Injection
2. XSS (Cross-Site Scripting)
3. Hardcoded credentials
4. Unsafe file operations
When you find a vulnerability, explain it clearly and suggest a fix. Do not fix
it yourself; just report it.
@@ -146,21 +142,22 @@ it yourself; just report it.
| `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. |
| `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). |
| `temperature` | number | No | Model temperature (0.0 - 2.0). |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. |
| `timeout_mins` | number | No | Maximum execution time in minutes. |
### Optimizing your sub-agent
The main agent's system prompt encourages it to use an expert sub-agent when one
is available. It decides whether an agent is a relevant expert based on the
agent's description. You can improve the reliability with which an agent is used
by updating the description to more clearly indicate:
The main agent system prompt contains language that encourages use of an expert
sub-agent when one is available for the task at hand. It decides whether an
agent is a relevant expert based on the agent's description. You can improve the
reliability with which an agent is used by updating the description to more
clearly indicate:
- Its area of expertise.
- When it should be used.
- Some example scenarios.
For example, the following sub-agent description should be called fairly
For example: the following sub-agent description should be called fairly
consistently for Git operations.
> Git expert agent which should be used for all local and remote git operations.
@@ -176,7 +173,7 @@ that your sub-agent was called with a specific prompt and the given description.
## Remote subagents (Agent2Agent) (experimental)
Gemini CLI can also delegate tasks to remote sub-agents using the Agent-to-Agent
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
(A2A) protocol.
> **Note: Remote subagents are currently an experimental feature.**
+8
View File
@@ -215,6 +215,14 @@ a few things you can try in order of recommendation:
MCP servers of their own. This should not be used as an airtight security
mechanism.
- **`autoAccept`** (boolean):
- **Description:** Controls whether the CLI automatically accepts and executes
tool calls that are considered safe (e.g., read-only operations) without
explicit user confirmation. If set to `true`, the CLI will bypass the
confirmation prompt for tools deemed safe.
- **Default:** `false`
- **Example:** `"autoAccept": true`
- **`theme`** (string):
- **Description:** Sets the visual [theme](../cli/themes.md) for Gemini CLI.
- **Default:** `"Default"`
+40 -61
View File
@@ -98,6 +98,10 @@ their corresponding top-level category object in your `settings.json` file.
#### `general`
- **`general.previewFeatures`** (boolean):
- **Description:** Enable preview features (e.g., preview models).
- **Default:** `false`
- **`general.preferredEditor`** (string):
- **Description:** The preferred editor to open files in.
- **Default:** `undefined`
@@ -106,17 +110,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Enable Vim keybindings
- **Default:** `false`
- **`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.
- **Default:** `"default"`
- **Values:** `"default"`, `"auto_edit"`, `"plan"`
- **`general.devtools`** (boolean):
- **Description:** Enable DevTools inspector on launch.
- **Default:** `false`
- **`general.enableAutoUpdate`** (boolean):
- **Description:** Enable automatic updates.
- **Default:** `true`
@@ -177,15 +170,6 @@ their corresponding top-level category object in your `settings.json` file.
available options.
- **Default:** `undefined`
- **`ui.autoThemeSwitching`** (boolean):
- **Description:** Automatically switch between default light and dark themes
based on terminal background color.
- **Default:** `true`
- **`ui.terminalBackgroundPollingInterval`** (number):
- **Description:** Interval in seconds to poll the terminal background color.
- **Default:** `60`
- **`ui.customThemes`** (object):
- **Description:** Custom theme definitions.
- **Default:** `{}`
@@ -195,11 +179,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`ui.inlineThinkingMode`** (enum):
- **Description:** Display model thinking inline: off or full.
- **Default:** `"off"`
- **Values:** `"off"`, `"full"`
- **`ui.showStatusInTitle`** (boolean):
- **Description:** Show Gemini CLI model thoughts in the terminal window title
during the working phase
@@ -347,12 +326,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `0.5`
- **Requires restart:** Yes
- **`model.disableLoopDetection`** (boolean):
- **Description:** Disable automatic detection and prevention of infinite
loops.
- **Default:** `false`
- **Requires restart:** Yes
- **`model.skipNextSpeakerCheck`** (boolean):
- **Description:** Skip the next speaker check.
- **Default:** `true`
@@ -688,6 +661,18 @@ their corresponding top-level category object in your `settings.json` file.
performance.
- **Default:** `true`
- **`tools.autoAccept`** (boolean):
- **Description:** Automatically accept and execute tool calls that are
considered safe (e.g., read-only operations).
- **Default:** `false`
- **`tools.approvalMode`** (enum):
- **Description:** The default approval mode for tool execution. 'default'
prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is
read-only mode. 'yolo' is not supported yet.
- **Default:** `"default"`
- **Values:** `"default"`, `"auto_edit"`, `"plan"`
- **`tools.core`** (array):
- **Description:** Restrict the set of built-in tools with an allowlist. Match
semantics mirror tools.allowed; see the built-in tools documentation for
@@ -725,10 +710,20 @@ their corresponding top-level category object in your `settings.json` file.
implementation. Provides faster search performance.
- **Default:** `true`
- **`tools.enableToolOutputTruncation`** (boolean):
- **Description:** Enable truncation of large tool outputs.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.truncateToolOutputThreshold`** (number):
- **Description:** Maximum characters to show when truncating large tool
outputs. Set to 0 or negative to disable truncation.
- **Default:** `40000`
- **Description:** Truncate tool output if it is larger than this many
characters. Set to -1 to disable.
- **Default:** `4000000`
- **Requires restart:** Yes
- **`tools.truncateToolOutputLines`** (number):
- **Description:** The number of lines to keep when truncating tool output.
- **Default:** `1000`
- **Requires restart:** Yes
- **`tools.disableLLMCorrection`** (boolean):
@@ -738,6 +733,12 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.enableHooks`** (boolean):
- **Description:** Enables the hooks system experiment. When disabled, the
hooks system is completely deactivated regardless of other settings.
- **Default:** `true`
- **Requires restart:** Yes
#### `mcp`
- **`mcp.serverCommand`** (string):
@@ -778,16 +779,9 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.allowedExtensions`** (array):
- **Description:** List of Regex patterns for allowed extensions. If nonempty,
only extensions that match the patterns in this list are allowed. Overrides
the blockGitExtensions setting.
- **Default:** `[]`
- **Requires restart:** Yes
- **`security.folderTrust.enabled`** (boolean):
- **Description:** Setting to track whether Folder trust is enabled.
- **Default:** `true`
- **Default:** `false`
- **Requires restart:** Yes
- **`security.environmentVariableRedaction.allowed`** (array):
@@ -861,12 +855,12 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.extensionConfig`** (boolean):
- **Description:** Enable requesting and fetching of extension settings.
- **Default:** `true`
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.extensionRegistry`** (boolean):
- **Description:** Enable extension registry explore UI.
- **Default:** `false`
- **`experimental.enableEventDrivenScheduler`** (boolean):
- **Description:** Enables event-driven scheduler within the CLI session.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.extensionReloading`** (boolean):
@@ -990,10 +984,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** If false, disallows MCP servers from being used.
- **Default:** `true`
- **`admin.mcp.config`** (object):
- **Description:** Admin-configured MCP servers.
- **Default:** `{}`
- **`admin.skills.enabled`** (boolean):
- **Description:** If false, disallows agent skills from being used.
- **Default:** `true`
@@ -1175,13 +1165,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Specifies the default Gemini model to use.
- Overrides the hardcoded default
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"`
- **`GEMINI_CLI_HOME`**:
- Specifies the root directory for Gemini CLI's user-level configuration and
storage.
- By default, this is the user's system home directory. The CLI will create a
`.gemini` folder inside this directory.
- Useful for shared compute environments or keeping CLI state isolated.
- Example: `export GEMINI_CLI_HOME="/path/to/user/config"`
- **`GOOGLE_API_KEY`**:
- Your Google Cloud API key.
- Required for using Vertex AI in express mode.
@@ -1202,10 +1185,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **Description:** The path to your Google Application Credentials JSON file.
- **Example:**
`export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
- **`GOOGLE_GENAI_API_VERSION`**:
- Specifies the API version to use for Gemini API requests.
- When set, overrides the default API version used by the SDK.
- Example: `export GOOGLE_GENAI_API_VERSION="v1"`
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
- Your Google Cloud Project ID for Telemetry in Google Cloud
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
+2 -2
View File
@@ -167,8 +167,8 @@ case is response validation and automatic retries.
- `reason`: Required if denied. This text is sent **to the agent as a new
prompt** to request a correction.
- `continue`: Set to `false` to **stop the session** without retrying.
- `hookSpecificOutput.clearContext`: If `true`, clears conversation history
(LLM memory) while preserving UI display.
- `clearContext`: If `true`, clears conversation history (LLM memory) while
preserving UI display.
- **Exit Code 2 (Retry)**: Rejects the response and triggers an automatic retry
turn using `stderr` as the feedback prompt.
+139 -111
View File
@@ -1,121 +1,149 @@
# Gemini CLI documentation
# Welcome to Gemini CLI documentation
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
into your terminal. It is designed to be a terminal-first, extensible, and
powerful tool for developers, engineers, SREs, and beyond.
This documentation provides a comprehensive guide to installing, using, and
developing Gemini CLI, a tool that lets you interact with Gemini models through
a command-line interface.
Gemini CLI integrates with your local environment. It can read and edit files,
execute shell commands, and search the web, all while maintaining your project
context.
## Gemini CLI overview
## Get started
Gemini CLI brings the capabilities of Gemini models to your terminal in an
interactive Read-Eval-Print Loop (REPL) environment. Gemini CLI consists of a
client-side application (`packages/cli`) that communicates with a local server
(`packages/core`), which in turn manages requests to the Gemini API and its AI
models. Gemini CLI also contains a variety of tools for tasks such as performing
file system operations, running shells, and web fetching, which are managed by
`packages/core`.
Begin your journey with Gemini CLI by setting up your environment and learning
the basics.
## Navigating the documentation
- **[Quickstart](./get-started/index.md):** A streamlined guide to get you
chatting in minutes.
- **[Installation](./get-started/installation.md):** Instructions for macOS,
Linux, and Windows.
- **[Authentication](./get-started/authentication.md):** Set up access using
Google OAuth, API keys, or Vertex AI.
- **[Examples](./get-started/examples.md):** View common usage scenarios to
inspire your own workflows.
This documentation is organized into the following sections:
## Use Gemini CLI
### Overview
Master the core capabilities that let Gemini CLI interact with your system
safely and effectively.
- **[Architecture overview](./architecture.md):** Understand the high-level
design of Gemini CLI, including its components and how they interact.
- **[Contribution guide](../CONTRIBUTING.md):** Information for contributors and
developers, including setup, building, testing, and coding conventions.
- **[Using the CLI](./cli/index.md):** Learn the basics of the command-line
interface.
- **[File management](./tools/file-system.md):** Grant the model the ability to
read code and apply changes directly to your files.
- **[Shell commands](./tools/shell.md):** Allow the model to run builds, tests,
and git commands.
- **[Memory management](./tools/memory.md):** Teach Gemini CLI facts about your
project and preferences that persist across sessions.
- **[Project context](./cli/gemini-md.md):** Use `GEMINI.md` files to provide
persistent context for your projects.
- **[Web search and fetch](./tools/web-search.md):** Enable the model to fetch
real-time information from the internet.
- **[Session management](./cli/session-management.md):** Save, resume, and
organize your chat sessions.
### Get started
## Configuration
Customize Gemini CLI to match your workflow and preferences.
- **[Settings](./cli/settings.md):** Control response creativity, output
verbosity, and more.
- **[Model selection](./cli/model.md):** Choose the best Gemini model for your
specific task.
- **[Ignore files](./cli/gemini-ignore.md):** Use `.geminiignore` to keep
sensitive files out of the model's context.
- **[Trusted folders](./cli/trusted-folders.md):** Define security boundaries
for file access and execution.
- **[Token caching](./cli/token-caching.md):** Optimize performance and cost by
caching context.
- **[Themes](./cli/themes.md):** Personalize the visual appearance of the CLI.
## Advanced features
Explore powerful features for complex workflows and enterprise environments.
- **[Headless mode](./cli/headless.md):** Run Gemini CLI in scripts or CI/CD
pipelines for automated reasoning.
- **[Sandboxing](./cli/sandbox.md):** Execute untrusted code or tools in a
secure, isolated container.
- **[Checkpointing](./cli/checkpointing.md):** Save and restore workspace state
to recover from experimental changes.
- **[Custom commands](./cli/custom-commands.md):** Create shortcuts for
frequently used prompts.
- **[System prompt override](./cli/system-prompt.md):** Customize the core
instructions given to the model.
- **[Telemetry](./cli/telemetry.md):** Understand how usage data is collected
and managed.
- **[Enterprise](./cli/enterprise.md):** Manage configurations and policies for
large teams.
## Extensions
Extend Gemini CLI's capabilities with new tools and behaviors using extensions.
- **[Introduction](./extensions/index.md):** Learn about the extension system
and how to manage extensions.
- **[Writing extensions](./extensions/writing-extensions.md):** Learn how to
create your first extension.
- **[Extensions reference](./extensions/reference.md):** Deeply understand the
extension format, commands, and configuration.
- **[Best practices](./extensions/best-practices.md):** Learn strategies for
building great extensions.
- **[Extensions releasing](./extensions/releasing.md):** Learn how to share your
extensions with the world.
## Ecosystem and extensibility
Connect Gemini CLI to external services and other development tools.
- **[MCP servers](./tools/mcp-server.md):** Connect to external services using
the Model Context Protocol.
- **[IDE integration](./ide-integration/index.md):** Use Gemini CLI alongside VS
Code.
- **[Hooks](./hooks/index.md):** Write scripts that run on specific CLI events.
- **[Agent skills](./cli/skills.md):** Add specialized expertise and workflows.
- **[Sub-agents](./core/subagents.md):** (Preview) Delegate tasks to specialized
agents.
## Development and reference
Deep dive into the architecture and contribute to the project.
- **[Architecture](./architecture.md):** Understand the technical design of
- **[Gemini CLI quickstart](./get-started/index.md):** Let's get started with
Gemini CLI.
- **[Command reference](./cli/commands.md):** A complete list of available
commands.
- **[Local development](./local-development.md):** Set up your environment to
contribute to Gemini CLI.
- **[Contributing](../CONTRIBUTING.md):** Learn how to submit pull requests and
report issues.
- **[FAQ](./faq.md):** Answers to common questions.
- **[Troubleshooting](./troubleshooting.md):** Solutions for common issues.
- **[Gemini 3 Pro on Gemini CLI](./get-started/gemini-3.md):** Learn how to
enable and use Gemini 3.
- **[Authentication](./get-started/authentication.md):** Authenticate to Gemini
CLI.
- **[Configuration](./get-started/configuration.md):** Learn how to configure
the CLI.
- **[Installation](./get-started/installation.md):** Install and run Gemini CLI.
- **[Examples](./get-started/examples.md):** Example usage of Gemini CLI.
### CLI
- **[Introduction: Gemini CLI](./cli/index.md):** Overview of the command-line
interface.
- **[Commands](./cli/commands.md):** Description of available CLI commands.
- **[Checkpointing](./cli/checkpointing.md):** Documentation for the
checkpointing feature.
- **[Custom commands](./cli/custom-commands.md):** Create your own commands and
shortcuts for frequently used prompts.
- **[Enterprise](./cli/enterprise.md):** Gemini CLI for enterprise.
- **[Headless mode](./cli/headless.md):** Use Gemini CLI programmatically for
scripting and automation.
- **[Keyboard shortcuts](./cli/keyboard-shortcuts.md):** A reference for all
keyboard shortcuts to improve your workflow.
- **[Model selection](./cli/model.md):** Select the model used to process your
commands with `/model`.
- **[Sandbox](./cli/sandbox.md):** Isolate tool execution in a secure,
containerized environment.
- **[Agent Skills](./cli/skills.md):** Extend the CLI with specialized expertise
and procedural workflows.
- **[Settings](./cli/settings.md):** Configure various aspects of the CLI's
behavior and appearance with `/settings`.
- **[Telemetry](./cli/telemetry.md):** Overview of telemetry in the CLI.
- **[Themes](./cli/themes.md):** Themes for Gemini CLI.
- **[Token caching](./cli/token-caching.md):** Token caching and optimization.
- **[Trusted Folders](./cli/trusted-folders.md):** An overview of the Trusted
Folders security feature.
- **[Tutorials](./cli/tutorials.md):** Tutorials for Gemini CLI.
- **[Uninstall](./cli/uninstall.md):** Methods for uninstalling the Gemini CLI.
### Core
- **[Introduction: Gemini CLI core](./core/index.md):** Information about Gemini
CLI core.
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
exposes tools.
- **[System Prompt Override](./cli/system-prompt.md):** Replace built-in system
instructions using `GEMINI_SYSTEM_MD`.
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
fine-grained control over tool execution.
### Tools
- **[Introduction: Gemini CLI tools](./tools/index.md):** Information about
Gemini CLI's tools.
- **[File system tools](./tools/file-system.md):** Documentation for the
`read_file` and `write_file` tools.
- **[Shell tool](./tools/shell.md):** Documentation for the `run_shell_command`
tool.
- **[Web fetch tool](./tools/web-fetch.md):** Documentation for the `web_fetch`
tool.
- **[Web search tool](./tools/web-search.md):** Documentation for the
`google_web_search` tool.
- **[Memory tool](./tools/memory.md):** Documentation for the `save_memory`
tool.
- **[Todo tool](./tools/todos.md):** Documentation for the `write_todos` tool.
- **[MCP servers](./tools/mcp-server.md):** Using MCP servers with Gemini CLI.
### Extensions
- **[Introduction: Extensions](./extensions/index.md):** How to extend the CLI
with new functionality.
- **[Writing extensions](./extensions/writing-extensions.md):** Learn how to
build your own extension.
- **[Extension releasing](./extensions/releasing.md):** How to release Gemini
CLI extensions.
### Hooks
- **[Hooks](./hooks/index.md):** Intercept and customize Gemini CLI behavior at
key lifecycle points.
- **[Writing Hooks](./hooks/writing-hooks.md):** Learn how to create your first
hook with a comprehensive example.
- **[Best Practices](./hooks/best-practices.md):** Security, performance, and
debugging guidelines for hooks.
### IDE integration
- **[Introduction to IDE integration](./ide-integration/index.md):** Connect the
CLI to your editor.
- **[IDE companion extension spec](./ide-integration/ide-companion-spec.md):**
Spec for building IDE companion extensions.
### Development
- **[NPM](./npm.md):** Details on how the project's packages are structured.
- **[Releases](./releases.md):** Information on the project's releases and
deployment cadence.
- **[Changelog](./changelogs/index.md):** Highlights and notable changes to
Gemini CLI.
- **[Integration tests](./integration-tests.md):** Information about the
integration testing framework used in this project.
- **[Issue and PR automation](./issue-and-pr-automation.md):** A detailed
overview of the automated processes we use to manage and triage issues and
pull requests.
### Support
- **[FAQ](./faq.md):** Frequently asked questions.
- **[Troubleshooting guide](./troubleshooting.md):** Find solutions to common
problems.
- **[Quota and pricing](./quota-and-pricing.md):** Learn about the free tier and
paid options.
- **[Terms of service and privacy notice](./tos-privacy.md):** Information on
the terms of service and privacy notices applicable to your use of Gemini CLI.
We hope this documentation helps you make the most of Gemini CLI!
+8 -13
View File
@@ -20,7 +20,6 @@
{ "label": "Project context (GEMINI.md)", "slug": "docs/cli/gemini-md" },
{ "label": "Shell commands", "slug": "docs/tools/shell" },
{ "label": "Session management", "slug": "docs/cli/session-management" },
{ "label": "Plan mode (experimental)", "slug": "docs/cli/plan-mode" },
{ "label": "Todos", "slug": "docs/tools/todos" },
{ "label": "Web search and fetch", "slug": "docs/tools/web-search" }
]
@@ -80,10 +79,6 @@
"label": "Ecosystem and extensibility",
"items": [
{ "label": "Agent skills", "slug": "docs/cli/skills" },
{
"label": "Creating Agent skills",
"slug": "docs/cli/creating-skills"
},
{
"label": "Sub-agents (experimental)",
"slug": "docs/core/subagents"
@@ -124,6 +119,14 @@
"items": [
{ "label": "FAQ", "slug": "docs/faq" },
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
{
"label": "Releases",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
]
},
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
@@ -141,13 +144,5 @@
{ "label": "Local development", "slug": "docs/local-development" },
{ "label": "NPM package structure", "slug": "docs/npm" }
]
},
{
"label": "Releases",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
]
}
]
+6 -5
View File
@@ -117,13 +117,14 @@ directories) will be created.
`Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...`
- **Confirmation:** No.
## 5. `grep_search` (SearchText)
## 5. `search_file_content` (SearchText)
`grep_search` searches for a regular expression pattern within the content of
files in a specified directory. Can filter files by a glob pattern. Returns the
lines containing matches, along with their file paths and line numbers.
`search_file_content` searches for a regular expression pattern within the
content of files in a specified directory. Can filter files by a glob pattern.
Returns the lines containing matches, along with their file paths and line
numbers.
- **Tool name:** `grep_search`
- **Tool name:** `search_file_content`
- **Display name:** SearchText
- **File:** `grep.ts`
- **Parameters:**
-1
View File
@@ -63,7 +63,6 @@ const external = [
'@lydell/node-pty-win32-arm64',
'@lydell/node-pty-win32-x64',
'keytar',
'gemini-cli-devtools',
];
const baseConfig = {
+5 -36
View File
@@ -23,7 +23,6 @@ const __dirname = path.dirname(__filename);
// Determine the monorepo root (assuming eslint.config.js is at the root)
const projectRoot = __dirname;
const currentYear = new Date().getFullYear();
export default tseslint.config(
{
@@ -38,6 +37,7 @@ export default tseslint.config(
'dist/**',
'evals/**',
'packages/test-utils/**',
'packages/core/src/skills/builtin/skill-creator/scripts/*.cjs',
],
},
eslint.configs.recommended,
@@ -193,14 +193,6 @@ export default tseslint.config(
],
},
},
{
// Rules that only apply to product code
files: ['packages/*/src/**/*.{ts,tsx}'],
ignores: ['**/*.test.ts', '**/*.test.tsx'],
rules: {
'@typescript-eslint/no-unsafe-type-assertion': 'error',
},
},
{
// Allow os.homedir() in tests and paths.ts where it is used to implement the helper
files: [
@@ -251,7 +243,7 @@ export default tseslint.config(
},
},
{
files: ['./**/*.{tsx,ts,js,cjs}'],
files: ['./**/*.{tsx,ts,js}'],
plugins: {
headers,
import: importPlugin,
@@ -268,8 +260,8 @@ export default tseslint.config(
].join('\n'),
patterns: {
year: {
pattern: `202[5-${currentYear.toString().slice(-1)}]`,
defaultValue: currentYear.toString(),
pattern: '202[5-6]',
defaultValue: '2026',
},
},
},
@@ -277,6 +269,7 @@ export default tseslint.config(
'import/enforce-node-protocol-usage': ['error', 'always'],
},
},
// extra settings for scripts that we run directly with node
{
files: ['./scripts/**/*.js', 'esbuild.config.js'],
languageOptions: {
@@ -297,30 +290,6 @@ export default tseslint.config(
],
},
},
{
files: ['**/*.cjs'],
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.node,
},
},
rules: {
'no-restricted-syntax': 'off',
'no-console': 'off',
'no-empty': 'off',
'no-redeclare': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
{
files: ['packages/vscode-ide-companion/esbuild.js'],
languageOptions: {
-170
View File
@@ -1,170 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('Automated tool use', () => {
/**
* Tests that the agent always utilizes --fix when calling eslint.
* We provide a 'lint' script in the package.json, which helps elicit
* a repro by guiding the agent into using the existing deficient script.
*/
evalTest('USUALLY_PASSES', {
name: 'should use automated tools (eslint --fix) to fix code style issues',
files: {
'package.json': JSON.stringify(
{
name: 'typescript-project',
version: '1.0.0',
type: 'module',
scripts: {
lint: 'eslint .',
},
devDependencies: {
eslint: '^9.0.0',
globals: '^15.0.0',
typescript: '^5.0.0',
'typescript-eslint': '^8.0.0',
'@eslint/js': '^9.0.0',
},
},
null,
2,
),
'eslint.config.js': `
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{
files: ["**/*.{js,mjs,cjs,ts}"],
languageOptions: {
globals: globals.node
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"prefer-const": "error",
"@typescript-eslint/no-unused-vars": "off"
}
}
];
`,
'src/app.ts': `
export function main() {
let count = 10;
console.log(count);
}
`,
},
prompt:
'Fix the linter errors in this project. Make sure to avoid interactive commands.',
assert: async (rig) => {
// Check if run_shell_command was used with --fix
const toolCalls = rig.readToolLogs();
const shellCommands = toolCalls.filter(
(call) => call.toolRequest.name === 'run_shell_command',
);
const hasFixCommand = shellCommands.some((call) => {
let args = call.toolRequest.args;
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch (e) {
return false;
}
}
const cmd = (args as any)['command'];
return (
cmd &&
(cmd.includes('eslint') || cmd.includes('npm run lint')) &&
cmd.includes('--fix')
);
});
expect(
hasFixCommand,
'Expected agent to use eslint --fix via run_shell_command',
).toBe(true);
},
});
/**
* Tests that the agent uses prettier --write to fix formatting issues in files
* instead of trying to edit the files itself.
*/
evalTest('USUALLY_PASSES', {
name: 'should use automated tools (prettier --write) to fix formatting issues',
files: {
'package.json': JSON.stringify(
{
name: 'typescript-project',
version: '1.0.0',
type: 'module',
scripts: {},
devDependencies: {
prettier: '^3.0.0',
typescript: '^5.0.0',
},
},
null,
2,
),
'.prettierrc': JSON.stringify(
{
semi: true,
singleQuote: true,
},
null,
2,
),
'src/app.ts': `
export function main() {
const data={ name:'test',
val:123
}
console.log(data)
}
`,
},
prompt:
'Fix the formatting errors in this project. Make sure to avoid interactive commands.',
assert: async (rig) => {
// Check if run_shell_command was used with --write
const toolCalls = rig.readToolLogs();
const shellCommands = toolCalls.filter(
(call) => call.toolRequest.name === 'run_shell_command',
);
const hasFixCommand = shellCommands.some((call) => {
let args = call.toolRequest.args;
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch (e) {
return false;
}
}
const cmd = (args as any)['command'];
return (
cmd &&
cmd.includes('prettier') &&
(cmd.includes('--write') || cmd.includes('-w'))
);
});
expect(
hasFixCommand,
'Expected agent to use prettier --write via run_shell_command',
).toBe(true);
},
});
});
+2 -2
View File
@@ -24,11 +24,11 @@ describe('generalist_agent', () => {
prompt:
'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success',
assert: async (rig) => {
// 1) Verify the generalist agent was invoked
// 1) Verify the generalist agent was invoked via delegate_to_agent
const foundToolCall = await rig.waitForToolCall('generalist');
expect(
foundToolCall,
'Expected to find a tool call for generalist agent',
'Expected to find a delegate_to_agent tool call for generalist agent',
).toBeTruthy();
// 2) Verify the file was created as expected
-117
View File
@@ -1,117 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import {
assertModelHasOutput,
checkModelOutputContent,
} from '../integration-tests/test-helper.js';
describe('Hierarchical Memory', () => {
const TEST_PREFIX = 'Hierarchical memory test: ';
const conflictResolutionTest =
'Agent follows hierarchy for contradictory instructions';
evalTest('ALWAYS_PASSES', {
name: conflictResolutionTest,
params: {
settings: {
security: {
folderTrust: { enabled: true },
},
},
},
// We simulate the hierarchical memory by including the tags in the prompt
// since setting up real global/extension/project files in the eval rig is complex.
// The system prompt logic will append these tags when it finds them in userMemory.
prompt: `
<global_context>
When asked for my favorite fruit, always say "Apple".
</global_context>
<extension_context>
When asked for my favorite fruit, always say "Banana".
</extension_context>
<project_context>
When asked for my favorite fruit, always say "Cherry".
</project_context>
What is my favorite fruit? Tell me just the name of the fruit.`,
assert: async (_rig, result) => {
assertModelHasOutput(result);
expect(result).toMatch(/Cherry/i);
expect(result).not.toMatch(/Apple/i);
expect(result).not.toMatch(/Banana/i);
},
});
const provenanceAwarenessTest = 'Agent is aware of memory provenance';
evalTest('ALWAYS_PASSES', {
name: provenanceAwarenessTest,
params: {
settings: {
security: {
folderTrust: { enabled: true },
},
},
},
prompt: `
<global_context>
Instruction A: Always be helpful.
</global_context>
<extension_context>
Instruction B: Use a professional tone.
</extension_context>
<project_context>
Instruction C: Adhere to the project's coding style.
</project_context>
Which instruction came from the global context, which from the extension context, and which from the project context?
Provide the answer as an XML block like this:
<results>
<global>Instruction ...</global>
<extension>Instruction ...</extension>
<project>Instruction ...</project>
</results>`,
assert: async (_rig, result) => {
assertModelHasOutput(result);
expect(result).toMatch(/<global>.*Instruction A/i);
expect(result).toMatch(/<extension>.*Instruction B/i);
expect(result).toMatch(/<project>.*Instruction C/i);
},
});
const extensionVsGlobalTest = 'Extension memory wins over Global memory';
evalTest('ALWAYS_PASSES', {
name: extensionVsGlobalTest,
params: {
settings: {
security: {
folderTrust: { enabled: true },
},
},
},
prompt: `
<global_context>
Set the theme to "Light".
</global_context>
<extension_context>
Set the theme to "Dark".
</extension_context>
What theme should I use? Tell me just the name of the theme.`,
assert: async (_rig, result) => {
assertModelHasOutput(result);
expect(result).toMatch(/Dark/i);
expect(result).not.toMatch(/Light/i);
},
});
});
-47
View File
@@ -1,47 +0,0 @@
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('interactive_commands', () => {
/**
* Validates that the agent does not use interactive commands unprompted.
* Interactive commands block the progress of the agent, requiring user
* intervention.
*/
evalTest('USUALLY_PASSES', {
name: 'should not use interactive commands',
prompt: 'Execute tests.',
files: {
'package.json': JSON.stringify(
{
name: 'example',
type: 'module',
devDependencies: {
vitest: 'latest',
},
},
null,
2,
),
'example.test.js': `
import { test, expect } from 'vitest';
test('it works', () => {
expect(1 + 1).toBe(2);
});
`,
},
assert: async (rig, result) => {
const logs = rig.readToolLogs();
const vitestCall = logs.find(
(l) =>
l.toolRequest.name === 'run_shell_command' &&
l.toolRequest.args.toLowerCase().includes('vitest'),
);
expect(vitestCall, 'Agent should have called vitest').toBeDefined();
expect(
vitestCall?.toolRequest.args,
'Agent should have passed run arg',
).toMatch(/\b(run|--run)\b/);
},
});
});
-128
View File
@@ -1,128 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { ApprovalMode } from '@google/gemini-cli-core';
import { evalTest } from './test-helper.js';
import {
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
describe('plan_mode', () => {
const TEST_PREFIX = 'Plan Mode: ';
const settings = {
experimental: { plan: true },
};
evalTest('USUALLY_PASSES', {
name: 'should refuse file modification when in plan mode',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
files: {
'README.md': '# Original Content',
},
prompt: 'Please overwrite README.md with the text "Hello World"',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeTargets = toolLogs
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
)
.map((log) => {
try {
return JSON.parse(log.toolRequest.args).file_path;
} catch {
return null;
}
});
expect(
writeTargets,
'Should not attempt to modify README.md in plan mode',
).not.toContain('README.md');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/plan mode|read-only|cannot modify|refuse|exiting/i],
testName: `${TEST_PREFIX}should refuse file modification`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should enter plan mode when asked to create a plan',
approvalMode: ApprovalMode.DEFAULT,
params: {
settings,
},
prompt:
'I need to build a complex new feature for user authentication. Please create a detailed implementation plan.',
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('enter_plan_mode');
expect(wasToolCalled, 'Expected enter_plan_mode tool to be called').toBe(
true,
);
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should exit plan mode when plan is complete and implementation is requested',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
files: {
'plans/my-plan.md':
'# My Implementation Plan\n\n1. Step one\n2. Step two',
},
prompt:
'The plan in plans/my-plan.md is solid. Please proceed with the implementation.',
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('exit_plan_mode');
expect(wasToolCalled, 'Expected exit_plan_mode tool to be called').toBe(
true,
);
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should allow file modification in plans directory when in plan mode',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
prompt: 'Create a plan for a new login feature.',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeCall = toolLogs.find(
(log) => log.toolRequest.name === 'write_file',
);
expect(
writeCall,
'Should attempt to modify a file in the plans directory when in plan mode',
).toBeDefined();
if (writeCall) {
const args = JSON.parse(writeCall.toolRequest.args);
expect(args.file_path).toContain('.gemini/tmp');
expect(args.file_path).toContain('/plans/');
expect(args.file_path).toMatch(/\.md$/);
}
assertModelHasOutput(result);
},
});
});
+7 -259
View File
@@ -6,16 +6,11 @@
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import {
assertModelHasOutput,
checkModelOutputContent,
} from '../integration-tests/test-helper.js';
import { validateModelOutput } from '../integration-tests/test-helper.js';
describe('save_memory', () => {
const TEST_PREFIX = 'Save memory test: ';
const rememberingFavoriteColor = "Agent remembers user's favorite color";
evalTest('ALWAYS_PASSES', {
name: rememberingFavoriteColor,
name: 'should be able to save to memory',
params: {
settings: { tools: { core: ['save_memory'] } },
},
@@ -23,260 +18,13 @@ describe('save_memory', () => {
what is my favorite color? tell me that and surround it with $ symbol`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'blue',
testName: `${TEST_PREFIX}${rememberingFavoriteColor}`,
});
},
});
const rememberingCommandRestrictions = 'Agent remembers command restrictions';
evalTest('ALWAYS_PASSES', {
name: rememberingCommandRestrictions,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `I don't want you to ever run npm commands.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/not run npm commands|remember|ok/i],
testName: `${TEST_PREFIX}${rememberingCommandRestrictions}`,
});
},
});
const rememberingWorkflow = 'Agent remembers workflow preferences';
evalTest('ALWAYS_PASSES', {
name: rememberingWorkflow,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `I want you to always lint after building.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/always|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingWorkflow}`,
});
},
});
const ignoringTemporaryInformation =
'Agent ignores temporary conversation details';
evalTest('ALWAYS_PASSES', {
name: ignoringTemporaryInformation,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `I'm going to get a coffee.`,
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
const foundToolCall = await rig.waitForToolCall('save_memory');
expect(
wasToolCalled,
'save_memory should not be called for temporary information',
).toBe(false);
foundToolCall,
'Expected to find a save_memory tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
testName: `${TEST_PREFIX}${ignoringTemporaryInformation}`,
forbiddenContent: [/remember|will do/i],
});
},
});
const rememberingPetName = "Agent remembers user's pet's name";
evalTest('ALWAYS_PASSES', {
name: rememberingPetName,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `Please remember that my dog's name is Buddy.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/Buddy/i],
testName: `${TEST_PREFIX}${rememberingPetName}`,
});
},
});
const rememberingCommandAlias = 'Agent remembers custom command aliases';
evalTest('ALWAYS_PASSES', {
name: rememberingCommandAlias,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `When I say 'start server', you should run 'npm run dev'.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/npm run dev|start server|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingCommandAlias}`,
});
},
});
const ignoringDbSchemaLocation =
"Agent ignores workspace's database schema location";
evalTest('ALWAYS_PASSES', {
name: ignoringDbSchemaLocation,
params: {
settings: {
tools: {
core: [
'save_memory',
'list_directory',
'read_file',
'run_shell_command',
],
},
},
},
prompt: `The database schema for this workspace is located in \`db/schema.sql\`.`,
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'save_memory should not be called for workspace-specific information',
).toBe(false);
assertModelHasOutput(result);
},
});
const rememberingCodingStyle =
"Agent remembers user's coding style preference";
evalTest('ALWAYS_PASSES', {
name: rememberingCodingStyle,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `I prefer to use tabs instead of spaces for indentation.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/tabs instead of spaces|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingCodingStyle}`,
});
},
});
const ignoringBuildArtifactLocation =
'Agent ignores workspace build artifact location';
evalTest('ALWAYS_PASSES', {
name: ignoringBuildArtifactLocation,
params: {
settings: {
tools: {
core: [
'save_memory',
'list_directory',
'read_file',
'run_shell_command',
],
},
},
},
prompt: `In this workspace, build artifacts are stored in the \`dist/artifacts\` directory.`,
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'save_memory should not be called for workspace-specific information',
).toBe(false);
assertModelHasOutput(result);
},
});
const ignoringMainEntryPoint = "Agent ignores workspace's main entry point";
evalTest('ALWAYS_PASSES', {
name: ignoringMainEntryPoint,
params: {
settings: {
tools: {
core: [
'save_memory',
'list_directory',
'read_file',
'run_shell_command',
],
},
},
},
prompt: `The main entry point for this workspace is \`src/index.js\`.`,
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'save_memory should not be called for workspace-specific information',
).toBe(false);
assertModelHasOutput(result);
},
});
const rememberingBirthday = "Agent remembers user's birthday";
evalTest('ALWAYS_PASSES', {
name: rememberingBirthday,
params: {
settings: { tools: { core: ['save_memory'] } },
},
prompt: `My birthday is on June 15th.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory tool to be called').toBe(
true,
);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/June 15th|ok|remember|will do/i],
testName: `${TEST_PREFIX}${rememberingBirthday}`,
});
validateModelOutput(result, 'blue', 'Save memory test');
},
});
});
-110
View File
@@ -1,110 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('Shell Efficiency', () => {
const getCommand = (call: any): string | undefined => {
let args = call.toolRequest.args;
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch (e) {
// Ignore parse errors
}
}
return typeof args === 'string' ? args : (args as any)['command'];
};
evalTest('USUALLY_PASSES', {
name: 'should use --silent/--quiet flags when installing packages',
prompt: 'Install the "lodash" package using npm.',
assert: async (rig) => {
const toolCalls = rig.readToolLogs();
const shellCalls = toolCalls.filter(
(call) => call.toolRequest.name === 'run_shell_command',
);
const hasEfficiencyFlag = shellCalls.some((call) => {
const cmd = getCommand(call);
return (
cmd &&
cmd.includes('npm install') &&
(cmd.includes('--silent') ||
cmd.includes('--quiet') ||
cmd.includes('-q'))
);
});
expect(
hasEfficiencyFlag,
`Expected agent to use efficiency flags for npm install. Commands used: ${shellCalls
.map(getCommand)
.join(', ')}`,
).toBe(true);
},
});
evalTest('USUALLY_PASSES', {
name: 'should use --no-pager with git commands',
prompt: 'Show the git log.',
assert: async (rig) => {
const toolCalls = rig.readToolLogs();
const shellCalls = toolCalls.filter(
(call) => call.toolRequest.name === 'run_shell_command',
);
const hasNoPager = shellCalls.some((call) => {
const cmd = getCommand(call);
return cmd && cmd.includes('git') && cmd.includes('--no-pager');
});
expect(
hasNoPager,
`Expected agent to use --no-pager with git. Commands used: ${shellCalls
.map(getCommand)
.join(', ')}`,
).toBe(true);
},
});
evalTest('USUALLY_PASSES', {
name: 'should NOT use efficiency flags when enableShellOutputEfficiency is disabled',
params: {
settings: {
tools: {
shell: {
enableShellOutputEfficiency: false,
},
},
},
},
prompt: 'Install the "lodash" package using npm.',
assert: async (rig) => {
const toolCalls = rig.readToolLogs();
const shellCalls = toolCalls.filter(
(call) => call.toolRequest.name === 'run_shell_command',
);
const hasEfficiencyFlag = shellCalls.some((call) => {
const cmd = getCommand(call);
return (
cmd &&
cmd.includes('npm install') &&
(cmd.includes('--silent') ||
cmd.includes('--quiet') ||
cmd.includes('-q'))
);
});
expect(
hasEfficiencyFlag,
'Agent used efficiency flags even though enableShellOutputEfficiency was disabled',
).toBe(false);
},
});
});
+3 -67
View File
@@ -7,13 +7,9 @@
import { it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { execSync } from 'node:child_process';
import { TestRig } from '@google/gemini-cli-test-utils';
import {
createUnauthorizedToolError,
parseAgentMarkdown,
} from '@google/gemini-cli-core';
import { createUnauthorizedToolError } from '@google/gemini-cli-core';
export * from '@google/gemini-cli-test-utils';
@@ -45,64 +41,11 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
try {
rig.setup(evalCase.name, evalCase.params);
// Symlink node modules to reduce the amount of time needed to
// bootstrap test projects.
const rootNodeModules = path.join(process.cwd(), 'node_modules');
const testNodeModules = path.join(rig.testDir || '', 'node_modules');
if (fs.existsSync(rootNodeModules) && !fs.existsSync(testNodeModules)) {
fs.symlinkSync(rootNodeModules, testNodeModules, 'dir');
}
if (evalCase.files) {
const acknowledgedAgents: Record<string, Record<string, string>> = {};
const projectRoot = fs.realpathSync(rig.testDir!);
for (const [filePath, content] of Object.entries(evalCase.files)) {
const fullPath = path.join(rig.testDir!, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
// If it's an agent file, calculate hash for acknowledgement
if (
filePath.startsWith('.gemini/agents/') &&
filePath.endsWith('.md')
) {
const hash = crypto
.createHash('sha256')
.update(content)
.digest('hex');
try {
const agentDefs = await parseAgentMarkdown(fullPath, content);
if (agentDefs.length > 0) {
const agentName = agentDefs[0].name;
if (!acknowledgedAgents[projectRoot]) {
acknowledgedAgents[projectRoot] = {};
}
acknowledgedAgents[projectRoot][agentName] = hash;
}
} catch (error) {
console.warn(
`Failed to parse agent for test acknowledgement: ${filePath}`,
error,
);
}
}
}
// Write acknowledged_agents.json to the home directory
if (Object.keys(acknowledgedAgents).length > 0) {
const ackPath = path.join(
rig.homeDir!,
'.gemini',
'acknowledgments',
'agents.json',
);
fs.mkdirSync(path.dirname(ackPath), { recursive: true });
fs.writeFileSync(
ackPath,
JSON.stringify(acknowledgedAgents, null, 2),
);
}
const execOptions = { cwd: rig.testDir!, stdio: 'inherit' as const };
@@ -123,9 +66,8 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
const result = await rig.run({
args: evalCase.prompt,
approvalMode: evalCase.approvalMode ?? 'yolo',
timeout: evalCase.timeout,
env: {
GEMINI_CLI_ACTIVITY_LOG_TARGET: activityLogFile,
GEMINI_CLI_ACTIVITY_LOG_FILE: activityLogFile,
},
});
@@ -146,11 +88,6 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
});
}
if (rig._lastRunStderr) {
const stderrFile = path.join(logDir, `${sanitizedName}.stderr.log`);
await fs.promises.writeFile(stderrFile, rig._lastRunStderr);
}
await fs.promises.writeFile(
logFile,
JSON.stringify(rig.readToolLogs(), null, 2),
@@ -162,7 +99,7 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
if (policy === 'USUALLY_PASSES' && !process.env['RUN_EVALS']) {
it.skip(evalCase.name, fn);
} else {
it(evalCase.name, fn, evalCase.timeout);
it(evalCase.name, fn);
}
}
@@ -177,7 +114,6 @@ export interface EvalCase {
name: string;
params?: Record<string, any>;
prompt: string;
timeout?: number;
files?: Record<string, string>;
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
assert: (rig: TestRig, result: string) => Promise<void>;
-85
View File
@@ -1,85 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('validation_fidelity', () => {
evalTest('ALWAYS_PASSES', {
name: 'should perform exhaustive validation autonomously when guided by system instructions',
files: {
'src/types.ts': `
export interface LogEntry {
level: 'info' | 'warn' | 'error';
message: string;
}
`,
'src/logger.ts': `
import { LogEntry } from './types.js';
export function formatLog(entry: LogEntry): string {
return \`[\${entry.level.toUpperCase()}] \${entry.message}\`;
}
`,
'src/logger.test.ts': `
import { expect, test } from 'vitest';
import { formatLog } from './logger.js';
import { LogEntry } from './types.js';
test('formats log correctly', () => {
const entry: LogEntry = { level: 'info', message: 'test message' };
expect(formatLog(entry)).toBe('[INFO] test message');
});
`,
'package.json': JSON.stringify({
name: 'test-project',
type: 'module',
scripts: {
test: 'vitest run',
build: 'tsc --noEmit',
},
}),
'tsconfig.json': JSON.stringify({
compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'node',
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
}),
},
prompt:
"Refactor the 'LogEntry' interface in 'src/types.ts' to rename the 'message' field to 'payload'.",
timeout: 600000,
assert: async (rig) => {
// The goal of this eval is to see if the agent realizes it needs to update usages
// AND run 'npm run build' or 'tsc' autonomously to ensure project-wide structural integrity.
const toolLogs = rig.readToolLogs();
const shellCalls = toolLogs.filter(
(log) => log.toolRequest.name === 'run_shell_command',
);
const hasBuildOrTsc = shellCalls.some((log) => {
const cmd = JSON.parse(log.toolRequest.args).command.toLowerCase();
return (
cmd.includes('npm run build') ||
cmd.includes('tsc') ||
cmd.includes('typecheck') ||
cmd.includes('npm run verify')
);
});
expect(
hasBuildOrTsc,
'Expected the agent to autonomously run a build or type-check command to verify the refactoring',
).toBe(true);
},
});
});
@@ -1,79 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('validation_fidelity_pre_existing_errors', () => {
evalTest('USUALLY_PASSES', {
name: 'should handle pre-existing project errors gracefully during validation',
files: {
'src/math.ts': `
export function add(a: number, b: number): number {
return a + b;
}
`,
'src/index.ts': `
import { add } from './math.js';
console.log(add(1, 2));
`,
'src/utils.ts': `
export function multiply(a: number, b: number): number {
return a * c; // 'c' is not defined - PRE-EXISTING ERROR
}
`,
'package.json': JSON.stringify({
name: 'test-project',
type: 'module',
scripts: {
test: 'vitest run',
build: 'tsc --noEmit',
},
}),
'tsconfig.json': JSON.stringify({
compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'node',
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
}),
},
prompt: "In src/math.ts, rename the 'add' function to 'sum'.",
timeout: 600000,
assert: async (rig) => {
const toolLogs = rig.readToolLogs();
const replaceCalls = toolLogs.filter(
(log) => log.toolRequest.name === 'replace',
);
// Verify it did the work in math.ts
const mathRefactor = replaceCalls.some((log) => {
const args = JSON.parse(log.toolRequest.args);
return (
args.file_path.endsWith('src/math.ts') &&
args.new_string.includes('sum')
);
});
expect(mathRefactor, 'Agent should have refactored math.ts').toBe(true);
const shellCalls = toolLogs.filter(
(log) => log.toolRequest.name === 'run_shell_command',
);
const ranValidation = shellCalls.some((log) => {
const cmd = JSON.parse(log.toolRequest.args).command.toLowerCase();
return cmd.includes('build') || cmd.includes('tsc');
});
expect(ranValidation, 'Agent should have attempted validation').toBe(
true,
);
},
});
});
-163
View File
@@ -1,163 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { spawn, ChildProcess } from 'node:child_process';
import { join, resolve } from 'node:path';
import { writeFileSync, mkdirSync } from 'node:fs';
import { Writable, Readable } from 'node:stream';
import { env } from 'node:process';
import * as acp from '@agentclientprotocol/sdk';
const sandboxEnv = env['GEMINI_SANDBOX'];
const itMaybe = sandboxEnv && sandboxEnv !== 'false' ? it.skip : it;
class MockClient implements acp.Client {
updates: acp.SessionNotification[] = [];
sessionUpdate = async (params: acp.SessionNotification) => {
this.updates.push(params);
};
requestPermission = async (): Promise<acp.RequestPermissionResponse> => {
throw new Error('unexpected');
};
}
describe('ACP Environment and Auth', () => {
let rig: TestRig;
let child: ChildProcess | undefined;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
child?.kill();
child = undefined;
await rig.cleanup();
});
itMaybe(
'should load .env from project directory and use the provided API key',
async () => {
rig.setup('acp-env-loading');
// Create a project directory with a .env file containing a recognizable invalid key
const projectDir = resolve(join(rig.testDir!, 'project'));
mkdirSync(projectDir, { recursive: true });
writeFileSync(
join(projectDir, '.env'),
'GEMINI_API_KEY=test-key-from-env\n',
);
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn('node', [bundlePath, '--experimental-acp'], {
cwd: rig.homeDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
...process.env,
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_API_KEY: undefined,
VERBOSE: 'true',
},
});
const input = Writable.toWeb(child.stdin!);
const output = Readable.toWeb(
child.stdout!,
) as ReadableStream<Uint8Array>;
const testClient = new MockClient();
const stream = acp.ndJsonStream(input, output);
const connection = new acp.ClientSideConnection(() => testClient, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: {
fs: { readTextFile: false, writeTextFile: false },
},
});
// 1. newSession should succeed because it finds the key in .env
const { sessionId } = await connection.newSession({
cwd: projectDir,
mcpServers: [],
});
expect(sessionId).toBeDefined();
// 2. prompt should fail because the key is invalid,
// but the error should come from the API, not the internal auth check.
await expect(
connection.prompt({
sessionId,
prompt: [{ type: 'text', text: 'hello' }],
}),
).rejects.toSatisfy((error: unknown) => {
const acpError = error as acp.RequestError;
const errorData = acpError.data as
| { error?: { message?: string } }
| undefined;
const message = String(errorData?.error?.message || acpError.message);
// It should NOT be our internal "Authentication required" message
expect(message).not.toContain('Authentication required');
// It SHOULD be an API error mentioning the invalid key
expect(message).toContain('API key not valid');
return true;
});
child.stdin!.end();
},
);
itMaybe(
'should fail with authRequired when no API key is found',
async () => {
rig.setup('acp-auth-failure');
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn('node', [bundlePath, '--experimental-acp'], {
cwd: rig.homeDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
...process.env,
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_API_KEY: undefined,
VERBOSE: 'true',
},
});
const input = Writable.toWeb(child.stdin!);
const output = Readable.toWeb(
child.stdout!,
) as ReadableStream<Uint8Array>;
const testClient = new MockClient();
const stream = acp.ndJsonStream(input, output);
const connection = new acp.ClientSideConnection(() => testClient, stream);
await connection.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: {
fs: { readTextFile: false, writeTextFile: false },
},
});
await expect(
connection.newSession({
cwd: resolve(rig.testDir!),
mcpServers: [],
}),
).rejects.toMatchObject({
message: expect.stringContaining(
'Gemini API key is missing or not configured.',
),
});
child.stdin!.end();
},
);
});
-155
View File
@@ -1,155 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { GitService, Storage } from '@google/gemini-cli-core';
describe('Checkpointing Integration', () => {
let tmpDir: string;
let projectRoot: string;
let fakeHome: string;
let originalEnv: NodeJS.ProcessEnv;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'gemini-checkpoint-test-'),
);
projectRoot = path.join(tmpDir, 'project');
fakeHome = path.join(tmpDir, 'home');
await fs.mkdir(projectRoot, { recursive: true });
await fs.mkdir(fakeHome, { recursive: true });
// Save original env
originalEnv = { ...process.env };
// Simulate environment with NO global gitconfig
process.env['HOME'] = fakeHome;
delete process.env['GIT_CONFIG_GLOBAL'];
delete process.env['GIT_CONFIG_SYSTEM'];
});
afterEach(async () => {
// Restore env
process.env = originalEnv;
// Cleanup
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch (e) {
console.error('Failed to cleanup temp dir', e);
}
});
it('should successfully create and restore snapshots without global git config', async () => {
const storage = new Storage(projectRoot);
const gitService = new GitService(projectRoot, storage);
// 1. Initialize
await gitService.initialize();
// Verify system config empty file creation
// We need to access getHistoryDir logic or replicate it.
// Since we don't have access to private getHistoryDir, we can infer it or just trust the functional test.
// 2. Create initial state
await fs.writeFile(path.join(projectRoot, 'file1.txt'), 'version 1');
await fs.writeFile(path.join(projectRoot, 'file2.txt'), 'permanent file');
// 3. Create Snapshot
const snapshotHash = await gitService.createFileSnapshot('Checkpoint 1');
expect(snapshotHash).toBeDefined();
// 4. Modify files
await fs.writeFile(
path.join(projectRoot, 'file1.txt'),
'version 2 (BAD CHANGE)',
);
await fs.writeFile(
path.join(projectRoot, 'file3.txt'),
'new file (SHOULD BE GONE)',
);
await fs.rm(path.join(projectRoot, 'file2.txt'));
// 5. Restore
await gitService.restoreProjectFromSnapshot(snapshotHash);
// 6. Verify state
const file1Content = await fs.readFile(
path.join(projectRoot, 'file1.txt'),
'utf-8',
);
expect(file1Content).toBe('version 1');
const file2Exists = await fs
.stat(path.join(projectRoot, 'file2.txt'))
.then(() => true)
.catch(() => false);
expect(file2Exists).toBe(true);
const file2Content = await fs.readFile(
path.join(projectRoot, 'file2.txt'),
'utf-8',
);
expect(file2Content).toBe('permanent file');
const file3Exists = await fs
.stat(path.join(projectRoot, 'file3.txt'))
.then(() => true)
.catch(() => false);
expect(file3Exists).toBe(false);
});
it('should ignore user global git config and use isolated identity', async () => {
// 1. Create a fake global gitconfig with a specific user
const globalConfigPath = path.join(fakeHome, '.gitconfig');
const globalConfigContent = `[user]
name = Global User
email = global@example.com
`;
await fs.writeFile(globalConfigPath, globalConfigContent);
// Point HOME to fakeHome so git picks up this global config (if we didn't isolate it)
process.env['HOME'] = fakeHome;
// Ensure GIT_CONFIG_GLOBAL is NOT set for the process initially,
// so it would default to HOME/.gitconfig if GitService didn't override it.
delete process.env['GIT_CONFIG_GLOBAL'];
const storage = new Storage(projectRoot);
const gitService = new GitService(projectRoot, storage);
await gitService.initialize();
// 2. Create a file and snapshot
await fs.writeFile(path.join(projectRoot, 'test.txt'), 'content');
await gitService.createFileSnapshot('Snapshot with global config present');
// 3. Verify the commit author in the shadow repo
const historyDir = storage.getHistoryDir();
const { execFileSync } = await import('node:child_process');
const logOutput = execFileSync(
'git',
['log', '-1', '--pretty=format:%an <%ae>'],
{
cwd: historyDir,
env: {
...process.env,
GIT_DIR: path.join(historyDir, '.git'),
GIT_CONFIG_GLOBAL: path.join(historyDir, '.gitconfig'),
GIT_CONFIG_SYSTEM: path.join(historyDir, '.gitconfig_system_empty'),
},
encoding: 'utf-8',
},
);
expect(logOutput).toBe('Gemini CLI <gemini-cli@google.com>');
expect(logOutput).not.toContain('Global User');
});
});
+5 -13
View File
@@ -7,12 +7,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import {
TestRig,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('file-system', () => {
let rig: TestRig;
@@ -48,11 +43,8 @@ describe('file-system', () => {
'Expected to find a read_file tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'hello world',
testName: 'File read test',
});
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, 'hello world', 'File read test');
});
it('should be able to write a file', async () => {
@@ -82,8 +74,8 @@ describe('file-system', () => {
'Expected to find a write_file, edit, or replace tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, { testName: 'File write test' });
// Validate model output - will throw if no output
validateModelOutput(result, null, 'File write test');
const fileContent = rig.readFile('test.txt');
-6
View File
@@ -13,7 +13,6 @@ import { mkdir, readdir, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
import { disableMouseTracking } from '@google/gemini-cli-core';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
@@ -73,11 +72,6 @@ export async function setup() {
}
export async function teardown() {
// Disable mouse tracking
if (process.stdout.isTTY) {
disableMouseTracking();
}
// Cleanup the test run directory unless KEEP_OUTPUT is set
if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
try {
+7 -11
View File
@@ -6,12 +6,7 @@
import { WEB_SEARCH_TOOL_NAME } from '../packages/core/src/tools/tool-names.js';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
TestRig,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('web search tool', () => {
let rig: TestRig;
@@ -73,11 +68,12 @@ describe('web search tool', () => {
`Expected to find a call to ${WEB_SEARCH_TOOL_NAME}`,
).toBeTruthy();
assertModelHasOutput(result);
const hasExpectedContent = checkModelOutputContent(result, {
expectedContent: ['weather', 'london'],
testName: 'Google web search test',
});
// Validate model output - will throw if no output, warn if missing expected content
const hasExpectedContent = validateModelOutput(
result,
['weather', 'london'],
'Google web search test',
);
// If content was missing, log the search queries used
if (!hasExpectedContent) {
+3 -7
View File
@@ -9,8 +9,7 @@ import {
TestRig,
poll,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
validateModelOutput,
} from './test-helper.js';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
@@ -69,10 +68,7 @@ describe('list_directory', () => {
throw e;
}
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: ['file1.txt', 'subdir'],
testName: 'List directory test',
});
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, ['file1.txt', 'subdir'], 'List directory test');
});
});
+3 -8
View File
@@ -5,12 +5,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
TestRig,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('read_many_files', () => {
let rig: TestRig;
@@ -55,7 +50,7 @@ describe('read_many_files', () => {
'Expected to find either read_many_files or multiple read_file tool calls',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, { testName: 'Read many files test' });
// Validate model output - will throw if no output
validateModelOutput(result, null, 'Read many files test');
});
});
-1
View File
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Session started."}],"role":"model"},"finishReason":"STOP","index":0}]}]}
-42
View File
@@ -1,42 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('resume-repro', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('should be able to resume a session without "Storage must be initialized before use"', async () => {
const responsesPath = path.join(__dirname, 'resume_repro.responses');
await rig.setup('should be able to resume a session', {
fakeResponsesPath: responsesPath,
});
// 1. First run to create a session
await rig.run({
args: 'hello',
});
// 2. Second run with --resume latest
// This should NOT fail with "Storage must be initialized before use"
const result = await rig.run({
args: ['--resume', 'latest', 'continue'],
});
expect(result).toContain('Session started');
});
});
-4
View File
@@ -28,10 +28,6 @@ class MockConfig {
return true;
}
getFileFilteringRespectGitIgnore() {
return true;
}
getFileFilteringRespectGeminiIgnore() {
return true;
}
+19 -38
View File
@@ -5,12 +5,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
TestRig,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
import { getShellConfiguration } from '../packages/core/src/utils/shell-utils.js';
const { shell } = getShellConfiguration();
@@ -120,11 +115,13 @@ describe('run_shell_command', () => {
'Expected to find a run_shell_command tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: ['hello-world', 'exit code 0'],
testName: 'Shell command test',
});
// Validate model output - will throw if no output, warn if missing expected content
// Model often reports exit code instead of showing output
validateModelOutput(
result,
['hello-world', 'exit code 0'],
'Shell command test',
);
});
it('should be able to run a shell command via stdin', async () => {
@@ -152,11 +149,8 @@ describe('run_shell_command', () => {
'Expected to find a run_shell_command tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'test-stdin',
testName: 'Shell command stdin test',
});
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, 'test-stdin', 'Shell command stdin test');
});
it.skip('should run allowed sub-command in non-interactive mode', async () => {
@@ -500,11 +494,12 @@ describe('run_shell_command', () => {
)[0];
expect(toolCall.toolRequest.success).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'test-allow-all',
testName: 'Shell command stdin allow all',
});
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(
result,
'test-allow-all',
'Shell command stdin allow all',
);
});
it('should propagate environment variables to the child process', async () => {
@@ -533,11 +528,7 @@ describe('run_shell_command', () => {
foundToolCall,
'Expected to find a run_shell_command tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: varValue,
testName: 'Env var propagation test',
});
validateModelOutput(result, varValue, 'Env var propagation test');
expect(result).toContain(varValue);
} finally {
delete process.env[varName];
@@ -567,27 +558,17 @@ describe('run_shell_command', () => {
'Expected to find a run_shell_command tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: fileName,
testName: 'Platform-specific listing test',
});
validateModelOutput(result, fileName, 'Platform-specific listing test');
expect(result).toContain(fileName);
});
it('rejects invalid shell expressions', async () => {
await rig.setup('rejects invalid shell expressions', {
settings: {
tools: {
core: ['run_shell_command'],
allowed: ['run_shell_command(echo)'], // Specifically allow echo
},
},
settings: { tools: { core: ['run_shell_command'] } },
});
const invalidCommand = getInvalidCommand();
const result = await rig.run({
args: `I am testing the error handling of the run_shell_command tool. Please attempt to run the following command, which I know has invalid syntax: \`${invalidCommand}\`. If the command fails as expected, please return the word FAIL, otherwise return the word SUCCESS.`,
approvalMode: 'default', // Use default mode so safety fallback triggers confirmation
});
expect(result).toContain('FAIL');
+3 -11
View File
@@ -11,12 +11,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
TestRig,
poll,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
import { TestRig, poll, validateModelOutput } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
@@ -231,11 +226,8 @@ describe.skip('simple-mcp-server', () => {
expect(foundToolCall, 'Expected to find an add tool call').toBeTruthy();
assertModelHasOutput(output);
checkModelOutputContent(output, {
expectedContent: '15',
testName: 'MCP server test',
});
// Validate model output - will throw if no output, fail if missing expected content
validateModelOutput(output, '15', 'MCP server test');
expect(
output.includes('15'),
'Expected output to contain the sum (15)',
+2 -11
View File
@@ -5,12 +5,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
TestRig,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe.skip('stdin context', () => {
let rig: TestRig;
@@ -72,11 +67,7 @@ describe.skip('stdin context', () => {
}
// Validate model output
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: randomString,
testName: 'STDIN context test',
});
validateModelOutput(result, randomString, 'STDIN context test');
expect(
result.toLowerCase().includes(randomString),
-3
View File
@@ -20,8 +20,5 @@ export default defineConfig({
maxThreads: 16,
},
},
env: {
GEMINI_TEST_TYPE: 'integration',
},
},
});
+3 -7
View File
@@ -9,8 +9,7 @@ import {
TestRig,
createToolCallErrorMessage,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
validateModelOutput,
} from './test-helper.js';
describe('write_file', () => {
@@ -47,11 +46,8 @@ describe('write_file', () => {
),
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'dad.txt',
testName: 'Write file test',
});
// Validate model output - will throw if no output, warn if missing expected content
validateModelOutput(result, 'dad.txt', 'Write file test');
const newFilePath = 'dad.txt';
+8 -105
View File
@@ -1,19 +1,18 @@
{
"name": "@google/gemini-cli",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"workspaces": [
"packages/*"
],
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.8",
"latest-version": "^9.0.0",
"proper-lockfile": "^4.1.2",
"simple-git": "^3.28.0"
},
"bin": {
@@ -27,11 +26,9 @@
"@types/minimatch": "^5.1.2",
"@types/mock-fs": "^4.13.4",
"@types/prompts": "^2.4.9",
"@types/proper-lockfile": "^4.1.4",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/shell-quote": "^1.7.5",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/eslint-plugin": "^1.3.4",
"cross-env": "^7.0.3",
@@ -76,7 +73,6 @@
"@lydell/node-pty-linux-x64": "1.1.0",
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0",
"gemini-cli-devtools": "^0.2.1",
"keytar": "^7.9.0",
"node-pty": "^1.0.0"
}
@@ -2255,7 +2251,6 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2436,7 +2431,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2470,7 +2464,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2839,7 +2832,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2873,7 +2865,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/resources": "2.0.1"
@@ -2926,7 +2917,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/resources": "2.0.1",
@@ -4112,16 +4102,6 @@
"kleur": "^3.0.3"
}
},
"node_modules/@types/proper-lockfile": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz",
"integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/retry": "*"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -4142,7 +4122,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4217,13 +4196,6 @@
"node": ">= 0.6"
}
},
"node_modules/@types/retry": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz",
"integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/sarif": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz",
@@ -4354,16 +4326,6 @@
"boxen": "^7.1.1"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -4437,7 +4399,6 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -5430,7 +5391,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8440,7 +8400,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8981,7 +8940,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -9617,18 +9575,6 @@
"node": ">=14"
}
},
"node_modules/gemini-cli-devtools": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/gemini-cli-devtools/-/gemini-cli-devtools-0.2.1.tgz",
"integrity": "sha512-PcqPL9ZZjgjsp3oYhcXnUc6yNeLvdZuU/UQp0aT+DA8pt3BZzPzXthlOmIrRRqHBdLjMLPwN5GD29zR5bASXtQ==",
"optional": true,
"dependencies": {
"ws": "^8.16.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/gemini-cli-vscode-ide-companion": {
"resolved": "packages/vscode-ide-companion",
"link": true
@@ -10595,7 +10541,6 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -14095,32 +14040,6 @@
"react-is": "^16.13.1"
}
},
"node_modules/proper-lockfile": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"retry": "^0.12.0",
"signal-exit": "^3.0.2"
}
},
"node_modules/proper-lockfile/node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/proper-lockfile/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -14380,7 +14299,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14391,7 +14309,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -16628,7 +16545,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16852,8 +16768,7 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -16861,7 +16776,6 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -17034,7 +16948,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17242,7 +17155,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -17356,7 +17268,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -17369,7 +17280,6 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -18074,7 +17984,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -18090,14 +17999,13 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"google-auth-library": "^9.11.0",
"tar": "^7.5.2",
"uuid": "^13.0.0",
"winston": "^3.17.0"
@@ -18147,7 +18055,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -18175,7 +18083,6 @@
"mnemonist": "^0.40.3",
"open": "^10.1.2",
"prompts": "^2.4.2",
"proper-lockfile": "^4.1.2",
"react": "^19.2.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
@@ -18187,7 +18094,6 @@
"tinygradient": "^1.1.5",
"undici": "^7.10.0",
"wrap-ansi": "9.0.2",
"ws": "^8.16.0",
"yargs": "^17.7.2",
"zod": "^3.23.8"
},
@@ -18206,7 +18112,6 @@
"@types/semver": "^7.7.0",
"@types/shell-quote": "^1.7.5",
"@types/tar": "^6.1.13",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"archiver": "^7.0.1",
"ink-testing-library": "^4.0.0",
@@ -18237,7 +18142,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
@@ -18279,7 +18184,6 @@
"mnemonist": "^0.40.3",
"open": "^10.1.2",
"picomatch": "^4.0.1",
"proper-lockfile": "^4.1.2",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
@@ -18374,7 +18278,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -18397,7 +18300,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18414,7 +18317,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+3 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.0-nightly.20260203.71f46f116"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -32,7 +32,7 @@
"docs:settings": "tsx ./scripts/generate-settings-doc.ts",
"docs:keybindings": "tsx ./scripts/generate-keybindings-doc.ts",
"build": "node scripts/build.js",
"build-and-start": "npm run build && npm run start --",
"build-and-start": "npm run build && npm run start",
"build:vscode": "node scripts/build_vscode_companion.js",
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
@@ -86,11 +86,9 @@
"@types/minimatch": "^5.1.2",
"@types/mock-fs": "^4.13.4",
"@types/prompts": "^2.4.9",
"@types/proper-lockfile": "^4.1.4",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/shell-quote": "^1.7.5",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/eslint-plugin": "^1.3.4",
"cross-env": "^7.0.3",
@@ -128,7 +126,6 @@
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.8",
"latest-version": "^9.0.0",
"proper-lockfile": "^4.1.2",
"simple-git": "^3.28.0"
},
"optionalDependencies": {
@@ -138,7 +135,6 @@
"@lydell/node-pty-linux-x64": "1.1.0",
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0",
"gemini-cli-devtools": "^0.2.1",
"keytar": "^7.9.0",
"node-pty": "^1.0.0"
},
-31
View File
@@ -1,31 +0,0 @@
# Pre-built production image for a2a-server
# Used with Cloud Build: npm install + build runs in step 1, then Docker copies artifacts
FROM docker.io/library/node:20-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 curl git jq ripgrep ca-certificates \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy everything including pre-installed node_modules and pre-built dist
COPY package.json package-lock.json ./
COPY node_modules/ node_modules/
COPY packages/core/ packages/core/
COPY packages/a2a-server/ packages/a2a-server/
# Create workspace directory for agent operations
RUN mkdir -p /workspace && chown -R node:node /workspace
USER node
ENV CODER_AGENT_WORKSPACE_PATH=/workspace
ENV CODER_AGENT_PORT=8080
ENV NODE_ENV=production
# Prevent git from prompting for credentials interactively — fails fast instead of hanging
ENV GIT_TERMINAL_PROMPT=0
ENV CODER_AGENT_HOST=0.0.0.0
EXPOSE 8080
CMD ["node", "packages/a2a-server/dist/src/http/server.js"]
-35
View File
@@ -1,35 +0,0 @@
steps:
# Step 1: Install all dependencies and build
- name: 'node:20-slim'
entrypoint: 'bash'
args:
- '-c'
- |
apt-get update && apt-get install -y python3 make g++ git
npm pkg delete scripts.prepare
npm install
npm run build
env:
- 'HUSKY=0'
# Step 2: Build Docker image (using pre-built dist/ from step 1)
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/a2a-server:latest'
- '-f'
- 'packages/a2a-server/Dockerfile'
- '.'
# Step 3: Push to Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/a2a-server:latest'
images:
- 'us-central1-docker.pkg.dev/$PROJECT_ID/gemini-a2a/a2a-server:latest'
timeout: '1800s'
options:
machineType: 'E2_HIGHCPU_8'
-74
View File
@@ -1,74 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gemini-a2a-server
labels:
app: gemini-a2a-server
spec:
replicas: 1
selector:
matchLabels:
app: gemini-a2a-server
template:
metadata:
labels:
app: gemini-a2a-server
spec:
containers:
- name: a2a-server
image: us-central1-docker.pkg.dev/adamfweidman-test/gemini-a2a/a2a-server:latest
ports:
- containerPort: 8080
protocol: TCP
env:
- name: CODER_AGENT_PORT
value: "8080"
- name: CODER_AGENT_HOST
value: "0.0.0.0"
- name: CODER_AGENT_WORKSPACE_PATH
value: "/workspace"
- name: GEMINI_API_KEY
valueFrom:
secretKeyRef:
name: gemini-secrets
key: api-key
- name: GEMINI_YOLO_MODE
value: "true"
- name: CHAT_BRIDGE_A2A_URL
value: "http://localhost:8080"
- name: NODE_ENV
value: "production"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
readinessProbe:
httpGet:
path: /.well-known/agent-card.json
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /.well-known/agent-card.json
port: 8080
initialDelaySeconds: 15
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: gemini-a2a-server
labels:
app: gemini-a2a-server
spec:
type: ClusterIP
selector:
app: gemini-a2a-server
ports:
- port: 80
targetPort: 8080
protocol: TCP
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -29,7 +29,6 @@
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"google-auth-library": "^9.11.0",
"fs-extra": "^11.3.0",
"tar": "^7.5.2",
"uuid": "^13.0.0",
@@ -1,157 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Builder functions for A2UI standard catalog components.
* These create the component objects that go into updateComponents messages.
*/
import type { A2UIComponent } from './a2ui-extension.js';
// Layout components
export function column(
id: string,
children: string[],
opts?: { align?: string; justify?: string; weight?: number },
): A2UIComponent {
return {
id,
component: 'Column',
children,
...opts,
};
}
export function row(
id: string,
children: string[],
opts?: { align?: string; justify?: string },
): A2UIComponent {
return {
id,
component: 'Row',
children,
...opts,
};
}
export function card(
id: string,
child: string,
opts?: Record<string, unknown>,
): A2UIComponent {
return {
id,
component: 'Card',
child,
...opts,
};
}
// Content components
export function text(
id: string,
textContent: string | { path: string },
opts?: { variant?: string },
): A2UIComponent {
return {
id,
component: 'Text',
text: textContent,
...opts,
};
}
export function icon(id: string, name: string): A2UIComponent {
return {
id,
component: 'Icon',
name,
};
}
export function divider(
id: string,
axis: 'horizontal' | 'vertical' = 'horizontal',
): A2UIComponent {
return {
id,
component: 'Divider',
axis,
};
}
// Interactive components
export function button(
id: string,
child: string,
action: {
event?: { name: string; context: Record<string, unknown> };
functionCall?: { call: string; args: Record<string, unknown> };
},
opts?: { variant?: 'primary' | 'borderless' },
): A2UIComponent {
return {
id,
component: 'Button',
child,
action,
...opts,
};
}
export function textField(
id: string,
label: string,
valuePath: string,
opts?: {
variant?: 'shortText' | 'longText';
checks?: Array<{
call: string;
args: Record<string, unknown>;
message: string;
}>;
},
): A2UIComponent {
return {
id,
component: 'TextField',
label,
value: { path: valuePath },
...opts,
};
}
export function checkBox(
id: string,
label: string,
valuePath: string,
): A2UIComponent {
return {
id,
component: 'CheckBox',
label,
value: { path: valuePath },
};
}
export function choicePicker(
id: string,
options: Array<{ label: string; value: string }>,
valuePath: string,
opts?: { variant?: 'mutuallyExclusive' | 'multiSelect' },
): A2UIComponent {
return {
id,
component: 'ChoicePicker',
options,
value: { path: valuePath },
...opts,
};
}
@@ -1,194 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A2UI (Agent-to-UI) Extension for A2A protocol.
* Implements the A2UI v0.10 specification for generating declarative UI
* messages that clients can render natively.
*
* @see https://a2ui.org/specification/v0_10/docs/a2ui_protocol.md
* @see https://a2ui.org/specification/v0_10/docs/a2ui_extension_specification.md
*/
import type { Part } from '@a2a-js/sdk';
// Extension constants
export const A2UI_EXTENSION_URI = 'https://a2ui.org/a2a-extension/a2ui/v0.10';
export const A2UI_MIME_TYPE = 'application/json+a2ui';
export const A2UI_VERSION = 'v0.10';
export const STANDARD_CATALOG_ID =
'https://a2ui.org/specification/v0_10/standard_catalog.json';
// Metadata keys
export const MIME_TYPE_KEY = 'mimeType';
export const A2UI_CLIENT_CAPABILITIES_KEY = 'a2uiClientCapabilities';
export const A2UI_CLIENT_DATA_MODEL_KEY = 'a2uiClientDataModel';
/**
* A2UI message types (server-to-client).
*/
export interface CreateSurfaceMessage {
version: typeof A2UI_VERSION;
createSurface: {
surfaceId: string;
catalogId: string;
theme?: Record<string, unknown>;
sendDataModel?: boolean;
};
}
export interface UpdateComponentsMessage {
version: typeof A2UI_VERSION;
updateComponents: {
surfaceId: string;
components: A2UIComponent[];
};
}
export interface UpdateDataModelMessage {
version: typeof A2UI_VERSION;
updateDataModel: {
surfaceId: string;
path?: string;
value?: unknown;
};
}
export interface DeleteSurfaceMessage {
version: typeof A2UI_VERSION;
deleteSurface: {
surfaceId: string;
};
}
export type A2UIServerMessage =
| CreateSurfaceMessage
| UpdateComponentsMessage
| UpdateDataModelMessage
| DeleteSurfaceMessage;
/**
* A2UI component definition.
*/
export interface A2UIComponent {
id: string;
component: string;
[key: string]: unknown;
}
/**
* A2UI client-to-server action message.
*/
export interface A2UIActionMessage {
version: typeof A2UI_VERSION;
action: {
name: string;
surfaceId: string;
sourceComponentId: string;
timestamp: string;
context: Record<string, unknown>;
};
}
/**
* A2UI client capabilities sent in metadata.
*/
export interface A2UIClientCapabilities {
supportedCatalogIds: string[];
inlineCatalogs?: unknown[];
}
/**
* Creates an A2A DataPart containing A2UI messages.
* Per the spec, the data field contains an ARRAY of A2UI messages.
*/
export function createA2UIPart(messages: A2UIServerMessage[]): Part {
return {
kind: 'data',
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
data: messages as unknown as Record<string, unknown>,
metadata: {
[MIME_TYPE_KEY]: A2UI_MIME_TYPE,
},
} as Part;
}
/**
* Creates a single A2A DataPart from one A2UI message.
*/
export function createA2UISinglePart(message: A2UIServerMessage): Part {
return createA2UIPart([message]);
}
/**
* Checks if an A2A Part contains A2UI data.
*/
export function isA2UIPart(part: Part): boolean {
return (
part.kind === 'data' &&
part.metadata != null &&
part.metadata[MIME_TYPE_KEY] === A2UI_MIME_TYPE
);
}
/**
* Extracts A2UI action messages from an A2A Part.
*/
export function extractA2UIActions(part: Part): A2UIActionMessage[] {
if (!isA2UIPart(part)) return [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const data = (part as unknown as { data?: unknown[] }).data;
if (!Array.isArray(data)) return [];
return data.filter(
(msg): msg is A2UIActionMessage =>
typeof msg === 'object' &&
msg !== null &&
'action' in msg &&
'version' in msg,
);
}
/**
* Creates the A2UI AgentExtension configuration for the AgentCard.
*/
export function getA2UIAgentExtension(
supportedCatalogIds: string[] = [STANDARD_CATALOG_ID],
acceptsInlineCatalogs = false,
): {
uri: string;
description: string;
required: boolean;
params: Record<string, unknown>;
} {
const params: Record<string, unknown> = {};
if (supportedCatalogIds.length > 0) {
params['supportedCatalogIds'] = supportedCatalogIds;
}
if (acceptsInlineCatalogs) {
params['acceptsInlineCatalogs'] = true;
}
return {
uri: A2UI_EXTENSION_URI,
description: 'Provides agent driven UI using the A2UI JSON format.',
required: false,
params,
};
}
/**
* Checks if the A2UI extension was requested via extension headers or message.
*/
export function isA2UIRequested(
requestedExtensions?: string[],
messageExtensions?: string[],
): boolean {
return (
(requestedExtensions?.includes(A2UI_EXTENSION_URI) ?? false) ||
(messageExtensions?.includes(A2UI_EXTENSION_URI) ?? false)
);
}
@@ -1,468 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Manages A2UI surfaces for the Gemini CLI A2A server.
* Creates and updates surfaces for:
* - Tool call approval UIs
* - Agent text/thought streaming displays
* - Task status indicators
*/
import type { Part } from '@a2a-js/sdk';
import { logger } from '../utils/logger.js';
import {
A2UI_VERSION,
STANDARD_CATALOG_ID,
createA2UIPart,
type A2UIServerMessage,
type A2UIComponent,
} from './a2ui-extension.js';
import {
column,
row,
text,
button,
card,
icon,
divider,
} from './a2ui-components.js';
/**
* Generates A2UI parts for tool call approval surfaces.
*/
export function createToolCallApprovalSurface(
taskId: string,
toolCall: {
callId: string;
name: string;
displayName?: string;
description?: string;
args?: Record<string, unknown>;
kind?: string;
},
): Part {
const surfaceId = `tool_approval_${taskId}_${toolCall.callId}`;
const toolDisplayName = toolCall.displayName || toolCall.name;
const argsPreview = toolCall.args
? JSON.stringify(toolCall.args, null, 2).substring(0, 500)
: 'No arguments';
logger.info(
`[A2UI] Creating tool approval surface: ${surfaceId} for tool: ${toolDisplayName}`,
);
const messages: A2UIServerMessage[] = [
// 1. Create the surface
{
version: A2UI_VERSION,
createSurface: {
surfaceId,
catalogId: STANDARD_CATALOG_ID,
theme: {
primaryColor: '#1a73e8',
agentDisplayName: 'Gemini CLI Agent',
},
sendDataModel: true,
},
},
// 2. Define the components
{
version: A2UI_VERSION,
updateComponents: {
surfaceId,
components: buildToolApprovalComponents(
taskId,
toolCall.callId,
toolDisplayName,
toolCall.description || '',
argsPreview,
toolCall.kind || 'tool',
),
},
},
// 3. Populate the data model
{
version: A2UI_VERSION,
updateDataModel: {
surfaceId,
value: {
tool: {
callId: toolCall.callId,
name: toolCall.name,
displayName: toolDisplayName,
description: toolCall.description || '',
args: argsPreview,
kind: toolCall.kind || 'tool',
status: 'awaiting_approval',
},
taskId,
},
},
},
];
return createA2UIPart(messages);
}
function buildToolApprovalComponents(
taskId: string,
callId: string,
toolName: string,
description: string,
argsPreview: string,
kind: string,
): A2UIComponent[] {
return [
// Root card
card('root', 'main_column'),
// Main vertical layout
column(
'main_column',
[
'header_row',
'description_text',
'divider_1',
'args_label',
'args_text',
'divider_2',
'action_row',
],
{ align: 'stretch' },
),
// Header with icon and tool name
row('header_row', ['tool_icon', 'tool_name_text'], {
align: 'center',
}),
icon('tool_icon', kind === 'shell' ? 'terminal' : 'build'),
text('tool_name_text', `**${toolName}** requires approval`, {
variant: 'h3',
}),
// Description
text(
'description_text',
description || 'This tool needs your permission to execute.',
),
divider('divider_1'),
// Arguments preview
text('args_label', '**Arguments:**', { variant: 'caption' }),
text('args_text', `\`\`\`\n${argsPreview}\n\`\`\``),
divider('divider_2'),
// Action buttons row
row(
'action_row',
['approve_button', 'approve_always_button', 'reject_button'],
{ justify: 'spaceBetween' },
),
// Approve button
text('approve_label', 'Approve'),
button(
'approve_button',
'approve_label',
{
event: {
name: 'tool_confirmation',
context: {
taskId,
callId,
outcome: 'proceed_once',
},
},
},
{ variant: 'primary' },
),
// Approve always button
text('approve_always_label', 'Always Allow'),
button('approve_always_button', 'approve_always_label', {
event: {
name: 'tool_confirmation',
context: {
taskId,
callId,
outcome: 'proceed_always_tool',
},
},
}),
// Reject button
text('reject_label', 'Reject'),
button('reject_button', 'reject_label', {
event: {
name: 'tool_confirmation',
context: {
taskId,
callId,
outcome: 'cancel',
},
},
}),
];
}
/**
* Creates an A2UI surface update for tool execution status.
*/
export function updateToolCallStatus(
taskId: string,
callId: string,
status: string,
output?: string,
): Part {
const surfaceId = `tool_approval_${taskId}_${callId}`;
logger.info(
`[A2UI] Updating tool status surface: ${surfaceId} status: ${status}`,
);
const messages: A2UIServerMessage[] = [
{
version: A2UI_VERSION,
updateDataModel: {
surfaceId,
path: '/tool/status',
value: status,
},
},
];
// If tool completed, update the UI to show result
if (['success', 'error', 'cancelled'].includes(status)) {
messages.push({
version: A2UI_VERSION,
updateComponents: {
surfaceId,
components: [
// Replace action row with status indicator
row('action_row', ['status_icon', 'status_text'], {
align: 'center',
}),
icon(
'status_icon',
status === 'success'
? 'check_circle'
: status === 'error'
? 'error'
: 'cancel',
),
text(
'status_text',
status === 'success'
? 'Tool executed successfully'
: status === 'error'
? 'Tool execution failed'
: 'Tool execution cancelled',
),
],
},
});
if (output) {
messages.push({
version: A2UI_VERSION,
updateDataModel: {
surfaceId,
path: '/tool/output',
value: output,
},
});
}
}
return createA2UIPart(messages);
}
/**
* Creates an A2UI text content surface for agent messages.
*/
export function createTextContentPart(
taskId: string,
content: string,
surfaceId?: string,
): Part {
const sid = surfaceId || `agent_text_${taskId}`;
const messages: A2UIServerMessage[] = [
{
version: A2UI_VERSION,
updateDataModel: {
surfaceId: sid,
path: '/content/text',
value: content,
},
},
];
return createA2UIPart(messages);
}
/**
* Creates the initial agent response surface.
*/
export function createAgentResponseSurface(taskId: string): Part {
const surfaceId = `agent_response_${taskId}`;
logger.info(`[A2UI] Creating agent response surface: ${surfaceId}`);
const messages: A2UIServerMessage[] = [
{
version: A2UI_VERSION,
createSurface: {
surfaceId,
catalogId: STANDARD_CATALOG_ID,
theme: {
primaryColor: '#1a73e8',
agentDisplayName: 'Gemini CLI Agent',
},
},
},
{
version: A2UI_VERSION,
updateComponents: {
surfaceId,
components: [
card('root', 'response_column'),
column('response_column', ['response_text', 'status_text'], {
align: 'stretch',
}),
text('response_text', { path: '/response/text' }),
text(
'status_text',
{ path: '/response/status' },
{
variant: 'caption',
},
),
],
},
},
{
version: A2UI_VERSION,
updateDataModel: {
surfaceId,
value: {
response: {
text: '',
status: 'Working...',
},
},
},
},
];
return createA2UIPart(messages);
}
/**
* Updates the agent response surface with new text content.
*/
export function updateAgentResponseText(
taskId: string,
content: string,
status?: string,
): Part {
const surfaceId = `agent_response_${taskId}`;
const messages: A2UIServerMessage[] = [
{
version: A2UI_VERSION,
updateDataModel: {
surfaceId,
path: '/response/text',
value: content,
},
},
];
if (status) {
messages.push({
version: A2UI_VERSION,
updateDataModel: {
surfaceId,
path: '/response/status',
value: status,
},
});
}
return createA2UIPart(messages);
}
/**
* Creates an A2UI thought surface.
*/
export function createThoughtPart(
taskId: string,
subject: string,
description: string,
): Part {
const surfaceId = `thought_${taskId}_${Date.now()}`;
const messages: A2UIServerMessage[] = [
{
version: A2UI_VERSION,
createSurface: {
surfaceId,
catalogId: STANDARD_CATALOG_ID,
theme: {
primaryColor: '#7c4dff',
agentDisplayName: 'Gemini CLI Agent',
},
},
},
{
version: A2UI_VERSION,
updateComponents: {
surfaceId,
components: [
card('root', 'thought_column'),
column('thought_column', ['thought_icon_row', 'thought_desc'], {
align: 'stretch',
}),
row('thought_icon_row', ['thought_icon', 'thought_subject'], {
align: 'center',
}),
icon('thought_icon', 'psychology'),
text('thought_subject', `*${subject}*`, { variant: 'h4' }),
text('thought_desc', description),
],
},
},
];
return createA2UIPart(messages);
}
/**
* Deletes a tool approval surface after resolution.
*/
export function deleteToolApprovalSurface(
taskId: string,
callId: string,
): Part {
const surfaceId = `tool_approval_${taskId}_${callId}`;
logger.info(`[A2UI] Deleting tool approval surface: ${surfaceId}`);
const messages: A2UIServerMessage[] = [
{
version: A2UI_VERSION,
deleteSurface: {
surfaceId,
},
},
];
return createA2UIPart(messages);
}
+1 -64
View File
@@ -36,10 +36,6 @@ import { loadExtensions } from '../config/extension.js';
import { Task } from './task.js';
import { requestStorage } from '../http/requestStorage.js';
import { pushTaskStateFailed } from '../utils/executor_utils.js';
import {
A2UI_CLIENT_CAPABILITIES_KEY,
A2UI_EXTENSION_URI,
} from '../a2ui/a2ui-extension.js';
/**
* Provides a wrapper for Task. Passes data from Task to SDKTask.
@@ -77,24 +73,6 @@ class TaskWrapper {
artifacts: [],
};
sdkTask.metadata!['_contextId'] = this.task.contextId;
// Persist conversation history for session resumability.
// GCSTaskStore saves this as a separate object and restores it on load.
try {
const conversationHistory = this.task.geminiClient.getHistory();
if (conversationHistory.length > 0) {
sdkTask.metadata!['_conversationHistory'] = conversationHistory;
logger.info(
`Task ${this.task.id}: Persisting ${conversationHistory.length} conversation history entries.`,
);
}
} catch {
// GeminiClient may not be initialized yet
logger.warn(
`Task ${this.task.id}: Could not get conversation history for persistence.`,
);
}
return sdkTask;
}
}
@@ -139,7 +117,6 @@ export class CoderAgentExecutor implements AgentExecutor {
const agentSettings = persistedState._agentSettings;
const config = await this.getConfig(agentSettings, sdkTask.id);
const contextId: string =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(metadata['_contextId'] as string) || sdkTask.contextId;
const runtimeTask = await Task.create(
sdkTask.id,
@@ -149,25 +126,7 @@ export class CoderAgentExecutor implements AgentExecutor {
agentSettings.autoExecute,
);
runtimeTask.taskState = persistedState._taskState;
// Restore conversation history if available from the TaskStore.
// This enables session resumability — the LLM gets full context of
// prior interactions rather than starting with a blank slate.
const conversationHistory = metadata['_conversationHistory'];
if (Array.isArray(conversationHistory) && conversationHistory.length > 0) {
logger.info(
`Task ${sdkTask.id}: Resuming with ${conversationHistory.length} conversation history entries.`,
);
// History was serialized from GeminiClient.getHistory() which returns
// Content[]. After JSON round-trip it's structurally identical.
await runtimeTask.geminiClient.initialize();
runtimeTask.geminiClient.setHistory(
conversationHistory,
);
} else {
await runtimeTask.geminiClient.initialize();
}
await runtimeTask.geminiClient.initialize();
const wrapper = new TaskWrapper(runtimeTask, agentSettings);
this.tasks.set(sdkTask.id, wrapper);
@@ -181,7 +140,6 @@ export class CoderAgentExecutor implements AgentExecutor {
agentSettingsInput?: AgentSettings,
eventBus?: ExecutionEventBus,
): Promise<TaskWrapper> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const agentSettings = agentSettingsInput || ({} as AgentSettings);
const config = await this.getConfig(agentSettings, taskId);
const runtimeTask = await Task.create(
@@ -332,7 +290,6 @@ export class CoderAgentExecutor implements AgentExecutor {
const contextId: string =
userMessage.contextId ||
sdkTask?.contextId ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(sdkTask?.metadata?.['_contextId'] as string) ||
uuidv4();
@@ -428,7 +385,6 @@ export class CoderAgentExecutor implements AgentExecutor {
}
} else {
logger.info(`[CoderAgentExecutor] Creating new task ${taskId}.`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const agentSettings = userMessage.metadata?.[
'coderAgent'
] as AgentSettings;
@@ -475,22 +431,6 @@ export class CoderAgentExecutor implements AgentExecutor {
const currentTask = wrapper.task;
// Detect A2UI extension activation from the request
// Check if user message metadata contains A2UI client capabilities
// or if the extensions header includes the A2UI URI
const messageMetadata = userMessage.metadata;
const hasA2UICapabilities =
messageMetadata?.[A2UI_CLIENT_CAPABILITIES_KEY] != null;
// Also check if extension URI is referenced in message extensions
const messageExtensions = messageMetadata?.['extensions'];
const hasA2UIExtension =
Array.isArray(messageExtensions) &&
messageExtensions.includes(A2UI_EXTENSION_URI);
if (hasA2UICapabilities || hasA2UIExtension) {
currentTask.a2uiEnabled = true;
logger.info(`[CoderAgentExecutor] A2UI enabled for task ${taskId}`);
}
if (['canceled', 'failed', 'completed'].includes(currentTask.taskState)) {
logger.warn(
`[CoderAgentExecutor] Attempted to execute task ${taskId} which is already in state ${currentTask.taskState}. Ignoring.`,
@@ -608,9 +548,6 @@ export class CoderAgentExecutor implements AgentExecutor {
logger.info(
`[CoderAgentExecutor] Task ${taskId}: Agent turn finished, setting to input-required.`,
);
// Finalize A2UI surfaces before marking complete
currentTask.finalizeA2UISurfaces();
const stateChange: StateChange = {
kind: CoderAgentEvent.StateChangeEvent,
};
+7 -211
View File
@@ -56,15 +56,6 @@ import type {
Citation,
} from '../types.js';
import type { PartUnion, Part as genAiPart } from '@google/genai';
import {
createToolCallApprovalSurface,
updateToolCallStatus,
createAgentResponseSurface,
updateAgentResponseText,
createThoughtPart as createA2UIThoughtPart,
deleteToolApprovalSurface,
} from '../a2ui/a2ui-surface-manager.js';
import { isA2UIPart, extractA2UIActions } from '../a2ui/a2ui-extension.js';
type UnionKeys<T> = T extends T ? keyof T : never;
@@ -84,11 +75,6 @@ export class Task {
promptCount = 0;
autoExecute: boolean;
// A2UI support
a2uiEnabled = false;
private accumulatedText = '';
private a2uiResponseSurfaceCreated = false;
// For tool waiting logic
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
private toolCompletionPromise?: Promise<void>;
@@ -392,7 +378,6 @@ export class Task {
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
this.pendingToolConfirmationDetails.set(
tc.request.callId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
tc.confirmationDetails as ToolCallConfirmationDetails,
);
}
@@ -405,44 +390,6 @@ export class Task {
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
const message = this.toolStatusMessage(tc, this.id, this.contextId);
// Add A2UI parts for tool call updates if A2UI is enabled
if (this.a2uiEnabled) {
try {
if (tc.status === 'awaiting_approval') {
const a2uiPart = createToolCallApprovalSurface(this.id, {
callId: tc.request.callId,
name: tc.request.name,
displayName: tc.tool?.displayName || tc.tool?.name,
description: tc.tool?.description,
args: tc.request.args as Record<string, unknown> | undefined,
kind: tc.tool?.kind,
});
message.parts.push(a2uiPart);
logger.info(
`[Task] A2UI: Added tool approval surface for ${tc.request.callId}`,
);
} else if (['success', 'error', 'cancelled'].includes(tc.status)) {
const output =
'liveOutput' in tc ? String(tc.liveOutput) : undefined;
const a2uiPart = updateToolCallStatus(
this.id,
tc.request.callId,
tc.status,
output,
);
message.parts.push(a2uiPart);
logger.info(
`[Task] A2UI: Updated tool status for ${tc.request.callId}: ${tc.status}`,
);
}
} catch (a2uiError) {
logger.error(
'[Task] A2UI: Error generating tool call surface:',
a2uiError,
);
}
}
const event = this._createStatusUpdateEvent(
this.taskState,
coderAgentMessage,
@@ -464,7 +411,7 @@ export class Task {
);
toolCalls.forEach((tc: ToolCall) => {
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-unsafe-type-assertion
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(tc.confirmationDetails as ToolCallConfirmationDetails).onConfirm(
ToolConfirmationOutcome.ProceedOnce,
);
@@ -518,14 +465,12 @@ export class Task {
T extends ToolCall | AnyDeclarativeTool,
K extends UnionKeys<T>,
>(from: T, ...fields: K[]): Partial<T> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const ret = {} as Pick<T, K>;
for (const field of fields) {
if (field in from) {
ret[field] = from[field];
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return ret as Partial<T>;
}
@@ -548,7 +493,6 @@ export class Task {
);
if (tc.tool) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
serializableToolCall.tool = this._pickFields(
tc.tool,
'name',
@@ -678,11 +622,8 @@ export class Task {
request.args['new_string']
) {
const newContent = await this.getProposedContent(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
request.args['file_path'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
request.args['old_string'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
request.args['new_string'] as string,
);
return { ...request, args: { ...request.args, newContent } };
@@ -778,7 +719,6 @@ export class Task {
case GeminiEventType.Error:
default: {
// Block scope for lexical declaration
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const errorEvent = event as ServerGeminiErrorEvent; // Type assertion
const errorMessage =
errorEvent.value?.error.message ?? 'Unknown error from LLM stream';
@@ -867,7 +807,6 @@ export class Task {
if (confirmationDetails.type === 'edit') {
const payload = part.data['newContent']
? ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
newContent: part.data['newContent'] as string,
} as ToolConfirmationPayload)
: undefined;
@@ -1006,66 +945,7 @@ export class Task {
let anyConfirmationHandled = false;
let hasContentForLlm = false;
// Reset A2UI accumulated text for new user turn
if (this.a2uiEnabled) {
this.accumulatedText = '';
this.a2uiResponseSurfaceCreated = false;
}
for (const part of userMessage.parts) {
// Handle A2UI action messages (e.g., button clicks for tool approval)
if (this.a2uiEnabled && isA2UIPart(part)) {
const actions = extractA2UIActions(part);
for (const action of actions) {
if (action.action.name === 'tool_confirmation') {
const ctx = action.action.context;
// Convert A2UI action to a tool confirmation data part
const syntheticPart: Part = {
kind: 'data',
data: {
callId: ctx['callId'],
outcome: ctx['outcome'],
},
} as Part;
const handled =
await this._handleToolConfirmationPart(syntheticPart);
if (handled) {
anyConfirmationHandled = true;
// Emit a delete surface part for the approval UI
try {
const deletePart = deleteToolApprovalSurface(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(ctx['taskId'] as string) || this.id,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
ctx['callId'] as string,
);
const deleteMessage: Message = {
kind: 'message',
role: 'agent',
parts: [deletePart],
messageId: uuidv4(),
taskId: this.id,
contextId: this.contextId,
};
const event = this._createStatusUpdateEvent(
this.taskState,
{ kind: CoderAgentEvent.ToolCallUpdateEvent },
deleteMessage,
false,
);
this.eventBus?.publish(event);
} catch (a2uiError) {
logger.error(
'[Task] A2UI: Error deleting approval surface:',
a2uiError,
);
}
}
}
}
continue;
}
const confirmationHandled = await this._handleToolConfirmationPart(part);
if (confirmationHandled) {
anyConfirmationHandled = true;
@@ -1131,33 +1011,6 @@ export class Task {
}
logger.info('[Task] Sending text content to event bus.');
const message = this._createTextMessage(content);
// Add A2UI response surface parts if A2UI is enabled
if (this.a2uiEnabled) {
try {
this.accumulatedText += content;
if (!this.a2uiResponseSurfaceCreated) {
const surfacePart = createAgentResponseSurface(this.id);
message.parts.push(surfacePart);
this.a2uiResponseSurfaceCreated = true;
logger.info(
`[Task] A2UI: Created agent response surface for task ${this.id}`,
);
}
const updatePart = updateAgentResponseText(
this.id,
this.accumulatedText,
'Working...',
);
message.parts.push(updatePart);
} catch (a2uiError) {
logger.error(
'[Task] A2UI: Error generating text content surface:',
a2uiError,
);
}
}
const textContent: TextContent = {
kind: CoderAgentEvent.TextContentEvent,
};
@@ -1179,35 +1032,15 @@ export class Task {
return;
}
logger.info('[Task] Sending thought to event bus.');
const parts: Part[] = [
{
kind: 'data',
data: content,
} as Part,
];
// Add A2UI thought surface if A2UI is enabled
if (this.a2uiEnabled) {
try {
const a2uiPart = createA2UIThoughtPart(
this.id,
content.subject || 'Thinking...',
content.description || '',
);
parts.push(a2uiPart);
logger.info(`[Task] A2UI: Added thought surface for task ${this.id}`);
} catch (a2uiError) {
logger.error(
'[Task] A2UI: Error generating thought surface:',
a2uiError,
);
}
}
const message: Message = {
kind: 'message',
role: 'agent',
parts,
parts: [
{
kind: 'data',
data: content,
} as Part,
],
messageId: uuidv4(),
taskId: this.id,
contextId: this.contextId,
@@ -1228,43 +1061,6 @@ export class Task {
);
}
/**
* Finalizes A2UI surfaces when the agent turn is complete.
* Updates the response surface status to "Done".
*/
finalizeA2UISurfaces(): void {
if (!this.a2uiEnabled || !this.a2uiResponseSurfaceCreated) {
return;
}
try {
const finalPart = updateAgentResponseText(
this.id,
this.accumulatedText,
'Done',
);
const message: Message = {
kind: 'message',
role: 'agent',
parts: [finalPart],
messageId: uuidv4(),
taskId: this.id,
contextId: this.contextId,
};
const event = this._createStatusUpdateEvent(
this.taskState,
{ kind: CoderAgentEvent.TextContentEvent },
message,
false,
);
this.eventBus?.publish(event);
logger.info(
`[Task] A2UI: Finalized response surface for task ${this.id}`,
);
} catch (a2uiError) {
logger.error('[Task] A2UI: Error finalizing surfaces:', a2uiError);
}
}
_sendCitation(citation: string) {
if (!citation || citation.trim() === '') {
return;
@@ -1,268 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A2A client wrapper for the Google Chat bridge.
* Connects to the A2A server (local or remote) and sends/receives messages.
* Follows the patterns from core/agents/a2a-client-manager.ts and
* core/agents/remote-invocation.ts.
*/
import type { Message, Task, Part, MessageSendParams } from '@a2a-js/sdk';
import {
type Client,
ClientFactory,
ClientFactoryOptions,
DefaultAgentCardResolver,
RestTransportFactory,
JsonRpcTransportFactory,
} from '@a2a-js/sdk/client';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../utils/logger.js';
import { A2UI_EXTENSION_URI, A2UI_MIME_TYPE } from '../a2ui/a2ui-extension.js';
export type A2AResponse = Message | Task;
/**
* Extracts contextId and taskId from an A2A response.
* Follows extractIdsFromResponse pattern from a2aUtils.ts.
*/
export function extractIdsFromResponse(result: A2AResponse): {
contextId?: string;
taskId?: string;
} {
if (result.kind === 'message') {
return {
contextId: result.contextId,
taskId: result.taskId,
};
}
if (result.kind === 'task') {
const contextId = result.contextId;
let taskId: string | undefined = result.id;
// Clear taskId on terminal states so next interaction starts a fresh task
const state = result.status?.state;
if (state === 'completed' || state === 'failed' || state === 'canceled') {
taskId = undefined;
}
return { contextId, taskId };
}
return {};
}
/**
* Extracts all parts from an A2A response.
* For Tasks, checks history (accumulated from intermediate status-update events),
* the final status message, and artifacts. The blocking DefaultRequestHandler
* accumulates intermediate events into task.history, so the A2UI response content
* from "working" events lives there even if the final status message is empty.
*/
export function extractAllParts(result: A2AResponse): Part[] {
const parts: Part[] = [];
if (result.kind === 'message') {
parts.push(...(result.parts ?? []));
} else if (result.kind === 'task') {
// Parts from task history (accumulated intermediate status-update messages)
if (result.history) {
for (const msg of result.history) {
if (msg.parts) {
parts.push(...msg.parts);
}
}
}
// Parts from the final status message
if (result.status?.message?.parts) {
parts.push(...result.status.message.parts);
}
// Parts from artifacts
if (result.artifacts) {
for (const artifact of result.artifacts) {
parts.push(...(artifact.parts ?? []));
}
}
}
return parts;
}
/**
* Extracts plain text content from response parts.
*/
export function extractTextFromParts(parts: Part[]): string {
return parts
.filter((p) => p.kind === 'text')
.map(
(p) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(p as unknown as { text: string }).text,
)
.filter(Boolean)
.join('\n');
}
/**
* Extracts A2UI data parts from response parts.
* A2UI parts are DataParts with metadata.mimeType === 'application/json+a2ui'.
*/
export function extractA2UIParts(parts: Part[]): unknown[][] {
const a2uiMessages: unknown[][] = [];
for (const part of parts) {
if (
part.kind === 'data' &&
part.metadata != null &&
part.metadata['mimeType'] === A2UI_MIME_TYPE
) {
// The data field is an array of A2UI messages
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const data = (part as unknown as { data: unknown }).data;
if (Array.isArray(data)) {
a2uiMessages.push(data);
}
}
}
return a2uiMessages;
}
/**
* A2A client for the chat bridge.
* Manages connection to the A2A server and provides message send/receive.
*/
export class A2ABridgeClient {
private client: Client | null = null;
private agentUrl: string;
constructor(agentUrl: string) {
this.agentUrl = agentUrl;
}
/**
* Initializes the client connection to the A2A server.
*/
async initialize(): Promise<void> {
if (this.client) return;
const resolver = new DefaultAgentCardResolver({});
const options = ClientFactoryOptions.createFrom(
ClientFactoryOptions.default,
{
transports: [
new RestTransportFactory({}),
new JsonRpcTransportFactory({}),
],
cardResolver: resolver,
},
);
const factory = new ClientFactory(options);
// createFromUrl expects the agent card URL, not just the base URL
const agentCardUrl =
this.agentUrl.replace(/\/$/, '') + '/.well-known/agent-card.json';
this.client = await factory.createFromUrl(agentCardUrl, '');
const card = await this.client.getAgentCard();
logger.info(
`[ChatBridge] Connected to A2A agent: ${card.name} (${card.url})`,
);
}
/**
* Sends a text message to the A2A server using blocking mode.
* The blocking DefaultRequestHandler accumulates all intermediate events
* (including A2UI content from "working" status updates) into the Task's
* history array, so extractAllParts can find them.
*/
async sendMessage(
text: string,
options: { contextId?: string; taskId?: string },
): Promise<A2AResponse> {
if (!this.client) {
throw new Error('A2A client not initialized. Call initialize() first.');
}
const params: MessageSendParams = {
message: {
kind: 'message',
role: 'user',
messageId: uuidv4(),
parts: [{ kind: 'text', text }],
contextId: options.contextId,
taskId: options.taskId,
// Signal A2UI support in message metadata
metadata: {
extensions: [A2UI_EXTENSION_URI],
},
},
configuration: {
blocking: true,
},
};
return this.client.sendMessage(params);
}
/**
* Sends a tool confirmation action back to the A2A server.
* The action is sent as a DataPart containing the A2UI action message.
*/
async sendToolConfirmation(
callId: string,
outcome: string,
taskId: string,
options: { contextId?: string },
): Promise<A2AResponse> {
if (!this.client) {
throw new Error('A2A client not initialized. Call initialize() first.');
}
// Build the A2UI action message as a DataPart
const actionPart: Part = {
kind: 'data',
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
data: [
{
version: 'v0.10',
action: {
name: 'tool_confirmation',
surfaceId: `tool_approval_${taskId}_${callId}`,
sourceComponentId:
outcome === 'cancel' ? 'reject_button' : 'approve_button',
timestamp: new Date().toISOString(),
context: { callId, outcome, taskId },
},
},
] as unknown as Record<string, unknown>,
metadata: {
mimeType: A2UI_MIME_TYPE,
},
} as Part;
const params: MessageSendParams = {
message: {
kind: 'message',
role: 'user',
messageId: uuidv4(),
parts: [actionPart],
contextId: options.contextId,
taskId,
metadata: {
extensions: [A2UI_EXTENSION_URI],
},
},
configuration: {
blocking: true,
},
};
return this.client.sendMessage(params);
}
}
@@ -1,329 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Google Chat webhook handler.
* Processes incoming Google Chat events, forwards them to the A2A server,
* and converts responses back to Google Chat format.
*/
import type { ChatEvent, ChatResponse, ChatBridgeConfig } from './types.js';
import { SessionStore } from './session-store.js';
import {
A2ABridgeClient,
extractIdsFromResponse,
} from './a2a-bridge-client.js';
import { renderResponse, extractToolApprovals } from './response-renderer.js';
import { logger } from '../utils/logger.js';
export class ChatBridgeHandler {
private sessionStore: SessionStore;
private a2aClient: A2ABridgeClient;
private initialized = false;
constructor(private config: ChatBridgeConfig) {
this.sessionStore = new SessionStore(config.gcsBucket);
this.a2aClient = new A2ABridgeClient(config.a2aServerUrl);
}
/**
* Initializes the A2A client connection and restores persisted sessions.
* Must be called before handling events.
*/
async initialize(): Promise<void> {
if (this.initialized) return;
await this.a2aClient.initialize();
await this.sessionStore.restore();
this.initialized = true;
logger.info(
`[ChatBridge] Handler initialized, connected to ${this.config.a2aServerUrl}`,
);
}
/**
* Main entry point for handling Google Chat webhook events.
*/
async handleEvent(event: ChatEvent): Promise<ChatResponse> {
if (!this.initialized) {
await this.initialize();
}
logger.info(
`[ChatBridge] Received event: type=${event.type}, space=${event.space.name}`,
);
switch (event.type) {
case 'MESSAGE':
return this.handleMessage(event);
case 'CARD_CLICKED':
return this.handleCardClicked(event);
case 'ADDED_TO_SPACE':
return this.handleAddedToSpace(event);
case 'REMOVED_FROM_SPACE':
return this.handleRemovedFromSpace(event);
default:
logger.warn(`[ChatBridge] Unknown event type: ${event.type}`);
return { text: 'Unknown event type.' };
}
}
/**
* Handles a MESSAGE event: user sent a text message in Chat.
*/
private async handleMessage(event: ChatEvent): Promise<ChatResponse> {
const message = event.message;
if (!message?.thread?.name) {
return { text: 'Error: Missing thread information.' };
}
const text = message.argumentText || message.text || '';
if (!text.trim()) {
return { text: "I didn't receive any text. Please try again." };
}
const threadName = message.thread.name;
const spaceName = event.space.name;
// Handle slash commands
const trimmed = text.trim().toLowerCase();
if (
trimmed === '/reset' ||
trimmed === '/clear' ||
trimmed === 'reset' ||
trimmed === 'clear'
) {
this.sessionStore.remove(threadName);
logger.info(`[ChatBridge] Session cleared for thread ${threadName}`);
return { text: 'Session cleared. Send a new message to start fresh.' };
}
const session = this.sessionStore.getOrCreate(threadName, spaceName);
if (trimmed === '/yolo') {
session.yoloMode = true;
logger.info(`[ChatBridge] YOLO mode enabled for thread ${threadName}`);
return {
text: 'YOLO mode enabled. All tool calls will be auto-approved.',
};
}
if (trimmed === '/safe') {
session.yoloMode = false;
logger.info(`[ChatBridge] YOLO mode disabled for thread ${threadName}`);
return { text: 'Safe mode enabled. Tool calls will require approval.' };
}
logger.info(
`[ChatBridge] MESSAGE from ${event.user.displayName}: "${text.substring(0, 100)}"`,
);
// Handle text-based tool approval responses
const lowerText = trimmed;
if (
session.pendingToolApproval &&
(lowerText === 'approve' ||
lowerText === 'yes' ||
lowerText === 'y' ||
lowerText === 'reject' ||
lowerText === 'no' ||
lowerText === 'n' ||
lowerText === 'always allow')
) {
const approval = session.pendingToolApproval;
const isReject =
lowerText === 'reject' || lowerText === 'no' || lowerText === 'n';
const isAlwaysAllow = lowerText === 'always allow';
const outcome = isReject
? 'cancel'
: isAlwaysAllow
? 'proceed_always_tool'
: 'proceed_once';
logger.info(
`[ChatBridge] Text-based tool ${outcome}: callId=${approval.callId}, taskId=${approval.taskId}`,
);
session.pendingToolApproval = undefined;
try {
const response = await this.a2aClient.sendToolConfirmation(
approval.callId,
outcome,
approval.taskId,
{ contextId: session.contextId },
);
const { contextId: newCtxId, taskId: newTaskId } =
extractIdsFromResponse(response);
if (newCtxId) session.contextId = newCtxId;
this.sessionStore.updateTaskId(threadName, newTaskId);
const threadKey = message.thread.threadKey || threadName;
return renderResponse(response, threadKey, threadName);
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : 'Unknown error';
logger.error(
`[ChatBridge] Error sending tool confirmation: ${errorMsg}`,
error,
);
return { text: `Error processing tool confirmation: ${errorMsg}` };
}
}
try {
const response = await this.a2aClient.sendMessage(text, {
contextId: session.contextId,
taskId: session.taskId,
});
// Update session with new IDs from response
const { contextId, taskId } = extractIdsFromResponse(response);
if (contextId) {
session.contextId = contextId;
}
this.sessionStore.updateTaskId(threadName, taskId);
// Check for pending tool approvals and store for text-based confirmation
const approvals = extractToolApprovals(response);
if (approvals.length > 0) {
const firstApproval = approvals[0];
session.pendingToolApproval = {
callId: firstApproval.callId,
taskId: firstApproval.taskId,
toolName: firstApproval.displayName || firstApproval.name,
};
logger.info(
`[ChatBridge] Pending tool approval: ${firstApproval.displayName || firstApproval.name} callId=${firstApproval.callId}`,
);
} else {
session.pendingToolApproval = undefined;
}
// Convert A2A response to Chat format
const threadKey = message.thread.threadKey || threadName;
return renderResponse(response, threadKey, threadName);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
logger.error(`[ChatBridge] Error handling message: ${errorMsg}`, error);
return {
text: `Sorry, I encountered an error processing your request: ${errorMsg}`,
};
}
}
/**
* Handles a CARD_CLICKED event: user clicked a button on a card.
* Used for tool approval/rejection flows.
*/
private async handleCardClicked(event: ChatEvent): Promise<ChatResponse> {
const action = event.action;
if (!action) {
return { text: 'Error: Missing action data.' };
}
const threadName = event.message?.thread?.name;
if (!threadName) {
return { text: 'Error: Missing thread information.' };
}
const session = this.sessionStore.get(threadName);
if (!session) {
return { text: 'Error: No active session found for this thread.' };
}
logger.info(
`[ChatBridge] CARD_CLICKED: function=${action.actionMethodName}`,
);
if (action.actionMethodName === 'tool_confirmation') {
return this.handleToolConfirmation(event, session.contextId);
}
return { text: `Unknown action: ${action.actionMethodName}` };
}
/**
* Handles tool confirmation actions from card button clicks.
*/
private async handleToolConfirmation(
event: ChatEvent,
contextId: string,
): Promise<ChatResponse> {
const params = event.action?.parameters || [];
const paramMap = new Map(params.map((p) => [p.key, p.value]));
const callId = paramMap.get('callId');
const outcome = paramMap.get('outcome');
const taskId = paramMap.get('taskId');
if (!callId || !outcome || !taskId) {
return { text: 'Error: Missing tool confirmation parameters.' };
}
logger.info(
`[ChatBridge] Tool confirmation: callId=${callId}, outcome=${outcome}, taskId=${taskId}`,
);
try {
const response = await this.a2aClient.sendToolConfirmation(
callId,
outcome,
taskId,
{ contextId },
);
// Update session
const threadName = event.message?.thread?.name;
if (threadName) {
const { contextId: newContextId, taskId: newTaskId } =
extractIdsFromResponse(response);
if (newContextId) {
const session = this.sessionStore.get(threadName);
if (session) session.contextId = newContextId;
}
this.sessionStore.updateTaskId(threadName, newTaskId);
}
return renderResponse(response);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
logger.error(
`[ChatBridge] Error sending tool confirmation: ${errorMsg}`,
error,
);
return {
text: `Error processing tool confirmation: ${errorMsg}`,
};
}
}
/**
* Handles ADDED_TO_SPACE event: bot was added to a space or DM.
*/
private handleAddedToSpace(event: ChatEvent): ChatResponse {
const spaceType = event.space.type === 'DM' ? 'DM' : 'space';
logger.info(`[ChatBridge] Bot added to ${spaceType}: ${event.space.name}`);
return {
text:
`Hello! I'm the Gemini CLI Agent. Send me a message to get started with code generation and development tasks.\n\n` +
`I can:\n` +
`- Generate code from natural language\n` +
`- Edit files and run commands\n` +
`- Answer questions about code\n\n` +
`I'll ask for your approval before executing tools.`,
};
}
/**
* Handles REMOVED_FROM_SPACE event: bot was removed from a space.
*/
private handleRemovedFromSpace(event: ChatEvent): ChatResponse {
logger.info(`[ChatBridge] Bot removed from space: ${event.space.name}`);
// Clean up any sessions for this space
return {};
}
}
@@ -1,413 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Converts A2A/A2UI responses into Google Chat messages and Cards V2.
*
* This renderer understands the A2UI v0.10 surface structures produced by our
* a2a-server (tool approval surfaces, agent response surfaces, thought surfaces)
* and converts them to Google Chat's Cards V2 format.
*
* Inspired by the A2UI web_core message processor pattern but simplified for
* server-side rendering to a constrained card format.
*/
import type {
ChatResponse,
ChatCardV2,
ChatCardSection,
ChatWidget,
} from './types.js';
import {
type A2AResponse,
extractAllParts,
extractTextFromParts,
extractA2UIParts,
} from './a2a-bridge-client.js';
export interface ToolApprovalInfo {
taskId: string;
callId: string;
name: string;
displayName: string;
description: string;
args: string;
kind: string;
status: string;
}
interface AgentResponseInfo {
text: string;
status: string;
}
/**
* Extracts tool approval info from an A2A response.
* Used by the handler to track pending approvals for text-based confirmation.
*/
export function extractToolApprovals(
response: A2AResponse,
): ToolApprovalInfo[] {
const parts = extractAllParts(response);
const a2uiMessageGroups = extractA2UIParts(parts);
const toolApprovals: ToolApprovalInfo[] = [];
const agentResponses: AgentResponseInfo[] = [];
const thoughts: Array<{ subject: string; description: string }> = [];
for (const messages of a2uiMessageGroups) {
parseA2UIMessages(messages, toolApprovals, agentResponses, thoughts);
}
return deduplicateToolApprovals(toolApprovals);
}
/**
* Renders an A2A response as a Google Chat response.
* Extracts text content and A2UI surfaces, converting them to Chat format.
*/
export function renderResponse(
response: A2AResponse,
threadKey?: string,
threadName?: string,
): ChatResponse {
const parts = extractAllParts(response);
const textContent = extractTextFromParts(parts);
const a2uiMessageGroups = extractA2UIParts(parts);
// Parse A2UI surfaces for known types
const toolApprovals: ToolApprovalInfo[] = [];
const agentResponses: AgentResponseInfo[] = [];
const thoughts: Array<{ subject: string; description: string }> = [];
for (const messages of a2uiMessageGroups) {
parseA2UIMessages(messages, toolApprovals, agentResponses, thoughts);
}
// Deduplicate tool approvals by surfaceId — A2UI history contains both
// initial 'awaiting_approval' and later 'success' events for auto-approved tools.
const dedupedApprovals = deduplicateToolApprovals(toolApprovals);
const cards: ChatCardV2[] = [];
// Only render tool approval cards for tools still awaiting approval.
// In YOLO mode, tools are auto-approved and their status becomes "success"
// so we skip rendering approval cards for those.
for (const approval of dedupedApprovals) {
if (approval.status === 'awaiting_approval') {
cards.push(renderToolApprovalCard(approval));
}
}
// Build text response from agent responses and plain text
const responseTexts: string[] = [];
// Add thought summaries
for (const thought of thoughts) {
responseTexts.push(`_${thought.subject}_: ${thought.description}`);
}
// Add agent response text (from A2UI surfaces).
// Use only the last non-empty response since later updates supersede earlier
// ones for the same surface (history contains multiple status-update messages).
for (let i = agentResponses.length - 1; i >= 0; i--) {
if (agentResponses[i].text) {
responseTexts.push(agentResponses[i].text);
break;
}
}
// Fall back to plain text content if no A2UI response text
if (responseTexts.length === 0 && textContent) {
responseTexts.push(textContent);
}
// Add task state info
if (response.kind === 'task' && response.status) {
const state = response.status.state;
if (state === 'input-required' && cards.length > 0) {
responseTexts.push('*Waiting for your approval to continue...*');
} else if (state === 'failed') {
responseTexts.push('*Task failed.*');
} else if (state === 'canceled') {
responseTexts.push('*Task was cancelled.*');
}
}
const chatResponse: ChatResponse = {};
if (responseTexts.length > 0) {
chatResponse.text = responseTexts.join('\n\n');
}
if (cards.length > 0) {
chatResponse.cardsV2 = cards;
}
if (threadKey || threadName) {
chatResponse.thread = {};
if (threadKey) chatResponse.thread.threadKey = threadKey;
if (threadName) chatResponse.thread.name = threadName;
}
// Ensure we always return something
if (!chatResponse.text && !chatResponse.cardsV2) {
chatResponse.text = '_Agent is processing..._';
}
return chatResponse;
}
/**
* Renders a CARD_CLICKED acknowledgment response.
*/
export function renderActionAcknowledgment(
action: string,
outcome: string,
): ChatResponse {
const emoji =
outcome === 'cancel'
? 'Rejected'
: outcome === 'proceed_always_tool'
? 'Always Allowed'
: 'Approved';
return {
actionResponse: { type: 'UPDATE_MESSAGE' },
text: `*Tool ${emoji}* - Processing...`,
};
}
/** Safely extracts a string property from an unknown object. */
function str(obj: Record<string, unknown>, key: string): string {
const v = obj[key];
return typeof v === 'string' ? v : '';
}
/** Safely checks if an unknown value is a record. */
function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
/** Safely extracts a nested object property. */
function obj(
parent: Record<string, unknown>,
key: string,
): Record<string, unknown> | undefined {
const v = parent[key];
return isRecord(v) ? v : undefined;
}
/**
* Deduplicates tool approvals by surfaceId, keeping the last entry per surface.
* In blocking mode, A2UI history accumulates ALL intermediate events a tool
* surface may appear first as 'awaiting_approval' then as 'success' (YOLO mode).
* By keeping only the last entry per surfaceId, auto-approved tools show 'success'.
*/
function deduplicateToolApprovals(
approvals: ToolApprovalInfo[],
): ToolApprovalInfo[] {
const byId = new Map<string, ToolApprovalInfo>();
for (const a of approvals) {
const key = `${a.taskId}_${a.callId}`;
byId.set(key, a);
}
return [...byId.values()];
}
/**
* Parses A2UI v0.10 messages to extract known surface types.
* Our server produces specific surfaces: tool approval, agent response, thought.
*/
function parseA2UIMessages(
messages: unknown[],
toolApprovals: ToolApprovalInfo[],
agentResponses: AgentResponseInfo[],
thoughts: Array<{ subject: string; description: string }>,
): void {
for (const msg of messages) {
if (!isRecord(msg)) continue;
// Look for updateDataModel messages that contain tool approval or response data
const updateDM = obj(msg, 'updateDataModel');
if (updateDM) {
const surfaceId = str(updateDM, 'surfaceId');
const value = obj(updateDM, 'value');
const path = str(updateDM, 'path');
if (value && !path) {
// Full data model update (initial) - check for known structures
const tool = obj(value, 'tool');
if (surfaceId.startsWith('tool_approval_') && tool) {
toolApprovals.push({
taskId: str(value, 'taskId'),
callId: str(tool, 'callId'),
name: str(tool, 'name'),
displayName: str(tool, 'displayName'),
description: str(tool, 'description'),
args: str(tool, 'args'),
kind: str(tool, 'kind') || 'tool',
status: str(tool, 'status') || 'unknown',
});
}
const resp = obj(value, 'response');
if (surfaceId.startsWith('agent_response_') && resp) {
agentResponses.push({
text: str(resp, 'text'),
status: str(resp, 'status'),
});
}
}
// Partial data model updates (path-based)
if (path === '/response/text' && updateDM['value'] != null) {
agentResponses.push({
text: String(updateDM['value']),
status: '',
});
}
// Tool status updates (e.g., YOLO mode changes status to 'success')
if (
surfaceId.startsWith('tool_approval_') &&
path === '/tool/status' &&
typeof updateDM['value'] === 'string'
) {
// Find existing tool approval for this surface and update its status
const existing = toolApprovals.find(
(a) => `tool_approval_${a.taskId}_${a.callId}` === surfaceId,
);
if (existing) {
existing.status = updateDM['value'];
}
}
}
// Look for updateComponents to extract thought text
const updateComp = obj(msg, 'updateComponents');
if (updateComp) {
const surfaceId = str(updateComp, 'surfaceId');
const components = updateComp['components'];
if (surfaceId.startsWith('thought_') && Array.isArray(components)) {
const subject = extractComponentText(components, 'thought_subject');
const desc = extractComponentText(components, 'thought_desc');
if (subject || desc) {
thoughts.push({
subject: subject || 'Thinking',
description: desc || '',
});
}
}
}
}
}
/**
* Extracts the text content from a named component in a component array.
* Components use our a2ui-components.ts builder format.
*/
function extractComponentText(
components: unknown[],
componentId: string,
): string {
for (const comp of components) {
if (!isRecord(comp)) continue;
if (comp['id'] === componentId && comp['component'] === 'text') {
return str(comp, 'text');
}
}
return '';
}
/**
* Extracts a concise command summary from tool approval args.
* For shell tools, returns just the command string.
* For file tools, returns the file path.
*/
function extractCommandSummary(approval: ToolApprovalInfo): string {
if (!approval.args || approval.args === 'No arguments') return '';
try {
const parsed: unknown = JSON.parse(approval.args);
if (isRecord(parsed)) {
// Shell tool: {"command": "ls -F"}
if (typeof parsed['command'] === 'string') {
return parsed['command'];
}
// File tools: {"file_path": "/path/to/file", ...}
if (typeof parsed['file_path'] === 'string') {
const action =
approval.name || approval.displayName || 'File operation';
return `${action}: ${parsed['file_path']}`;
}
}
} catch {
// Not JSON, return as-is if short enough
if (approval.args.length <= 200) return approval.args;
}
return '';
}
/**
* Renders a tool approval surface as a Google Chat Card V2.
*/
function renderToolApprovalCard(approval: ToolApprovalInfo): ChatCardV2 {
const widgets: ChatWidget[] = [];
// Show a concise summary of what the tool will do.
// For shell commands, extract just the command string from the args JSON.
const commandSummary = extractCommandSummary(approval);
if (commandSummary) {
widgets.push({
decoratedText: {
text: `\`${commandSummary}\``,
topLabel: approval.displayName || approval.name,
startIcon: { knownIcon: 'DESCRIPTION' },
wrapText: true,
},
});
} else if (approval.args && approval.args !== 'No arguments') {
// Fallback: show truncated args
const truncatedArgs =
approval.args.length > 300
? approval.args.substring(0, 300) + '...'
: approval.args;
widgets.push({
decoratedText: {
text: truncatedArgs,
topLabel: approval.displayName || approval.name,
startIcon: { knownIcon: 'DESCRIPTION' },
wrapText: true,
},
});
}
// Text-based approval instructions (card click buttons don't work
// with the current Add-ons routing configuration)
widgets.push({
textParagraph: {
text: 'Reply <b>approve</b>, <b>always allow</b>, or <b>reject</b>',
},
});
const sections: ChatCardSection[] = [
{
widgets,
},
];
return {
cardId: `tool_approval_${approval.callId}`,
card: {
header: {
title: 'Tool Approval Required',
subtitle: approval.displayName || approval.name,
},
sections,
},
};
}
@@ -1,362 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Express routes for the Google Chat bridge webhook.
* Adds a POST /chat/webhook endpoint to the existing Express app.
* Includes JWT verification for Google Chat requests when configured.
*/
import type { Router, Request, Response, NextFunction } from 'express';
import { Router as createRouter } from 'express';
import { OAuth2Client } from 'google-auth-library';
import type { ChatEvent, ChatBridgeConfig, ChatResponse } from './types.js';
import { ChatBridgeHandler } from './handler.js';
import { logger } from '../utils/logger.js';
const CHAT_ISSUER = 'chat@system.gserviceaccount.com';
/**
* Creates middleware that verifies Google Chat JWT tokens.
*
* On Cloud Run (detected via K_SERVICE env var), authentication is handled by
* Cloud Run's IAM layer only principals with roles/run.invoker can reach the
* container. Cloud Run strips the Authorization header after validation, so our
* middleware cannot re-verify the token. We trust Cloud Run's IAM instead.
*
* When NOT on Cloud Run and projectNumber is set, requests must include a valid
* Bearer token signed by Google Chat with the correct audience.
*
* When neither condition applies, verification is skipped (local testing).
*/
function createAuthMiddleware(
projectNumber: string | undefined,
): (req: Request, res: Response, next: NextFunction) => void {
// On Cloud Run, IAM handles auth — the Authorization header is stripped
// before reaching the container, so we cannot verify it ourselves.
if (process.env['K_SERVICE']) {
logger.info(
'[ChatBridge] Running on Cloud Run — auth delegated to Cloud Run IAM.',
);
return (_req: Request, _res: Response, next: NextFunction) => {
next();
};
}
if (!projectNumber) {
logger.warn(
'[ChatBridge] CHAT_PROJECT_NUMBER not set — JWT verification disabled. ' +
'Set it in production to verify requests come from Google Chat.',
);
return (_req: Request, _res: Response, next: NextFunction) => {
next();
};
}
const authClient = new OAuth2Client();
return (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn('[ChatBridge] Missing or invalid Authorization header');
res.status(401).json({ error: 'Unauthorized: missing Bearer token' });
return;
}
const token = authHeader.substring(7);
// Debug: decode token payload without verification to inspect claims
try {
const payloadB64 = token.split('.')[1];
if (payloadB64) {
const decoded = JSON.parse(
Buffer.from(payloadB64, 'base64').toString(),
);
logger.info(
`[ChatBridge] Token claims: iss=${String(decoded.iss ?? 'none')} ` +
`aud=${String(decoded.aud ?? 'none')} ` +
`email=${String(decoded.email ?? 'none')} ` +
`sub=${String(decoded.sub ?? 'none')}`,
);
}
} catch {
logger.warn('[ChatBridge] Could not decode token for debug logging');
}
authClient
.verifyIdToken({
idToken: token,
audience: projectNumber,
})
.then((ticket) => {
const payload = ticket.getPayload();
if (payload?.iss !== CHAT_ISSUER) {
logger.warn(
`[ChatBridge] Invalid token issuer: ${payload?.iss ?? 'unknown'}`,
);
res.status(403).json({ error: 'Forbidden: invalid token issuer' });
return;
}
next();
})
.catch((err: unknown) => {
const msg = err instanceof Error ? err.message : 'Unknown error';
logger.warn(`[ChatBridge] Token verification failed: ${msg}`);
res.status(401).json({ error: 'Unauthorized: invalid token' });
});
};
}
/** Safely extract a string from an unknown record. */
function str(obj: Record<string, unknown>, key: string): string {
const v = obj[key];
return typeof v === 'string' ? v : '';
}
/** Safely check if a value is a plain object. */
function isObj(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
/**
* Normalizes a Google Chat event to the legacy ChatEvent format.
* Workspace Add-ons send: {chat: {messagePayload, user, ...}, commonEventObject}
* Legacy format: {type: "MESSAGE", message: {...}, space: {...}, user: {...}}
*/
function normalizeEvent(raw: Record<string, unknown>): ChatEvent | null {
// Already in legacy format
if (typeof raw['type'] === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return raw as unknown as ChatEvent;
}
// Workspace Add-ons format
const chat = raw['chat'];
if (!isObj(chat)) return null;
const user = isObj(chat['user']) ? chat['user'] : {};
const eventTime = str(chat, 'eventTime');
// Check for card click actions (button clicks) via commonEventObject
const common = raw['commonEventObject'];
if (isObj(common) && typeof common['invokedFunction'] === 'string') {
const invokedFunction = common['invokedFunction'];
const params = isObj(common['parameters']) ? common['parameters'] : {};
// Build action parameters array from commonEventObject.parameters
const actionParams = Object.entries(params)
.filter(([, v]) => typeof v === 'string')
.map(([key, value]) => ({ key, value: String(value) }));
// Extract message/thread/space from chat object
const message = isObj(chat['message']) ? chat['message'] : {};
const thread = isObj(message['thread']) ? message['thread'] : {};
const space = isObj(chat['space'])
? chat['space']
: isObj(message['space'])
? message['space']
: {};
logger.info(
`[ChatBridge] Add-ons CARD_CLICKED: function=${invokedFunction} ` +
`params=${JSON.stringify(params)} thread=${str(thread, 'name')}`,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
type: 'CARD_CLICKED',
eventTime,
message: { ...message, thread, space },
space,
user,
action: {
actionMethodName: invokedFunction,
parameters: actionParams,
},
} as unknown as ChatEvent;
}
// Determine event type from which payload field is present
if (isObj(chat['messagePayload'])) {
const payload = chat['messagePayload'];
const message = isObj(payload['message']) ? payload['message'] : {};
const space = isObj(payload['space'])
? payload['space']
: isObj(message['space'])
? message['space']
: {};
const thread = isObj(message['thread']) ? message['thread'] : {};
logger.info(
`[ChatBridge] Add-ons MESSAGE: text="${str(message, 'text')}" ` +
`space=${str(space, 'name')} thread=${str(thread, 'name')}`,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
type: 'MESSAGE',
eventTime,
message: {
...message,
sender: message['sender'] ?? user,
thread,
space,
},
space,
user,
} as unknown as ChatEvent;
}
if (isObj(chat['addedToSpacePayload'])) {
const payload = chat['addedToSpacePayload'];
const space = isObj(payload['space']) ? payload['space'] : {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
type: 'ADDED_TO_SPACE',
eventTime,
space,
user,
} as unknown as ChatEvent;
}
if (isObj(chat['removedFromSpacePayload'])) {
const payload = chat['removedFromSpacePayload'];
const space = isObj(payload['space']) ? payload['space'] : {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
type: 'REMOVED_FROM_SPACE',
eventTime,
space,
user,
} as unknown as ChatEvent;
}
logger.warn(
`[ChatBridge] Unknown Add-ons event, chat keys: ${Object.keys(chat).join(',')}`,
);
return null;
}
/**
* Wraps a legacy ChatResponse in the Workspace Add-ons response format.
* Add-ons expects: {hostAppDataAction: {chatDataAction: {createMessageAction: {message}}}}
*/
function wrapAddOnsResponse(response: ChatResponse): Record<string, unknown> {
// Build the message object for the Add-ons format
const message: Record<string, unknown> = {};
if (response.text) {
message['text'] = response.text;
}
if (response.cardsV2) {
message['cardsV2'] = response.cardsV2;
}
// Include thread info so the reply goes to the user's thread
// instead of appearing as a top-level message
if (response.thread) {
const thread: Record<string, string> = {};
if (response.thread.name) thread['name'] = response.thread.name;
if (response.thread.threadKey)
thread['threadKey'] = response.thread.threadKey;
message['thread'] = thread;
}
// For action responses (like CARD_CLICKED acknowledgments), use updateMessageAction
if (response.actionResponse?.type === 'UPDATE_MESSAGE') {
return {
hostAppDataAction: {
chatDataAction: {
updateMessageAction: { message },
},
},
};
}
return {
hostAppDataAction: {
chatDataAction: {
createMessageAction: { message },
},
},
};
}
/**
* Creates Express routes for the Google Chat bridge.
*/
export function createChatBridgeRoutes(config: ChatBridgeConfig): Router {
const router = createRouter();
const handler = new ChatBridgeHandler(config);
const authMiddleware = createAuthMiddleware(config.projectNumber);
// Google Chat sends webhook events as POST requests
router.post(
'/chat/webhook',
authMiddleware,
async (req: Request, res: Response) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const rawBody = req.body as Record<string, unknown>;
// Normalize to legacy ChatEvent format. Google Chat HTTP endpoints
// configured as Workspace Add-ons send a different event structure:
// {chat: {messagePayload, user, eventTime}, commonEventObject: {...}}
// We convert to the legacy format our handler expects:
// {type: "MESSAGE", message: {...}, space: {...}, user: {...}}
const event = normalizeEvent(rawBody);
if (!event || !event.type) {
logger.warn(
`[ChatBridge] Could not parse event. Keys: ${Object.keys(rawBody).join(',')}`,
);
res.status(400).json({ error: 'Invalid event: missing type field' });
return;
}
logger.info(`[ChatBridge] Webhook received: type=${event.type}`);
// Detect if the request came in Add-ons format
const isAddOnsFormat = Boolean(rawBody['chat'] && !rawBody['type']);
const response = await handler.handleEvent(event);
// For CARD_CLICKED events, force UPDATE_MESSAGE so the card is
// replaced in-place rather than posting a new message.
if (event.type === 'CARD_CLICKED' && !response.actionResponse) {
response.actionResponse = { type: 'UPDATE_MESSAGE' };
}
if (isAddOnsFormat) {
// Wrap in Workspace Add-ons response format
const addOnsResponse = wrapAddOnsResponse(response);
logger.info(
`[ChatBridge] Add-ons response: ${JSON.stringify(addOnsResponse).substring(0, 200)}`,
);
res.json(addOnsResponse);
} else {
res.json(response);
}
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : 'Unknown error';
logger.error(`[ChatBridge] Webhook error: ${errorMsg}`, error);
res.status(500).json({
text: `Internal error: ${errorMsg}`,
});
}
},
);
// Health check endpoint for the chat bridge (no auth required)
router.get('/chat/health', (_req: Request, res: Response) => {
res.json({
status: 'ok',
bridge: 'google-chat',
a2aServerUrl: config.a2aServerUrl,
});
});
return router;
}
@@ -1,231 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Manages mapping between Google Chat threads and A2A sessions.
* Each Google Chat thread maintains a persistent contextId (conversation)
* and a transient taskId (active task within that conversation).
*
* Supports optional GCS persistence so session mappings survive
* Cloud Run instance restarts.
*/
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../utils/logger.js';
export interface PendingToolApproval {
callId: string;
taskId: string;
toolName: string;
}
export interface SessionInfo {
/** A2A contextId - persists for the lifetime of the Chat thread. */
contextId: string;
/** A2A taskId - cleared on terminal states, reused on input-required. */
taskId?: string;
/** Space name for async messaging. */
spaceName: string;
/** Thread name for async messaging. */
threadName: string;
/** Last activity timestamp. */
lastActivity: number;
/** Pending tool approval waiting for text-based response. */
pendingToolApproval?: PendingToolApproval;
/** When true, all tool calls are auto-approved. */
yoloMode?: boolean;
}
/** Serializable subset of SessionInfo for GCS persistence. */
interface PersistedSession {
contextId: string;
taskId?: string;
spaceName: string;
threadName: string;
lastActivity: number;
yoloMode?: boolean;
}
/**
* Session store mapping Google Chat thread names to A2A sessions.
* Optionally backed by GCS for persistence across restarts.
*/
export class SessionStore {
private sessions = new Map<string, SessionInfo>();
private gcsBucket?: string;
private gcsObjectPath = 'chat-bridge/sessions.json';
private dirty = false;
private flushTimer?: ReturnType<typeof setInterval>;
constructor(gcsBucket?: string) {
this.gcsBucket = gcsBucket;
if (gcsBucket) {
// Flush to GCS every 30 seconds if dirty
this.flushTimer = setInterval(() => {
if (this.dirty) {
this.persistToGCS().catch((err) =>
logger.warn(`[ChatBridge] GCS session flush failed:`, err),
);
}
}, 30000);
}
}
/**
* Restores sessions from GCS on startup.
*/
async restore(): Promise<void> {
if (!this.gcsBucket) return;
try {
const { Storage } = await import('@google-cloud/storage');
const storage = new Storage();
const file = storage.bucket(this.gcsBucket).file(this.gcsObjectPath);
const [exists] = await file.exists();
if (!exists) {
logger.info('[ChatBridge] No persisted sessions found in GCS.');
return;
}
const [contents] = await file.download();
const persisted: PersistedSession[] = JSON.parse(contents.toString());
for (const s of persisted) {
this.sessions.set(s.threadName, {
contextId: s.contextId,
taskId: s.taskId,
spaceName: s.spaceName,
threadName: s.threadName,
lastActivity: s.lastActivity,
yoloMode: s.yoloMode,
});
}
logger.info(
`[ChatBridge] Restored ${persisted.length} sessions from GCS.`,
);
} catch (err) {
logger.warn(`[ChatBridge] Could not restore sessions from GCS:`, err);
}
}
/**
* Persists current sessions to GCS.
*/
private async persistToGCS(): Promise<void> {
if (!this.gcsBucket) return;
try {
const { Storage } = await import('@google-cloud/storage');
const storage = new Storage();
const file = storage.bucket(this.gcsBucket).file(this.gcsObjectPath);
const persisted: PersistedSession[] = [];
for (const session of this.sessions.values()) {
persisted.push({
contextId: session.contextId,
taskId: session.taskId,
spaceName: session.spaceName,
threadName: session.threadName,
lastActivity: session.lastActivity,
yoloMode: session.yoloMode,
});
}
await file.save(JSON.stringify(persisted), {
contentType: 'application/json',
});
this.dirty = false;
logger.info(
`[ChatBridge] Persisted ${persisted.length} sessions to GCS.`,
);
} catch (err) {
logger.warn(`[ChatBridge] Failed to persist sessions to GCS:`, err);
}
}
/**
* Gets or creates a session for a Google Chat thread.
*/
getOrCreate(threadName: string, spaceName: string): SessionInfo {
let session = this.sessions.get(threadName);
if (!session) {
session = {
contextId: uuidv4(),
spaceName,
threadName,
lastActivity: Date.now(),
};
this.sessions.set(threadName, session);
this.dirty = true;
logger.info(
`[ChatBridge] New session for thread ${threadName}: contextId=${session.contextId}`,
);
}
session.lastActivity = Date.now();
return session;
}
/**
* Gets an existing session by thread name.
*/
get(threadName: string): SessionInfo | undefined {
return this.sessions.get(threadName);
}
/**
* Updates the taskId for a session.
*/
updateTaskId(threadName: string, taskId: string | undefined): void {
const session = this.sessions.get(threadName);
if (session) {
session.taskId = taskId;
this.dirty = true;
logger.info(
`[ChatBridge] Session ${threadName}: taskId=${taskId ?? 'cleared'}`,
);
}
}
/**
* Removes a session (e.g. when bot is removed from space).
*/
remove(threadName: string): void {
this.sessions.delete(threadName);
this.dirty = true;
}
/**
* Cleans up stale sessions older than the given max age (ms).
*/
cleanup(maxAgeMs: number = 24 * 60 * 60 * 1000): void {
const now = Date.now();
for (const [threadName, session] of this.sessions.entries()) {
if (now - session.lastActivity > maxAgeMs) {
this.sessions.delete(threadName);
this.dirty = true;
logger.info(`[ChatBridge] Cleaned up stale session: ${threadName}`);
}
}
}
/**
* Forces an immediate flush to GCS.
*/
async flush(): Promise<void> {
if (this.dirty) {
await this.persistToGCS();
}
}
/**
* Stops the periodic flush timer.
*/
dispose(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = undefined;
}
}
}
@@ -1,141 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Google Chat HTTP endpoint event types.
* @see https://developers.google.com/workspace/chat/api/reference/rest/v1/Event
*/
export interface ChatUser {
name: string;
displayName: string;
type?: 'HUMAN' | 'BOT';
}
export interface ChatThread {
name: string;
threadKey?: string;
}
export interface ChatSpace {
name: string;
type: 'DM' | 'ROOM' | 'SPACE';
displayName?: string;
}
export interface ChatMessage {
name: string;
sender: ChatUser;
createTime: string;
text?: string;
argumentText?: string;
thread: ChatThread;
space: ChatSpace;
cardsV2?: ChatCardV2[];
}
export interface ChatActionParameter {
key: string;
value: string;
}
export interface ChatAction {
actionMethodName: string;
parameters: ChatActionParameter[];
}
export type ChatEventType =
| 'MESSAGE'
| 'CARD_CLICKED'
| 'ADDED_TO_SPACE'
| 'REMOVED_FROM_SPACE';
export interface ChatEvent {
type: ChatEventType;
eventTime: string;
message?: ChatMessage;
space: ChatSpace;
user: ChatUser;
action?: ChatAction;
common?: Record<string, unknown>;
threadKey?: string;
}
// Google Chat Cards V2 response types
export interface ChatCardV2 {
cardId: string;
card: ChatCard;
}
export interface ChatCard {
header?: ChatCardHeader;
sections: ChatCardSection[];
}
export interface ChatCardHeader {
title: string;
subtitle?: string;
imageUrl?: string;
imageType?: 'CIRCLE' | 'SQUARE';
}
export interface ChatCardSection {
header?: string;
widgets: ChatWidget[];
collapsible?: boolean;
uncollapsibleWidgetsCount?: number;
}
export type ChatWidget =
| { textParagraph: { text: string } }
| { decoratedText: ChatDecoratedText }
| { buttonList: { buttons: ChatButton[] } }
| { divider: Record<string, never> };
export interface ChatDecoratedText {
text: string;
topLabel?: string;
bottomLabel?: string;
startIcon?: { knownIcon: string };
wrapText?: boolean;
}
export interface ChatButton {
text: string;
onClick: ChatOnClick;
color?: { red: number; green: number; blue: number; alpha?: number };
disabled?: boolean;
}
export interface ChatOnClick {
action: {
function: string;
parameters: ChatActionParameter[];
};
}
export interface ChatResponse {
text?: string;
cardsV2?: ChatCardV2[];
thread?: { threadKey?: string; name?: string };
actionResponse?: {
type: 'NEW_MESSAGE' | 'UPDATE_MESSAGE' | 'REQUEST_CONFIG';
};
}
// Bridge configuration
export interface ChatBridgeConfig {
/** URL of the A2A server to connect to (e.g. http://localhost:8080) */
a2aServerUrl: string;
/** Google Chat project number for verification (optional) */
projectNumber?: string;
/** Whether to enable debug logging */
debug?: boolean;
/** GCS bucket name for session persistence (optional) */
gcsBucket?: string;
}
-1
View File
@@ -85,7 +85,6 @@ export class InitCommand implements Command {
if (!context.agentExecutor) {
throw new Error('Agent executor not found in context.');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const agentExecutor = context.agentExecutor as CoderAgentExecutor;
const agentSettings: AgentSettings = {
+8 -147
View File
@@ -11,11 +11,6 @@ import type { Settings } from './settings.js';
import {
type ExtensionLoader,
FileDiscoveryService,
getCodeAssistServer,
Config,
ExperimentFlags,
fetchAdminControlsOnce,
type FetchAdminControlsResponse,
} from '@google/gemini-cli-core';
// Mock dependencies
@@ -24,37 +19,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
Config: vi.fn().mockImplementation((params) => {
const mockConfig = {
...params,
initialize: vi.fn(),
refreshAuth: vi.fn(),
getExperiments: vi.fn().mockReturnValue({
flags: {
[actual.ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
boolValue: false,
},
},
}),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
};
return mockConfig;
}),
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: { global: '', extension: '', project: '' },
fileCount: 0,
filePaths: [],
}),
Config: vi.fn().mockImplementation((params) => ({
initialize: vi.fn(),
refreshAuth: vi.fn(),
...params, // Expose params for assertion
})),
loadServerHierarchicalMemory: vi
.fn()
.mockResolvedValue({ memoryContent: '', fileCount: 0, filePaths: [] }),
startupProfiler: {
flush: vi.fn(),
},
FileDiscoveryService: vi.fn(),
getCodeAssistServer: vi.fn(),
fetchAdminControlsOnce: vi.fn(),
coreEvents: {
emitAdminSettingsChanged: vi.fn(),
},
};
});
@@ -80,121 +56,6 @@ describe('loadConfig', () => {
delete process.env['GEMINI_API_KEY'];
});
describe('admin settings overrides', () => {
it('should not fetch admin controls if experiment is disabled', async () => {
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(fetchAdminControlsOnce).not.toHaveBeenCalled();
});
describe('when admin controls experiment is enabled', () => {
beforeEach(() => {
// We need to cast to any here to modify the mock implementation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Config as any).mockImplementation((params: unknown) => {
const mockConfig = {
...(params as object),
initialize: vi.fn(),
refreshAuth: vi.fn(),
getExperiments: vi.fn().mockReturnValue({
flags: {
[ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
boolValue: true,
},
},
}),
getRemoteAdminSettings: vi.fn().mockReturnValue({}),
setRemoteAdminSettings: vi.fn(),
};
return mockConfig;
});
});
it('should fetch admin controls and apply them', async () => {
const mockAdminSettings: FetchAdminControlsResponse = {
mcpSetting: {
mcpEnabled: false,
},
cliFeatureSetting: {
extensionsSetting: {
extensionsEnabled: false,
},
},
strictModeDisabled: false,
};
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenLastCalledWith(
expect.objectContaining({
disableYoloMode: !mockAdminSettings.strictModeDisabled,
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
extensionsEnabled:
mockAdminSettings.cliFeatureSetting?.extensionsSetting
?.extensionsEnabled,
}),
);
});
it('should treat unset admin settings as false when admin settings are passed', async () => {
const mockAdminSettings: FetchAdminControlsResponse = {
mcpSetting: {
mcpEnabled: true,
},
};
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenLastCalledWith(
expect.objectContaining({
disableYoloMode: !false,
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
extensionsEnabled: undefined,
}),
);
});
it('should not pass default unset admin settings when no admin settings are present', async () => {
const mockAdminSettings: FetchAdminControlsResponse = {};
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenLastCalledWith(expect.objectContaining({}));
});
it('should fetch admin controls using the code assist server when available', async () => {
const mockAdminSettings: FetchAdminControlsResponse = {
mcpSetting: {
mcpEnabled: true,
},
strictModeDisabled: true,
};
const mockCodeAssistServer = { projectId: 'test-project' };
vi.mocked(getCodeAssistServer).mockReturnValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockCodeAssistServer as any,
);
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(fetchAdminControlsOnce).toHaveBeenCalledWith(
mockCodeAssistServer,
true,
);
expect(Config).toHaveBeenLastCalledWith(
expect.objectContaining({
disableYoloMode: !mockAdminSettings.strictModeDisabled,
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
extensionsEnabled: undefined,
}),
);
});
});
});
it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => {
const testPath = '/tmp/ignore';
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
+30 -73
View File
@@ -18,14 +18,12 @@ import {
loadServerHierarchicalMemory,
GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_MODEL,
type ExtensionLoader,
startupProfiler,
PREVIEW_GEMINI_MODEL,
homedir,
GitService,
fetchAdminControlsOnce,
getCodeAssistServer,
ExperimentFlags,
} from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js';
@@ -59,7 +57,9 @@ export async function loadConfig(
const configParams: ConfigParameters = {
sessionId: taskId,
model: PREVIEW_GEMINI_MODEL,
model: settings.general?.previewFeatures
? PREVIEW_GEMINI_MODEL
: DEFAULT_GEMINI_MODEL,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
sandbox: undefined, // Sandbox might not be relevant for a server-side agent
targetDir: workspaceDir, // Or a specific directory the agent operates on
@@ -77,7 +77,6 @@ export async function loadConfig(
cwd: workspaceDir,
telemetry: {
enabled: settings.telemetry?.enabled,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
target: settings.telemetry?.target as TelemetryTarget,
otlpEndpoint:
process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] ??
@@ -102,6 +101,7 @@ export async function loadConfig(
trustedFolder: true,
extensionLoader,
checkpointing,
previewFeatures: settings.general?.previewFeatures,
interactive: true,
enableInteractiveShell: true,
ptyInfo: 'auto',
@@ -124,50 +124,37 @@ export async function loadConfig(
configParams.userMemory = memoryContent;
configParams.geminiMdFileCount = fileCount;
configParams.geminiMdFilePaths = filePaths;
// Set an initial config to use to get a code assist server.
// This is needed to fetch admin controls.
const initialConfig = new Config({
const config = new Config({
...configParams,
});
const codeAssistServer = getCodeAssistServer(initialConfig);
const adminControlsEnabled =
initialConfig.getExperiments()?.flags[ExperimentFlags.ENABLE_ADMIN_CONTROLS]
?.boolValue ?? false;
// Initialize final config parameters to the previous parameters.
// If no admin controls are needed, these will be used as-is for the final
// config.
const finalConfigParams = { ...configParams };
if (adminControlsEnabled) {
const adminSettings = await fetchAdminControlsOnce(
codeAssistServer,
adminControlsEnabled,
);
// Admin settings are able to be undefined if unset, but if any are present,
// we should initialize them all.
// If any are present, undefined settings should be treated as if they were
// set to false.
// If NONE are present, disregard admin settings entirely, and pass the
// final config as is.
if (Object.keys(adminSettings).length !== 0) {
finalConfigParams.disableYoloMode = !adminSettings.strictModeDisabled;
finalConfigParams.mcpEnabled = adminSettings.mcpSetting?.mcpEnabled;
finalConfigParams.extensionsEnabled =
adminSettings.cliFeatureSetting?.extensionsSetting?.extensionsEnabled;
}
}
const config = new Config(finalConfigParams);
// Needed to initialize ToolRegistry, and git checkpointing if enabled
await config.initialize();
startupProfiler.flush(config);
await refreshAuthentication(config, adcFilePath, 'Config');
if (process.env['USE_CCPA']) {
logger.info('[Config] Using CCPA Auth:');
try {
if (adcFilePath) {
path.resolve(adcFilePath);
}
} catch (e) {
logger.error(
`[Config] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`,
);
}
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
logger.info(
`[Config] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`,
);
} else if (process.env['GEMINI_API_KEY']) {
logger.info('[Config] Using Gemini API Key');
await config.refreshAuth(AuthType.USE_GEMINI);
} else {
const errorMessage =
'[Config] Unable to set GeneratorConfig. Please provide a GEMINI_API_KEY or set USE_CCPA.';
logger.error(errorMessage);
throw new Error(errorMessage);
}
return config;
}
@@ -235,33 +222,3 @@ function findEnvFile(startDir: string): string | null {
currentDir = parentDir;
}
}
async function refreshAuthentication(
config: Config,
adcFilePath: string | undefined,
logPrefix: string,
): Promise<void> {
if (process.env['USE_CCPA']) {
logger.info(`[${logPrefix}] Using CCPA Auth:`);
try {
if (adcFilePath) {
path.resolve(adcFilePath);
}
} catch (e) {
logger.error(
`[${logPrefix}] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`,
);
}
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
logger.info(
`[${logPrefix}] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`,
);
} else if (process.env['GEMINI_API_KEY']) {
logger.info(`[${logPrefix}] Using Gemini API Key`);
await config.refreshAuth(AuthType.USE_GEMINI);
} else {
const errorMessage = `[${logPrefix}] Unable to set GeneratorConfig. Please provide a GEMINI_API_KEY or set USE_CCPA.`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
}
@@ -93,7 +93,6 @@ function loadExtension(extensionDir: string): GeminiCLIExtension | null {
try {
const configContent = fs.readFileSync(configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = JSON.parse(configContent) as ExtensionConfig;
if (!config.name || !config.version) {
logger.error(
@@ -108,7 +107,6 @@ function loadExtension(extensionDir: string): GeminiCLIExtension | null {
.map((contextFileName) => path.join(extensionDir, contextFileName))
.filter((contextFilePath) => fs.existsSync(contextFilePath));
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
name: config.name,
version: config.version,
@@ -142,7 +140,6 @@ export function loadInstallMetadata(
const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME);
try {
const configContent = fs.readFileSync(metadataFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const metadata = JSON.parse(configContent) as ExtensionInstallMetadata;
return metadata;
} catch (e) {
@@ -89,6 +89,67 @@ describe('loadSettings', () => {
vi.restoreAllMocks();
});
it('should load nested previewFeatures from user settings', () => {
const settings = {
general: {
previewFeatures: true,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBe(true);
});
it('should load nested previewFeatures from workspace settings', () => {
const settings = {
general: {
previewFeatures: true,
},
};
const workspaceSettingsPath = path.join(
mockGeminiWorkspaceDir,
'settings.json',
);
fs.writeFileSync(workspaceSettingsPath, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBe(true);
});
it('should prioritize workspace settings over user settings', () => {
const userSettings = {
general: {
previewFeatures: false,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(userSettings));
const workspaceSettings = {
general: {
previewFeatures: true,
},
};
const workspaceSettingsPath = path.join(
mockGeminiWorkspaceDir,
'settings.json',
);
fs.writeFileSync(workspaceSettingsPath, JSON.stringify(workspaceSettings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBe(true);
});
it('should handle missing previewFeatures', () => {
const settings = {
general: {},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.general?.previewFeatures).toBeUndefined();
});
it('should load other top-level settings correctly', () => {
const settings = {
showMemoryUsage: true,
+3 -4
View File
@@ -31,6 +31,9 @@ export interface Settings {
showMemoryUsage?: boolean;
checkpointing?: CheckpointingSettings;
folderTrust?: boolean;
general?: {
previewFeatures?: boolean;
};
// Git-aware file filtering settings
fileFiltering?: {
@@ -67,7 +70,6 @@ export function loadSettings(workspaceDir: string): Settings {
try {
if (fs.existsSync(USER_SETTINGS_PATH)) {
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parsedUserSettings = JSON.parse(
stripJsonComments(userContent),
) as Settings;
@@ -90,7 +92,6 @@ export function loadSettings(workspaceDir: string): Settings {
try {
if (fs.existsSync(workspaceSettingsPath)) {
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parsedWorkspaceSettings = JSON.parse(
stripJsonComments(projectContent),
) as Settings;
@@ -141,12 +142,10 @@ function resolveEnvVarsInObject<T>(obj: T): T {
}
if (typeof obj === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return resolveEnvVarsInString(obj) as unknown as T;
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T;
}
+3 -31
View File
@@ -28,8 +28,6 @@ import { commandRegistry } from '../commands/command-registry.js';
import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js';
import { GitService } from '@google/gemini-cli-core';
import { getA2UIAgentExtension } from '../a2ui/a2ui-extension.js';
import { createChatBridgeRoutes } from '../chat-bridge/routes.js';
type CommandResponse = {
name: string;
@@ -48,12 +46,11 @@ const coderAgentCard: AgentCard = {
url: 'https://google.com',
},
protocolVersion: '0.3.0',
version: '0.1.0', // A2UI-enabled version
version: '0.0.2', // Incremented version
capabilities: {
streaming: true,
pushNotifications: true,
pushNotifications: false,
stateTransitionHistory: true,
extensions: [getA2UIAgentExtension()],
},
securitySchemes: undefined,
security: undefined,
@@ -121,7 +118,6 @@ async function handleExecuteCommand(
const eventHandler = (event: AgentExecutionEvent) => {
const jsonRpcResponse = {
jsonrpc: '2.0',
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
id: 'taskId' in event ? event.taskId : (event as Message).messageId,
result: event,
};
@@ -203,28 +199,6 @@ export async function createApp() {
requestStorage.run({ req }, next);
});
// Mount Google Chat bridge routes BEFORE A2A SDK routes.
// The A2A SDK's setupRoutes registers a catch-all jsonRpcHandler middleware
// at baseUrl="" that intercepts ALL POST requests and returns 400 for
// non-JSON-RPC payloads. Chat bridge must be registered first.
const chatBridgeUrl =
process.env['CHAT_BRIDGE_A2A_URL'] || process.env['CODER_AGENT_PORT']
? `http://localhost:${process.env['CODER_AGENT_PORT'] || '8080'}`
: undefined;
if (chatBridgeUrl) {
expressApp.use(express.json());
const chatRoutes = createChatBridgeRoutes({
a2aServerUrl: chatBridgeUrl,
projectNumber: process.env['CHAT_PROJECT_NUMBER'],
debug: process.env['CHAT_BRIDGE_DEBUG'] === 'true',
gcsBucket: process.env['GCS_BUCKET_NAME'],
});
expressApp.use(chatRoutes);
logger.info(
`[CoreAgent] Google Chat bridge enabled at /chat/webhook (A2A: ${chatBridgeUrl})`,
);
}
const appBuilder = new A2AExpressApp(requestHandler);
expressApp = appBuilder.setupRoutes(expressApp, '');
expressApp.use(express.json());
@@ -232,7 +206,6 @@ export async function createApp() {
expressApp.post('/tasks', async (req, res) => {
try {
const taskId = uuidv4();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const agentSettings = req.body.agentSettings as
| AgentSettings
| undefined;
@@ -355,8 +328,7 @@ export async function main() {
const expressApp = await createApp();
const port = Number(process.env['CODER_AGENT_PORT'] || 0);
const host = process.env['CODER_AGENT_HOST'] || 'localhost';
const server = expressApp.listen(port, host, () => {
const server = expressApp.listen(port, 'localhost', () => {
const address = server.address();
let actualPort;
if (process.env['CODER_AGENT_PORT']) {
+1 -47
View File
@@ -18,7 +18,7 @@ import { setTargetDir } from '../config/config.js';
import { getPersistedState, type PersistedTaskMetadata } from '../types.js';
import { v4 as uuidv4 } from 'uuid';
type ObjectType = 'metadata' | 'workspace' | 'conversation';
type ObjectType = 'metadata' | 'workspace';
const getTmpArchiveFilename = (taskId: string): string =>
`task-${taskId}-workspace-${uuidv4()}.tar.gz`;
@@ -95,7 +95,6 @@ export class GCSTaskStore implements TaskStore {
await this.ensureBucketInitialized();
const taskId = task.id;
const persistedState = getPersistedState(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
task.metadata as PersistedTaskMetadata,
);
@@ -224,28 +223,6 @@ export class GCSTaskStore implements TaskStore {
`Workspace directory ${workDir} not found, skipping workspace save for task ${taskId}.`,
);
}
// Save conversation history if present in metadata
const rawHistory = dataToStore?.['_conversationHistory'];
const conversationHistory = Array.isArray(rawHistory)
? rawHistory
: undefined;
if (conversationHistory && conversationHistory.length > 0) {
const conversationObjectPath = this.getObjectPath(
taskId,
'conversation',
);
const historyJson = JSON.stringify(conversationHistory);
const compressedHistory = gzipSync(Buffer.from(historyJson));
const conversationFile = this.storage
.bucket(this.bucketName)
.file(conversationObjectPath);
await conversationFile.save(compressedHistory, {
contentType: 'application/gzip',
});
logger.info(
`Task ${taskId} conversation history saved to GCS: gs://${this.bucketName}/${conversationObjectPath} (${conversationHistory.length} entries)`,
);
}
} catch (error) {
logger.error(`Failed to save task ${taskId} to GCS:`, error);
throw error;
@@ -302,29 +279,6 @@ export class GCSTaskStore implements TaskStore {
logger.info(`Task ${taskId} workspace archive not found in GCS.`);
}
// Restore conversation history if available
const conversationObjectPath = this.getObjectPath(taskId, 'conversation');
const conversationFile = this.storage
.bucket(this.bucketName)
.file(conversationObjectPath);
const [conversationExists] = await conversationFile.exists();
if (conversationExists) {
try {
const [compressedHistory] = await conversationFile.download();
const historyJson = gunzipSync(compressedHistory).toString();
const conversationHistory: unknown[] = JSON.parse(historyJson);
loadedMetadata['_conversationHistory'] = conversationHistory;
logger.info(
`Task ${taskId} conversation history restored from GCS (${conversationHistory.length} entries)`,
);
} catch (historyError) {
logger.warn(
`Task ${taskId} conversation history could not be restored:`,
historyError,
);
}
}
return {
id: taskId,
contextId: loadedMetadata._contextId || uuidv4(),
-1
View File
@@ -125,7 +125,6 @@ export const METADATA_KEY = '__persistedState';
export function getPersistedState(
metadata: PersistedTaskMetadata,
): PersistedStateMetadata | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return metadata?.[METADATA_KEY] as PersistedStateMetadata | undefined;
}
+5 -13
View File
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as path from 'node:path';
import type {
Task as SDKTask,
TaskStatusUpdateEvent,
@@ -13,11 +12,11 @@ import type {
import {
ApprovalMode,
DEFAULT_GEMINI_MODEL,
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
GeminiClient,
HookSystem,
PolicyDecision,
tmpdir,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
import type { Config, Storage } from '@google/gemini-cli-core';
@@ -26,8 +25,6 @@ import { expect, vi } from 'vitest';
export function createMockConfig(
overrides: Partial<Config> = {},
): Partial<Config> {
const tmpDir = tmpdir();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const mockConfig = {
getToolRegistry: vi.fn().mockReturnValue({
getTool: vi.fn(),
@@ -42,15 +39,15 @@ export function createMockConfig(
getWorkspaceContext: vi.fn().mockReturnValue({
isPathWithinWorkspace: () => true,
}),
getTargetDir: () => tmpDir,
getTargetDir: () => '/test',
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
storage: {
getProjectTempDir: () => tmpDir,
getProjectTempCheckpointsDir: () => path.join(tmpDir, 'checkpoints'),
getProjectTempDir: () => '/tmp',
getProjectTempCheckpointsDir: () => '/tmp/checkpoints',
} as Storage,
getTruncateToolOutputThreshold: () =>
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL),
getDebugMode: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi.fn().mockReturnValue({ model: 'gemini-pro' }),
@@ -150,7 +147,6 @@ export function assertUniqueFinalEventIsLast(
events: SendStreamingMessageSuccessResponse[],
) {
// Final event is input-required & final
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const finalEvent = events[events.length - 1].result as TaskStatusUpdateEvent;
expect(finalEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'state-change',
@@ -160,11 +156,9 @@ export function assertUniqueFinalEventIsLast(
// There is only one event with final and its the last
expect(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
events.filter((e) => (e.result as TaskStatusUpdateEvent).final).length,
).toBe(1);
expect(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
events.findIndex((e) => (e.result as TaskStatusUpdateEvent).final),
).toBe(events.length - 1);
}
@@ -173,13 +167,11 @@ export function assertTaskCreationAndWorkingStatus(
events: SendStreamingMessageSuccessResponse[],
) {
// Initial task creation event
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const taskEvent = events[0].result as SDKTask;
expect(taskEvent.kind).toBe('task');
expect(taskEvent.status.state).toBe('submitted');
// Status update: working
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const workingEvent = events[1].result as TaskStatusUpdateEvent;
expect(workingEvent.kind).toBe('status-update');
expect(workingEvent.status.state).toBe('working');
-48
View File
@@ -1,48 +0,0 @@
#!/bin/bash
# Test script for the Google Chat bridge webhook endpoint.
# Simulates Google Chat events to verify the bridge works.
#
# Usage: ./test-chat-bridge.sh [PORT]
# Default port: 9090 (for kubectl port-forward)
PORT=${1:-9090}
BASE_URL="http://localhost:${PORT}"
echo "Testing chat bridge at ${BASE_URL}..."
# 1. Test health endpoint
echo -e "\n--- Health Check ---"
curl -s "${BASE_URL}/chat/health" | jq .
# 2. Test ADDED_TO_SPACE event
echo -e "\n--- ADDED_TO_SPACE ---"
curl -s -X POST "${BASE_URL}/chat/webhook" \
-H "Content-Type: application/json" \
-d '{
"type": "ADDED_TO_SPACE",
"eventTime": "2026-01-01T00:00:00Z",
"space": { "name": "spaces/test123", "type": "DM" },
"user": { "name": "users/123", "displayName": "Test User" }
}' | jq .
# 3. Test MESSAGE event
echo -e "\n--- MESSAGE (Hello) ---"
curl -s -X POST "${BASE_URL}/chat/webhook" \
-H "Content-Type: application/json" \
-d '{
"type": "MESSAGE",
"eventTime": "2026-01-01T00:01:00Z",
"message": {
"name": "spaces/test123/messages/msg1",
"sender": { "name": "users/123", "displayName": "Test User" },
"createTime": "2026-01-01T00:01:00Z",
"text": "Hello, write me a python hello world",
"argumentText": "Hello, write me a python hello world",
"thread": { "name": "spaces/test123/threads/thread1" },
"space": { "name": "spaces/test123", "type": "DM" }
},
"space": { "name": "spaces/test123", "type": "DM" },
"user": { "name": "users/123", "displayName": "Test User" }
}' | jq .
echo -e "\nDone."
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.29.0-nightly.20260203.71f46f116",
"version": "0.28.0-nightly.20260128.adc8e11bb",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -26,7 +26,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.0-nightly.20260203.71f46f116"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -54,7 +54,6 @@
"mnemonist": "^0.40.3",
"open": "^10.1.2",
"prompts": "^2.4.2",
"proper-lockfile": "^4.1.2",
"react": "^19.2.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
@@ -66,7 +65,6 @@
"tinygradient": "^1.1.5",
"undici": "^7.10.0",
"wrap-ansi": "9.0.2",
"ws": "^8.16.0",
"yargs": "^17.7.2",
"zod": "^3.23.8"
},
@@ -82,7 +80,6 @@
"@types/semver": "^7.7.0",
"@types/shell-quote": "^1.7.5",
"@types/tar": "^6.1.13",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"archiver": "^7.0.1",
"ink-testing-library": "^4.0.0",
@@ -17,26 +17,32 @@ import yargs from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import {
updateSetting,
promptForSetting,
getScopedEnvContents,
type ExtensionSetting,
} from '../../config/extensions/extensionSettings.js';
import prompts from 'prompts';
import * as fs from 'node:fs';
const { mockExtensionManager, mockGetExtensionManager, mockLoadSettings } =
vi.hoisted(() => {
const extensionManager = {
loadExtensionConfig: vi.fn(),
getExtensions: vi.fn(),
loadExtensions: vi.fn(),
getSettings: vi.fn(),
};
return {
mockExtensionManager: extensionManager,
mockGetExtensionManager: vi.fn(),
mockLoadSettings: vi.fn().mockReturnValue({ merged: {} }),
};
});
const {
mockExtensionManager,
mockGetExtensionAndManager,
mockGetExtensionManager,
mockLoadSettings,
} = vi.hoisted(() => {
const extensionManager = {
loadExtensionConfig: vi.fn(),
getExtensions: vi.fn(),
loadExtensions: vi.fn(),
getSettings: vi.fn(),
};
return {
mockExtensionManager: extensionManager,
mockGetExtensionAndManager: vi.fn(),
mockGetExtensionManager: vi.fn(),
mockLoadSettings: vi.fn().mockReturnValue({ merged: {} }),
};
});
vi.mock('../../config/extension-manager.js', () => ({
ExtensionManager: vi.fn().mockImplementation(() => mockExtensionManager),
@@ -56,13 +62,10 @@ vi.mock('../utils.js', () => ({
exitCli: vi.fn(),
}));
vi.mock('./utils.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils.js')>();
return {
...actual,
getExtensionManager: mockGetExtensionManager,
};
});
vi.mock('./utils.js', () => ({
getExtensionAndManager: mockGetExtensionAndManager,
getExtensionManager: mockGetExtensionManager,
}));
vi.mock('prompts');
@@ -88,6 +91,10 @@ describe('extensions configure command', () => {
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
// Default behaviors
mockLoadSettings.mockReturnValue({ merged: {} });
mockGetExtensionAndManager.mockResolvedValue({
extension: null,
extensionManager: null,
});
mockGetExtensionManager.mockResolvedValue(mockExtensionManager);
(ExtensionManager as unknown as Mock).mockImplementation(
() => mockExtensionManager,
@@ -110,6 +117,11 @@ describe('extensions configure command', () => {
path = '/test/path',
) => {
const extension = { name, path, id };
mockGetExtensionAndManager.mockImplementation(async (n) => {
if (n === name)
return { extension, extensionManager: mockExtensionManager };
return { extension: null, extensionManager: null };
});
mockExtensionManager.getExtensions.mockReturnValue([extension]);
mockExtensionManager.loadExtensionConfig.mockResolvedValue({
@@ -132,14 +144,17 @@ describe('extensions configure command', () => {
expect.objectContaining({ name: 'test-ext' }),
'test-id',
'TEST_VAR',
expect.any(Function),
promptForSetting,
'user',
tempWorkspaceDir,
);
});
it('should handle missing extension', async () => {
mockExtensionManager.getExtensions.mockReturnValue([]);
mockGetExtensionAndManager.mockResolvedValue({
extension: null,
extensionManager: null,
});
await runCommand('config missing-ext TEST_VAR');
@@ -175,7 +190,7 @@ describe('extensions configure command', () => {
expect.objectContaining({ name: 'test-ext' }),
'test-id',
'VAR_1',
expect.any(Function),
promptForSetting,
'user',
tempWorkspaceDir,
);
@@ -190,7 +205,7 @@ describe('extensions configure command', () => {
return {};
},
);
(prompts as unknown as Mock).mockResolvedValue({ confirm: true });
(prompts as unknown as Mock).mockResolvedValue({ overwrite: true });
(updateSetting as Mock).mockResolvedValue(undefined);
await runCommand('config test-ext');
@@ -226,7 +241,7 @@ describe('extensions configure command', () => {
const settings = [{ name: 'Setting 1', envVar: 'VAR_1' }];
setupExtension('test-ext', settings);
(getScopedEnvContents as Mock).mockResolvedValue({ VAR_1: 'existing' });
(prompts as unknown as Mock).mockResolvedValue({ confirm: false });
(prompts as unknown as Mock).mockResolvedValue({ overwrite: false });
await runCommand('config test-ext');
+150 -23
View File
@@ -5,17 +5,18 @@
*/
import type { CommandModule } from 'yargs';
import type { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
import {
configureAllExtensions,
configureExtension,
configureSpecificSetting,
getExtensionManager,
} from './utils.js';
updateSetting,
promptForSetting,
ExtensionSettingScope,
getScopedEnvContents,
} from '../../config/extensions/extensionSettings.js';
import { getExtensionAndManager, getExtensionManager } from './utils.js';
import { loadSettings } from '../../config/settings.js';
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
import prompts from 'prompts';
import type { ExtensionConfig } from '../../config/extension.js';
interface ConfigureArgs {
name?: string;
setting?: string;
@@ -63,36 +64,162 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
}
}
const extensionManager = await getExtensionManager();
// Case 1: Configure specific setting for an extension
if (name && setting) {
await configureSpecificSetting(
extensionManager,
name,
setting,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope as ExtensionSettingScope,
);
}
// Case 2: Configure all settings for an extension
else if (name) {
await configureExtension(
extensionManager,
name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope as ExtensionSettingScope,
);
await configureExtension(name, scope as ExtensionSettingScope);
}
// Case 3: Configure all extensions
else {
await configureAllExtensions(
extensionManager,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope as ExtensionSettingScope,
);
await configureAllExtensions(scope as ExtensionSettingScope);
}
await exitCli();
},
};
async function configureSpecificSetting(
extensionName: string,
settingKey: string,
scope: ExtensionSettingScope,
) {
const { extension, extensionManager } =
await getExtensionAndManager(extensionName);
if (!extension || !extensionManager) {
return;
}
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (!extensionConfig) {
debugLogger.error(
`Could not find configuration for extension "${extensionName}".`,
);
return;
}
await updateSetting(
extensionConfig,
extension.id,
settingKey,
promptForSetting,
scope,
process.cwd(),
);
}
async function configureExtension(
extensionName: string,
scope: ExtensionSettingScope,
) {
const { extension, extensionManager } =
await getExtensionAndManager(extensionName);
if (!extension || !extensionManager) {
return;
}
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
!extensionConfig ||
!extensionConfig.settings ||
extensionConfig.settings.length === 0
) {
debugLogger.log(
`Extension "${extensionName}" has no settings to configure.`,
);
return;
}
debugLogger.log(`Configuring settings for "${extensionName}"...`);
await configureExtensionSettings(extensionConfig, extension.id, scope);
}
async function configureAllExtensions(scope: ExtensionSettingScope) {
const extensionManager = await getExtensionManager();
const extensions = extensionManager.getExtensions();
if (extensions.length === 0) {
debugLogger.log('No extensions installed.');
return;
}
for (const extension of extensions) {
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
extensionConfig &&
extensionConfig.settings &&
extensionConfig.settings.length > 0
) {
debugLogger.log(`\nConfiguring settings for "${extension.name}"...`);
await configureExtensionSettings(extensionConfig, extension.id, scope);
}
}
}
async function configureExtensionSettings(
extensionConfig: ExtensionConfig,
extensionId: string,
scope: ExtensionSettingScope,
) {
const currentScopedSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
scope,
process.cwd(),
);
let workspaceSettings: Record<string, string> = {};
if (scope === ExtensionSettingScope.USER) {
workspaceSettings = await getScopedEnvContents(
extensionConfig,
extensionId,
ExtensionSettingScope.WORKSPACE,
process.cwd(),
);
}
if (!extensionConfig.settings) return;
for (const setting of extensionConfig.settings) {
const currentValue = currentScopedSettings[setting.envVar];
const workspaceValue = workspaceSettings[setting.envVar];
if (workspaceValue !== undefined) {
debugLogger.log(
`Note: Setting "${setting.name}" is already configured in the workspace scope.`,
);
}
if (currentValue !== undefined) {
const response = await prompts({
type: 'confirm',
name: 'overwrite',
message: `Setting "${setting.name}" (${setting.envVar}) is already set. Overwrite?`,
initial: false,
});
if (!response.overwrite) {
continue;
}
}
await updateSetting(
extensionConfig,
extensionId,
setting.envVar,
promptForSetting,
scope,
process.cwd(),
);
}
}
@@ -79,9 +79,7 @@ export const disableCommand: CommandModule = {
}),
handler: async (argv) => {
await handleDisable({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as string,
});
await exitCli();
@@ -105,9 +105,7 @@ export const enableCommand: CommandModule = {
}),
handler: async (argv) => {
await handleEnable({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as string,
});
await exitCli();
@@ -99,15 +99,10 @@ export const installCommand: CommandModule = {
}),
handler: async (argv) => {
await handleInstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
source: argv['source'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
ref: argv['ref'] as string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
autoUpdate: argv['auto-update'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
allowPreRelease: argv['pre-release'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
});
await exitCli();
+1 -6
View File
@@ -5,7 +5,6 @@
*/
import type { CommandModule } from 'yargs';
import chalk from 'chalk';
import {
debugLogger,
type ExtensionInstallMetadata,
@@ -50,9 +49,7 @@ export async function handleLink(args: InstallArgs) {
const extension =
await extensionManager.installOrUpdateExtension(installMetadata);
debugLogger.log(
chalk.green(
`Extension "${extension.name}" linked successfully and enabled.`,
),
`Extension "${extension.name}" linked successfully and enabled.`,
);
} catch (error) {
debugLogger.error(getErrorMessage(error));
@@ -79,9 +76,7 @@ export const linkCommand: CommandModule = {
.check((_) => true),
handler: async (argv) => {
await handleLink({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: argv['path'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
});
await exitCli();
@@ -62,7 +62,6 @@ export const listCommand: CommandModule = {
}),
handler: async (argv) => {
await handleList({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
outputFormat: argv['output-format'] as 'text' | 'json',
});
await exitCli();
@@ -98,9 +98,7 @@ export const newCommand: CommandModule = {
},
handler: async (args) => {
await handleNew({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: args['path'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
template: args['template'] as string | undefined,
});
await exitCli();
@@ -71,7 +71,6 @@ export const uninstallCommand: CommandModule = {
}),
handler: async (argv) => {
await handleUninstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
names: argv['names'] as string[],
});
await exitCli();

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