Compare commits

..

5 Commits

530 changed files with 7301 additions and 21802 deletions
-4
View File
@@ -45,10 +45,6 @@ Write precisely to ensure your instructions are unambiguous.
specific verbs.
- **Examples:** Use meaningful names in examples; avoid placeholders like
"foo" or "bar."
- **Quota and limit terminology:** For any content involving resource capacity
or using the word "quota" or "limit", strictly adhere to the guidelines in
the `quota-limit-style-guide.md` resource file. Generally, Use "quota" for the
administrative bucket and "limit" for the numerical ceiling.
### Formatting and syntax
Apply consistent formatting to make documentation visually organized and
@@ -1,61 +0,0 @@
# Style Guide: Quota vs. Limit
This guide defines the usage of "quota," "limit," and related terms in
user-facing interfaces.
## TL;DR
- **`quota`**: The administrative "bucket." Use for settings, billing, and
requesting increases. (e.g., "Adjust your storage **quota**.")
- **`limit`**: The real-time numerical "ceiling." Use for error messages when a
user is blocked. (e.g., "You've reached your request **limit**.")
- **When blocked, combine them:** Explain the **limit** that was hit and the
**quota** that is the remedy. (e.g., "You've reached the request **limit** for
your developer **quota**.")
- **Related terms:** Use `usage` for consumption tracking, `restriction` for
fixed rules, and `reset` for when a limit refreshes.
---
## Detailed Guidelines
### Definitions
- **Quota is the "what":** It identifies the category of resource being managed
(e.g., storage quota, GPU quota, request/prompt quota).
- **Limit is the "how much":** It defines the numerical boundary.
Use **quota** when referring to the administrative concept or the request for
more. Use **limit** when discussing the specific point of exhaustion.
### When to use "quota"
Use this term for **account management, billing, and settings.** It describes
the entitlement the user has purchased or been assigned.
**Examples:**
- **Navigation label:** Quota and usage
- **Contextual help:** Your **usage quota** is managed by your organization. To
request an increase, contact your administrator.
### When to use "limit"
Use this term for **real-time feedback, notifications, and error messages.** It
identifies the specific wall the user just hit.
**Examples:**
- **Error message:** Youve reached the 50-request-per-minute **limit**.
- **Inline warning:** Input exceeds the 32k token **limit**.
### How to use both together
When a user is blocked, combine both terms to explain the **event** (limit) and
the **remedy** (quota).
**Example:**
- **Heading:** Daily usage limit reached
- **Body:** You've reached the maximum daily capacity for your developer quota.
To continue working today, upgrade your quota.
-6
View File
@@ -14,9 +14,3 @@
# Docs have a dedicated approver group in addition to maintainers
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
# Prompt contents, tool definitions, and evals require reviews from prompt approvers
/packages/core/src/prompts/ @google-gemini/gemini-cli-prompt-approvers
/packages/core/src/tools/ @google-gemini/gemini-cli-prompt-approvers
/evals/ @google-gemini/gemini-cli-prompt-approvers
@@ -192,13 +192,6 @@ runs:
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
- name: '📦 Prepare bundled CLI for npm release'
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
node ${{ github.workspace }}/scripts/prepare-npm-release.js
- name: 'Get CLI Token'
uses: './.github/actions/npm-auth-token'
id: 'cli-token'
+4 -13
View File
@@ -44,8 +44,6 @@ runs:
- name: 'npm build'
shell: 'bash'
run: 'npm run build'
- name: 'Set up QEMU'
uses: 'docker/setup-qemu-action@v3'
- name: 'Set up Docker Buildx'
uses: 'docker/setup-buildx-action@v3'
- name: 'Log in to GitHub Container Registry'
@@ -71,19 +69,16 @@ runs:
env:
INPUTS_GITHUB_REF_NAME: '${{ inputs.github-ref-name }}'
INPUTS_GITHUB_SHA: '${{ inputs.github-sha }}'
# We build amd64 just so we can verify it.
# We build and push both amd64 and arm64 in the publish step.
- name: 'build'
id: 'docker_build'
shell: 'bash'
env:
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
GEMINI_SANDBOX: 'docker'
BUILD_SANDBOX_FLAGS: '--platform linux/amd64 --load'
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
run: |-
npm run build:sandbox -- \
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}" \
--image google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG} \
--output-file final_image_uri.txt
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
- name: 'verify'
@@ -97,14 +92,10 @@ runs:
- name: 'publish'
shell: 'bash'
if: "${{ inputs.dry-run != 'true' }}"
env:
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
GEMINI_SANDBOX: 'docker'
BUILD_SANDBOX_FLAGS: '--platform linux/amd64,linux/arm64 --push'
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
run: |-
npm run build:sandbox -- \
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}"
docker push "${STEPS_DOCKER_BUILD_OUTPUTS_URI}"
env:
STEPS_DOCKER_BUILD_OUTPUTS_URI: '${{ steps.docker_build.outputs.uri }}'
- name: 'Create issue on failure'
if: |-
${{ failure() }}
-8
View File
@@ -290,7 +290,6 @@ jobs:
with:
ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}'
fetch-depth: 0
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
@@ -303,14 +302,7 @@ jobs:
- name: 'Build project'
run: 'npm run build'
- name: 'Check if evals should run'
id: 'check_evals'
run: |
SHOULD_RUN=$(node scripts/changed_prompt.js)
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
- name: 'Run Evals (Required to pass)'
if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
run: 'npm run test:always_passing_evals'
+1
View File
@@ -117,6 +117,7 @@ jobs:
name: 'Slow E2E - Win'
runs-on: 'gemini-cli-windows-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
@@ -23,10 +23,6 @@ jobs:
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
env:
APP_ID: '${{ secrets.APP_ID }}'
if: |-
${{ env.APP_ID != '' }}
uses: 'actions/create-github-app-token@v2'
with:
app-id: '${{ secrets.APP_ID }}'
@@ -37,7 +33,7 @@ jobs:
env:
DRY_RUN: '${{ inputs.dry_run }}'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
github-token: '${{ steps.generate_token.outputs.token }}'
script: |
const dryRun = process.env.DRY_RUN === 'true';
const thirtyDaysAgo = new Date();
+1 -41
View File
@@ -25,7 +25,7 @@ jobs:
if: |-
github.repository == 'google-gemini/gemini-cli' &&
github.event_name == 'issue_comment' &&
(contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/unassign'))
contains(github.event.comment.body, '/assign')
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
@@ -38,7 +38,6 @@ jobs:
permission-issues: 'write'
- name: 'Assign issue to user'
if: "contains(github.event.comment.body, '/assign')"
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
@@ -109,42 +108,3 @@ jobs:
issue_number: issueNumber,
body: `👋 @${commenter}, you've been assigned to this issue! Thank you for taking the time to contribute. Make sure to check out our [contributing guidelines](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md).`
});
- name: 'Unassign issue from user'
if: "contains(github.event.comment.body, '/unassign')"
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |
const issueNumber = context.issue.number;
const commenter = context.actor;
const owner = context.repo.owner;
const repo = context.repo.repo;
const commentBody = context.payload.comment.body.trim();
if (commentBody !== '/unassign') {
return;
}
const issue = await github.rest.issues.get({
owner: owner,
repo: repo,
issue_number: issueNumber,
});
const isAssigned = issue.data.assignees.some(assignee => assignee.login === commenter);
if (isAssigned) {
await github.rest.issues.removeAssignees({
owner: owner,
repo: repo,
issue_number: issueNumber,
assignees: [commenter]
});
await github.rest.issues.createComment({
owner: owner,
repo: repo,
issue_number: issueNumber,
body: `👋 @${commenter}, you have been unassigned from this issue.`
});
}
-160
View File
@@ -1,160 +0,0 @@
name: 'Test Build Binary'
on:
workflow_dispatch:
permissions:
contents: 'read'
defaults:
run:
shell: 'bash'
jobs:
build-node-binary:
name: 'Build Binary (${{ matrix.os }})'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
matrix:
include:
- os: 'ubuntu-latest'
platform_name: 'linux-x64'
arch: 'x64'
- os: 'windows-latest'
platform_name: 'win32-x64'
arch: 'x64'
- os: 'macos-latest' # Apple Silicon (ARM64)
platform_name: 'darwin-arm64'
arch: 'arm64'
- os: 'macos-latest' # Intel (x64) running on ARM via Rosetta
platform_name: 'darwin-x64'
arch: 'x64'
steps:
- name: 'Checkout'
uses: 'actions/checkout@v4'
- name: 'Optimize Windows Performance'
if: "matrix.os == 'windows-latest'"
run: |
Set-MpPreference -DisableRealtimeMonitoring $true
Stop-Service -Name "wsearch" -Force -ErrorAction SilentlyContinue
Set-Service -Name "wsearch" -StartupType Disabled
Stop-Service -Name "SysMain" -Force -ErrorAction SilentlyContinue
Set-Service -Name "SysMain" -StartupType Disabled
shell: 'powershell'
- name: 'Set up Node.js'
uses: 'actions/setup-node@v4'
with:
node-version-file: '.nvmrc'
architecture: '${{ matrix.arch }}'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Check Secrets'
id: 'check_secrets'
run: |
echo "has_win_cert=${{ secrets.WINDOWS_PFX_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
echo "has_mac_cert=${{ secrets.MACOS_CERT_P12_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
- name: 'Setup Windows SDK (Windows)'
if: "matrix.os == 'windows-latest'"
uses: 'microsoft/setup-msbuild@v2'
- name: 'Add Signtool to Path (Windows)'
if: "matrix.os == 'windows-latest'"
run: |
$signtoolPath = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty DirectoryName
echo "Found signtool at: $signtoolPath"
echo "$signtoolPath" >> $env:GITHUB_PATH
shell: 'pwsh'
- name: 'Setup macOS Keychain'
if: "startsWith(matrix.os, 'macos') && steps.check_secrets.outputs.has_mac_cert == 'true' && github.event_name != 'pull_request'"
env:
BUILD_CERTIFICATE_BASE64: '${{ secrets.MACOS_CERT_P12_BASE64 }}'
P12_PASSWORD: '${{ secrets.MACOS_CERT_PASSWORD }}'
KEYCHAIN_PASSWORD: 'temp-password'
run: |
# Create the P12 file
echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
# Create a temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Import the certificate
security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
# Allow codesign to access it
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Set Identity for build script
echo "APPLE_IDENTITY=${{ secrets.MACOS_CERT_IDENTITY }}" >> "$GITHUB_ENV"
- name: 'Setup Windows Certificate'
if: "matrix.os == 'windows-latest' && steps.check_secrets.outputs.has_win_cert == 'true' && github.event_name != 'pull_request'"
env:
PFX_BASE64: '${{ secrets.WINDOWS_PFX_BASE64 }}'
PFX_PASSWORD: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
run: |
$pfx_cert_byte = [System.Convert]::FromBase64String("$env:PFX_BASE64")
$certPath = Join-Path (Get-Location) "cert.pfx"
[IO.File]::WriteAllBytes($certPath, $pfx_cert_byte)
echo "WINDOWS_PFX_FILE=$certPath" >> $env:GITHUB_ENV
echo "WINDOWS_PFX_PASSWORD=$env:PFX_PASSWORD" >> $env:GITHUB_ENV
shell: 'pwsh'
- name: 'Build Binary'
run: 'npm run build:binary'
- name: 'Build Core Package'
run: 'npm run build -w @google/gemini-cli-core'
- name: 'Verify Output Exists'
run: |
if [ -f "dist/${{ matrix.platform_name }}/gemini" ]; then
echo "Binary found at dist/${{ matrix.platform_name }}/gemini"
elif [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
echo "Binary found at dist/${{ matrix.platform_name }}/gemini.exe"
else
echo "Error: Binary not found in dist/${{ matrix.platform_name }}/"
ls -R dist/
exit 1
fi
- name: 'Smoke Test Binary'
run: |
echo "Running binary smoke test..."
if [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
"./dist/${{ matrix.platform_name }}/gemini.exe" --version
else
"./dist/${{ matrix.platform_name }}/gemini" --version
fi
- name: 'Run Integration Tests'
if: "github.event_name != 'pull_request'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
run: |
echo "Running integration tests with binary..."
if [[ "${{ matrix.os }}" == 'windows-latest' ]]; then
BINARY_PATH="$(cygpath -m "$(pwd)/dist/${{ matrix.platform_name }}/gemini.exe")"
else
BINARY_PATH="$(pwd)/dist/${{ matrix.platform_name }}/gemini"
fi
echo "Using binary at $BINARY_PATH"
export INTEGRATION_TEST_GEMINI_BINARY_PATH="$BINARY_PATH"
npm run test:integration:sandbox:none -- --testTimeout=600000
- name: 'Upload Artifact'
uses: 'actions/upload-artifact@v4'
with:
name: 'gemini-cli-${{ matrix.platform_name }}'
path: 'dist/${{ matrix.platform_name }}/'
retention-days: 5
@@ -1,315 +0,0 @@
name: 'Unassign Inactive Issue Assignees'
# This workflow runs daily and scans every open "help wanted" issue that has
# one or more assignees. For each assignee it checks whether they have a
# non-draft pull request (open and ready for review, or already merged) that
# is linked to the issue. Draft PRs are intentionally excluded so that
# contributors cannot reset the check by opening a no-op PR. If no
# qualifying PR is found within 7 days of assignment the assignee is
# automatically removed and a friendly comment is posted so that other
# contributors can pick up the work.
# Maintainers, org members, and collaborators (anyone with write access or
# above) are always exempted and will never be auto-unassigned.
on:
schedule:
- cron: '0 9 * * *' # Every day at 09:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Run in dry-run mode (no changes will be applied)'
required: false
default: false
type: 'boolean'
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
unassign-inactive-assignees:
if: "github.repository == 'google-gemini/gemini-cli'"
runs-on: 'ubuntu-latest'
permissions:
issues: 'write'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
uses: 'actions/create-github-app-token@v2'
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
- name: 'Unassign inactive assignees'
uses: 'actions/github-script@v7'
env:
DRY_RUN: '${{ inputs.dry_run }}'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |
const dryRun = process.env.DRY_RUN === 'true';
if (dryRun) {
core.info('DRY RUN MODE ENABLED: No changes will be applied.');
}
const owner = context.repo.owner;
const repo = context.repo.repo;
const GRACE_PERIOD_DAYS = 7;
const now = new Date();
let maintainerLogins = new Set();
const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];
for (const team_slug of teams) {
try {
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
org: owner,
team_slug,
});
for (const m of members) maintainerLogins.add(m.login.toLowerCase());
core.info(`Fetched ${members.length} members from team ${team_slug}.`);
} catch (e) {
core.warning(`Could not fetch team ${team_slug}: ${e.message}`);
}
}
const isGooglerCache = new Map();
const isGoogler = async (login) => {
if (isGooglerCache.has(login)) return isGooglerCache.get(login);
try {
for (const org of ['googlers', 'google']) {
try {
await github.rest.orgs.checkMembershipForUser({ org, username: login });
isGooglerCache.set(login, true);
return true;
} catch (e) {
if (e.status !== 404) throw e;
}
}
} catch (e) {
core.warning(`Could not check org membership for ${login}: ${e.message}`);
}
isGooglerCache.set(login, false);
return false;
};
const permissionCache = new Map();
const isPrivilegedUser = async (login) => {
if (maintainerLogins.has(login.toLowerCase())) return true;
if (permissionCache.has(login)) return permissionCache.get(login);
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: login,
});
const privileged = ['admin', 'maintain', 'write', 'triage'].includes(data.permission);
permissionCache.set(login, privileged);
if (privileged) {
core.info(` @${login} is a repo collaborator (${data.permission}) — exempt.`);
return true;
}
} catch (e) {
if (e.status !== 404) {
core.warning(`Could not check permission for ${login}: ${e.message}`);
}
}
const googler = await isGoogler(login);
permissionCache.set(login, googler);
return googler;
};
core.info('Fetching open "help wanted" issues with assignees...');
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: 'help wanted',
per_page: 100,
});
const assignedIssues = issues.filter(
(issue) => !issue.pull_request && issue.assignees && issue.assignees.length > 0
);
core.info(`Found ${assignedIssues.length} assigned "help wanted" issues.`);
let totalUnassigned = 0;
let timelineEvents = [];
try {
timelineEvents = await github.paginate(github.rest.issues.listEventsForTimeline, {
owner,
repo,
issue_number: issue.number,
per_page: 100,
mediaType: { previews: ['mockingbird'] },
});
} catch (err) {
core.warning(`Could not fetch timeline for issue #${issue.number}: ${err.message}`);
continue;
}
const assignedAtMap = new Map();
for (const event of timelineEvents) {
if (event.event === 'assigned' && event.assignee) {
const login = event.assignee.login.toLowerCase();
const at = new Date(event.created_at);
assignedAtMap.set(login, at);
} else if (event.event === 'unassigned' && event.assignee) {
assignedAtMap.delete(event.assignee.login.toLowerCase());
}
}
const linkedPRAuthorSet = new Set();
const seenPRKeys = new Set();
for (const event of timelineEvents) {
if (
event.event !== 'cross-referenced' ||
!event.source ||
event.source.type !== 'pull_request' ||
!event.source.issue ||
!event.source.issue.user ||
!event.source.issue.number ||
!event.source.issue.repository
) continue;
const prOwner = event.source.issue.repository.owner.login;
const prRepo = event.source.issue.repository.name;
const prNumber = event.source.issue.number;
const prAuthor = event.source.issue.user.login.toLowerCase();
const prKey = `${prOwner}/${prRepo}#${prNumber}`;
if (seenPRKeys.has(prKey)) continue;
seenPRKeys.add(prKey);
try {
const { data: pr } = await github.rest.pulls.get({
owner: prOwner,
repo: prRepo,
pull_number: prNumber,
});
const isReady = (pr.state === 'open' && !pr.draft) ||
(pr.state === 'closed' && pr.merged_at !== null);
core.info(
` PR ${prKey} by @${prAuthor}: ` +
`state=${pr.state}, draft=${pr.draft}, merged=${!!pr.merged_at} → ` +
(isReady ? 'qualifies' : 'does NOT qualify (draft or closed without merge)')
);
if (isReady) linkedPRAuthorSet.add(prAuthor);
} catch (err) {
core.warning(`Could not fetch PR ${prKey}: ${err.message}`);
}
}
const assigneesToRemove = [];
for (const assignee of issue.assignees) {
const login = assignee.login.toLowerCase();
if (await isPrivilegedUser(assignee.login)) {
core.info(` @${assignee.login}: privileged user — skipping.`);
continue;
}
const assignedAt = assignedAtMap.get(login);
if (!assignedAt) {
core.warning(
`No 'assigned' event found for @${login} on issue #${issue.number}; ` +
`falling back to issue creation date (${issue.created_at}).`
);
assignedAtMap.set(login, new Date(issue.created_at));
}
const resolvedAssignedAt = assignedAtMap.get(login);
const daysSinceAssignment = (now - resolvedAssignedAt) / (1000 * 60 * 60 * 24);
core.info(
` @${login}: assigned ${daysSinceAssignment.toFixed(1)} day(s) ago, ` +
`ready-for-review PR: ${linkedPRAuthorSet.has(login) ? 'yes' : 'no'}`
);
if (daysSinceAssignment < GRACE_PERIOD_DAYS) {
core.info(` → within grace period, skipping.`);
continue;
}
if (linkedPRAuthorSet.has(login)) {
core.info(` → ready-for-review PR found, keeping assignment.`);
continue;
}
core.info(` → no ready-for-review PR after ${GRACE_PERIOD_DAYS} days, will unassign.`);
assigneesToRemove.push(assignee.login);
}
if (assigneesToRemove.length === 0) {
continue;
}
if (!dryRun) {
try {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issue.number,
assignees: assigneesToRemove,
});
} catch (err) {
core.warning(
`Failed to unassign ${assigneesToRemove.join(', ')} from issue #${issue.number}: ${err.message}`
);
continue;
}
const mentionList = assigneesToRemove.map((l) => `@${l}`).join(', ');
const commentBody =
`👋 ${mentionList} — it has been more than ${GRACE_PERIOD_DAYS} days since ` +
`you were assigned to this issue and we could not find a pull request ` +
`ready for review.\n\n` +
`To keep the backlog moving and ensure issues stay accessible to all ` +
`contributors, we require a PR that is open and ready for review (not a ` +
`draft) within ${GRACE_PERIOD_DAYS} days of assignment.\n\n` +
`We are automatically unassigning you so that other contributors can pick ` +
`this up. If you are still actively working on this, please:\n` +
`1. Re-assign yourself by commenting \`/assign\`.\n` +
`2. Open a PR (not a draft) linked to this issue (e.g. \`Fixes #${issue.number}\`) ` +
`within ${GRACE_PERIOD_DAYS} days so the automation knows real progress is being made.\n\n` +
`Thank you for your contribution — we hope to see a PR from you soon! 🙏`;
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: commentBody,
});
} catch (err) {
core.warning(
`Failed to post comment on issue #${issue.number}: ${err.message}`
);
}
}
totalUnassigned += assigneesToRemove.length;
core.info(
` ${dryRun ? '[DRY RUN] Would have unassigned' : 'Unassigned'}: ${assigneesToRemove.join(', ')}`
);
}
core.info(`\nDone. Total assignees ${dryRun ? 'that would be' : ''} unassigned: ${totalUnassigned}`);
+1 -1
View File
@@ -61,4 +61,4 @@ gemini-debug.log
.genkit
.gemini-clipboard/
.eslintcache
evals/logs/
evals/logs/
-3
View File
@@ -7,9 +7,6 @@
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
+4 -7
View File
@@ -75,14 +75,11 @@ Replace `<PR_NUMBER>` with your pull request number. Authors are encouraged to
run this on their own PRs for self-review, and reviewers should use it to
augment their manual review process.
### Self-assigning and unassigning issues
### Self assigning issues
To assign an issue to yourself, simply add a comment with the text `/assign`. To
unassign yourself from an issue, add a comment with the text `/unassign`.
The comment must contain only that text and nothing else. These commands will
assign or unassign the issue as requested, provided the conditions are met
(e.g., an issue must be unassigned to be assigned).
To assign an issue to yourself, simply add a comment with the text `/assign`.
The comment must contain only that text and nothing else. This command will
assign the issue to you, provided it is not already assigned.
Please note that you can have a maximum of 3 issues assigned to you at any given
time.
+10 -11
View File
@@ -282,14 +282,14 @@ gemini
quickly.
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
auth configuration.
- [**Configuration Guide**](./docs/reference/configuration.md) - Settings and
- [**Configuration Guide**](./docs/get-started/configuration.md) - Settings and
customization.
- [**Keyboard Shortcuts**](./docs/reference/keyboard-shortcuts.md) -
Productivity tips.
- [**Keyboard Shortcuts**](./docs/cli/keyboard-shortcuts.md) - Productivity
tips.
### Core Features
- [**Commands Reference**](./docs/reference/commands.md) - All slash commands
- [**Commands Reference**](./docs/cli/commands.md) - All slash commands
(`/help`, `/chat`, etc).
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
reusable commands.
@@ -301,7 +301,7 @@ gemini
### Tools & Extensions
- [**Built-in Tools Overview**](./docs/reference/tools.md)
- [**Built-in Tools Overview**](./docs/tools/index.md)
- [File System Operations](./docs/tools/file-system.md)
- [Shell Commands](./docs/tools/shell.md)
- [Web Fetch & Search](./docs/tools/web-fetch.md)
@@ -323,15 +323,15 @@ gemini
- [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a
corporate environment.
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
- [**Tools reference**](./docs/reference/tools.md) - Built-in tools overview.
- [**Tools API Development**](./docs/core/tools-api.md) - Create custom tools.
- [**Local development**](./docs/local-development.md) - Local development
tooling.
### Troubleshooting & Support
- [**Troubleshooting Guide**](./docs/resources/troubleshooting.md) - Common
issues and solutions.
- [**FAQ**](./docs/resources/faq.md) - Frequently asked questions.
- [**Troubleshooting Guide**](./docs/troubleshooting.md) - Common issues and
solutions.
- [**FAQ**](./docs/faq.md) - Frequently asked questions.
- Use `/bug` command to report issues directly from the CLI.
### Using MCP Servers
@@ -377,8 +377,7 @@ for planned features and priorities.
### Uninstall
See the [Uninstall Guide](./docs/resources/uninstall.md) for removal
instructions.
See the [Uninstall Guide](docs/cli/uninstall.md) for removal instructions.
## 📄 Legal
+2 -27
View File
@@ -18,30 +18,6 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.32.0 - 2026-03-03
- **Generalist Agent:** The generalist agent is now enabled to improve task
delegation and routing
([#19665](https://github.com/google-gemini/gemini-cli/pull/19665) by
@joshualitt).
- **Model Steering in Workspace:** Added support for model steering directly in
the workspace
([#20343](https://github.com/google-gemini/gemini-cli/pull/20343) by
@joshualitt).
- **Plan Mode Enhancements:** Users can now open and modify plans in an external
editor, and the planning workflow has been adapted to handle complex tasks
more effectively with multi-select options
([#20348](https://github.com/google-gemini/gemini-cli/pull/20348) by @Adib234,
[#20465](https://github.com/google-gemini/gemini-cli/pull/20465) by @jerop).
- **Interactive Shell Autocompletion:** Introduced interactive shell
autocompletion for a more seamless experience
([#20082](https://github.com/google-gemini/gemini-cli/pull/20082) by
@mrpmohiburrahman).
- **Parallel Extension Loading:** Extensions are now loaded in parallel to
improve startup times
([#20229](https://github.com/google-gemini/gemini-cli/pull/20229) by
@scidomino).
## Announcements: v0.31.0 - 2026-02-27
- **Gemini 3.1 Pro Preview:** Gemini CLI now supports the new Gemini 3.1 Pro
@@ -488,9 +464,8 @@ on GitHub.
page in their default browser directly from the CLI using the `/extension`
explore command. ([pr](https://github.com/google-gemini/gemini-cli/pull/11846)
by [@JayadityaGit](https://github.com/JayadityaGit)).
- **Configurable compression:** Users can modify the context compression
threshold in `/settings` (decimal with percentage display). The default has
been made more proactive
- **Configurable compression:** Users can modify the compression threshold in
`/settings`. The default has been made more proactive
([pr](https://github.com/google-gemini/gemini-cli/pull/12317) by
[@scidomino](https://github.com/scidomino)).
- **API key authentication:** Users can now securely enter and store their
+397 -190
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.32.1
# Latest stable release: v0.31.0
Released: March 4, 2026
Released: February 27, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,198 +11,405 @@ npm install -g @google/gemini-cli
## Highlights
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including the
ability to open and modify plans in an external editor, adaptations for
complex tasks with multi-select options, and integration tests for plan mode.
- **Agent and Steering Improvements**: The generalist agent has been enabled to
enhance task delegation, model steering is now supported directly within the
workspace, and contiguous parallel admission is enabled for `Kind.Agent`
tools.
- **Interactive Shell**: Interactive shell autocompletion has been introduced,
significantly enhancing the user experience.
- **Core Stability and Performance**: Extensions are now loaded in parallel,
fetch timeouts have been increased, robust A2A streaming reassembly was
implemented, and orphaned processes when terminal closes have been prevented.
- **Billing and Quota Handling**: Implemented G1 AI credits overage flow with
billing telemetry and added support for quota error fallbacks across all
authentication types.
- **Gemini 3.1 Pro Preview:** Gemini CLI now supports the new Gemini 3.1 Pro
Preview model.
- **Experimental Browser Agent:** We've introduced a new experimental browser
agent to directly interact with web pages and retrieve context.
- **Policy Engine Updates:** The policy engine has been expanded to support
project-level policies, MCP server wildcards, and tool annotation matching,
providing greater control over tool executions.
- **Web Fetch Enhancements:** A new experimental direct web fetch tool has been
implemented, alongside rate-limiting features for enhanced security.
- **Improved Plan Mode:** Plan Mode now includes support for custom storage
directories, automatic model switching, and summarizing work after execution.
## What's Changed
- fix(patch): cherry-pick 0659ad1 to release/v0.32.0-pr-21042 to patch version
v0.32.0 and create version 0.32.1 by @gemini-cli-robot in
[#21048](https://github.com/google-gemini/gemini-cli/pull/21048)
- feat(plan): add integration tests for plan mode by @Adib234 in
[#20214](https://github.com/google-gemini/gemini-cli/pull/20214)
- fix(acp): update auth handshake to spec by @skeshive in
[#19725](https://github.com/google-gemini/gemini-cli/pull/19725)
- feat(core): implement robust A2A streaming reassembly and fix task continuity
by @adamfweidman in
[#20091](https://github.com/google-gemini/gemini-cli/pull/20091)
- feat(cli): load extensions in parallel by @scidomino in
[#20229](https://github.com/google-gemini/gemini-cli/pull/20229)
- Plumb the maxAttempts setting through Config args by @kevinjwang1 in
[#20239](https://github.com/google-gemini/gemini-cli/pull/20239)
- fix(cli): skip 404 errors in setup-github file downloads by @h30s in
[#20287](https://github.com/google-gemini/gemini-cli/pull/20287)
- fix(cli): expose model.name setting in settings dialog for persistence by
@achaljhawar in
[#19605](https://github.com/google-gemini/gemini-cli/pull/19605)
- docs: remove legacy cmd examples in favor of powershell by @scidomino in
[#20323](https://github.com/google-gemini/gemini-cli/pull/20323)
- feat(core): Enable model steering in workspace. by @joshualitt in
[#20343](https://github.com/google-gemini/gemini-cli/pull/20343)
- fix: remove trailing comma in issue triage workflow settings json by @Nixxx19
in [#20265](https://github.com/google-gemini/gemini-cli/pull/20265)
- feat(core): implement task tracker foundation and service by @anj-s in
[#19464](https://github.com/google-gemini/gemini-cli/pull/19464)
- test: support tests that include color information by @jacob314 in
[#20220](https://github.com/google-gemini/gemini-cli/pull/20220)
- feat(core): introduce Kind.Agent for sub-agent classification by @abhipatel12
in [#20369](https://github.com/google-gemini/gemini-cli/pull/20369)
- Changelog for v0.30.0 by @gemini-cli-robot in
[#20252](https://github.com/google-gemini/gemini-cli/pull/20252)
- Update changelog workflow to reject nightly builds by @g-samroberts in
[#20248](https://github.com/google-gemini/gemini-cli/pull/20248)
- Changelog for v0.31.0-preview.0 by @gemini-cli-robot in
[#20249](https://github.com/google-gemini/gemini-cli/pull/20249)
- feat(cli): hide workspace policy update dialog and auto-accept by default by
@Abhijit-2592 in
[#20351](https://github.com/google-gemini/gemini-cli/pull/20351)
- feat(core): rename grep_search include parameter to include_pattern by
- Use ranged reads and limited searches and fuzzy editing improvements by
@gundermanc in
[#19240](https://github.com/google-gemini/gemini-cli/pull/19240)
- Fix bottom border color by @jacob314 in
[#19266](https://github.com/google-gemini/gemini-cli/pull/19266)
- Release note generator fix by @g-samroberts in
[#19363](https://github.com/google-gemini/gemini-cli/pull/19363)
- test(evals): add behavioral tests for tool output masking by @NTaylorMullen in
[#19172](https://github.com/google-gemini/gemini-cli/pull/19172)
- docs: clarify preflight instructions in GEMINI.md by @NTaylorMullen in
[#19377](https://github.com/google-gemini/gemini-cli/pull/19377)
- feat(cli): add gemini --resume hint on exit by @Mag1ck in
[#16285](https://github.com/google-gemini/gemini-cli/pull/16285)
- fix: optimize height calculations for ask_user dialog by @jackwotherspoon in
[#19017](https://github.com/google-gemini/gemini-cli/pull/19017)
- feat(cli): add Alt+D for forward word deletion by @scidomino in
[#19300](https://github.com/google-gemini/gemini-cli/pull/19300)
- Disable failing eval test by @chrstnb in
[#19455](https://github.com/google-gemini/gemini-cli/pull/19455)
- fix(cli): support legacy onConfirm callback in ToolActionsContext by
@SandyTao520 in
[#20328](https://github.com/google-gemini/gemini-cli/pull/20328)
- feat(plan): support opening and modifying plan in external editor by @Adib234
in [#20348](https://github.com/google-gemini/gemini-cli/pull/20348)
- feat(cli): implement interactive shell autocompletion by @mrpmohiburrahman in
[#20082](https://github.com/google-gemini/gemini-cli/pull/20082)
- fix(core): allow /memory add to work in plan mode by @Jefftree in
[#20353](https://github.com/google-gemini/gemini-cli/pull/20353)
- feat(core): add HTTP 499 to retryable errors and map to RetryableQuotaError by
@bdmorgan in [#20432](https://github.com/google-gemini/gemini-cli/pull/20432)
- feat(core): Enable generalist agent by @joshualitt in
[#19665](https://github.com/google-gemini/gemini-cli/pull/19665)
- Updated tests in TableRenderer.test.tsx to use SVG snapshots by @devr0306 in
[#20450](https://github.com/google-gemini/gemini-cli/pull/20450)
- Refactor Github Action per b/485167538 by @google-admin in
[#19443](https://github.com/google-gemini/gemini-cli/pull/19443)
- fix(github): resolve actionlint and yamllint regressions from #19443 by @jerop
in [#20467](https://github.com/google-gemini/gemini-cli/pull/20467)
- fix: action var usage by @galz10 in
[#20492](https://github.com/google-gemini/gemini-cli/pull/20492)
- feat(core): improve A2A content extraction by @adamfweidman in
[#20487](https://github.com/google-gemini/gemini-cli/pull/20487)
- fix(cli): support quota error fallbacks for all authentication types by
@sehoon38 in [#20475](https://github.com/google-gemini/gemini-cli/pull/20475)
- fix(core): flush transcript for pure tool-call responses to ensure BeforeTool
hooks see complete state by @krishdef7 in
[#20419](https://github.com/google-gemini/gemini-cli/pull/20419)
- feat(plan): adapt planning workflow based on complexity of task by @jerop in
[#20465](https://github.com/google-gemini/gemini-cli/pull/20465)
- fix: prevent orphaned processes from consuming 100% CPU when terminal closes
by @yuvrajangadsingh in
[#16965](https://github.com/google-gemini/gemini-cli/pull/16965)
- feat(core): increase fetch timeout and fix [object Object] error
stringification by @bdmorgan in
[#20441](https://github.com/google-gemini/gemini-cli/pull/20441)
- [Gemma x Gemini CLI] Add an Experimental Gemma Router that uses a LiteRT-LM
shim into the Composite Model Classifier Strategy by @sidwan02 in
[#17231](https://github.com/google-gemini/gemini-cli/pull/17231)
- docs(plan): update documentation regarding supporting editing of plan files
during plan approval by @Adib234 in
[#20452](https://github.com/google-gemini/gemini-cli/pull/20452)
- test(cli): fix flaky ToolResultDisplay overflow test by @jwhelangoog in
[#20518](https://github.com/google-gemini/gemini-cli/pull/20518)
- ui(cli): reduce length of Ctrl+O hint by @jwhelangoog in
[#20490](https://github.com/google-gemini/gemini-cli/pull/20490)
- fix(ui): correct styled table width calculations by @devr0306 in
[#20042](https://github.com/google-gemini/gemini-cli/pull/20042)
- Avoid overaggressive unescaping by @scidomino in
[#20520](https://github.com/google-gemini/gemini-cli/pull/20520)
- feat(telemetry) Instrument traces with more attributes and make them available
to OTEL users by @heaventourist in
[#20237](https://github.com/google-gemini/gemini-cli/pull/20237)
- Add support for policy engine in extensions by @chrstnb in
[#20049](https://github.com/google-gemini/gemini-cli/pull/20049)
- Docs: Update to Terms of Service & FAQ by @jkcinouye in
[#20488](https://github.com/google-gemini/gemini-cli/pull/20488)
- Fix bottom border rendering for search and add a regression test. by @jacob314
in [#20517](https://github.com/google-gemini/gemini-cli/pull/20517)
- fix(core): apply retry logic to CodeAssistServer for all users by @bdmorgan in
[#20507](https://github.com/google-gemini/gemini-cli/pull/20507)
- Fix extension MCP server env var loading by @chrstnb in
[#20374](https://github.com/google-gemini/gemini-cli/pull/20374)
- feat(ui): add 'ctrl+o' hint to truncated content message by @jerop in
[#20529](https://github.com/google-gemini/gemini-cli/pull/20529)
- Fix flicker showing message to press ctrl-O again to collapse. by @jacob314 in
[#20414](https://github.com/google-gemini/gemini-cli/pull/20414)
- fix(cli): hide shortcuts hint while model is thinking or the user has typed a
prompt + add debounce to avoid flicker by @jacob314 in
[#19389](https://github.com/google-gemini/gemini-cli/pull/19389)
- feat(plan): update planning workflow to encourage multi-select with
descriptions of options by @Adib234 in
[#20491](https://github.com/google-gemini/gemini-cli/pull/20491)
- refactor(core,cli): useAlternateBuffer read from config by @psinha40898 in
[#20346](https://github.com/google-gemini/gemini-cli/pull/20346)
- fix(cli): ensure dialogs stay scrolled to bottom in alternate buffer mode by
@jacob314 in [#20527](https://github.com/google-gemini/gemini-cli/pull/20527)
- fix(core): revert auto-save of policies to user space by @Abhijit-2592 in
[#20531](https://github.com/google-gemini/gemini-cli/pull/20531)
- Demote unreliable test. by @gundermanc in
[#20571](https://github.com/google-gemini/gemini-cli/pull/20571)
- fix(core): handle optional response fields from code assist API by @sehoon38
in [#20345](https://github.com/google-gemini/gemini-cli/pull/20345)
- fix(cli): keep thought summary when loading phrases are off by @LyalinDotCom
in [#20497](https://github.com/google-gemini/gemini-cli/pull/20497)
- feat(cli): add temporary flag to disable workspace policies by @Abhijit-2592
in [#20523](https://github.com/google-gemini/gemini-cli/pull/20523)
- Disable expensive and scheduled workflows on personal forks by @dewitt in
[#20449](https://github.com/google-gemini/gemini-cli/pull/20449)
- Moved markdown parsing logic to a separate util file by @devr0306 in
[#20526](https://github.com/google-gemini/gemini-cli/pull/20526)
- fix(plan): prevent agent from using ask_user for shell command confirmation by
@Adib234 in [#20504](https://github.com/google-gemini/gemini-cli/pull/20504)
- fix(core): disable retries for code assist streaming requests by @sehoon38 in
[#20561](https://github.com/google-gemini/gemini-cli/pull/20561)
- feat(billing): implement G1 AI credits overage flow with billing telemetry by
@gsquared94 in
[#18590](https://github.com/google-gemini/gemini-cli/pull/18590)
- feat: better error messages by @gsquared94 in
[#20577](https://github.com/google-gemini/gemini-cli/pull/20577)
- fix(ui): persist expansion in AskUser dialog when navigating options by @jerop
in [#20559](https://github.com/google-gemini/gemini-cli/pull/20559)
- fix(cli): prevent sub-agent tool calls from leaking into UI by @abhipatel12 in
[#20580](https://github.com/google-gemini/gemini-cli/pull/20580)
- fix(cli): Shell autocomplete polish by @jacob314 in
[#20411](https://github.com/google-gemini/gemini-cli/pull/20411)
- Changelog for v0.31.0-preview.1 by @gemini-cli-robot in
[#20590](https://github.com/google-gemini/gemini-cli/pull/20590)
- Add slash command for promoting behavioral evals to CI blocking by @gundermanc
in [#20575](https://github.com/google-gemini/gemini-cli/pull/20575)
- Changelog for v0.30.1 by @gemini-cli-robot in
[#20589](https://github.com/google-gemini/gemini-cli/pull/20589)
- Add low/full CLI error verbosity mode for cleaner UI by @LyalinDotCom in
[#20399](https://github.com/google-gemini/gemini-cli/pull/20399)
- Disable Gemini PR reviews on draft PRs. by @gundermanc in
[#20362](https://github.com/google-gemini/gemini-cli/pull/20362)
- Docs: FAQ update by @jkcinouye in
[#20585](https://github.com/google-gemini/gemini-cli/pull/20585)
- fix(core): reduce intrusive MCP errors and deduplicate diagnostics by
[#19369](https://github.com/google-gemini/gemini-cli/pull/19369)
- chore(deps): bump tar from 7.5.7 to 7.5.8 by @.github/dependabot.yml[bot] in
[#19367](https://github.com/google-gemini/gemini-cli/pull/19367)
- fix(plan): allow safe fallback when experiment setting for plan is not enabled
but approval mode at startup is plan by @Adib234 in
[#19439](https://github.com/google-gemini/gemini-cli/pull/19439)
- Add explicit color-convert dependency by @chrstnb in
[#19460](https://github.com/google-gemini/gemini-cli/pull/19460)
- feat(devtools): migrate devtools package into monorepo by @SandyTao520 in
[#18936](https://github.com/google-gemini/gemini-cli/pull/18936)
- fix(core): clarify plan mode constraints and exit mechanism by @jerop in
[#19438](https://github.com/google-gemini/gemini-cli/pull/19438)
- feat(cli): add macOS run-event notifications (interactive only) by
@LyalinDotCom in
[#19056](https://github.com/google-gemini/gemini-cli/pull/19056)
- Changelog for v0.29.0 by @gemini-cli-robot in
[#19361](https://github.com/google-gemini/gemini-cli/pull/19361)
- fix(ui): preventing empty history items from being added by @devr0306 in
[#19014](https://github.com/google-gemini/gemini-cli/pull/19014)
- Changelog for v0.30.0-preview.0 by @gemini-cli-robot in
[#19364](https://github.com/google-gemini/gemini-cli/pull/19364)
- feat(core): add support for MCP progress updates by @NTaylorMullen in
[#19046](https://github.com/google-gemini/gemini-cli/pull/19046)
- fix(core): ensure directory exists before writing conversation file by
@godwiniheuwa in
[#18429](https://github.com/google-gemini/gemini-cli/pull/18429)
- fix(ui): move margin from top to bottom in ToolGroupMessage by @imadraude in
[#17198](https://github.com/google-gemini/gemini-cli/pull/17198)
- fix(cli): treat unknown slash commands as regular input instead of showing
error by @skyvanguard in
[#17393](https://github.com/google-gemini/gemini-cli/pull/17393)
- feat(core): experimental in-progress steering hints (2 of 2) by @joshualitt in
[#19307](https://github.com/google-gemini/gemini-cli/pull/19307)
- docs(plan): add documentation for plan mode command by @Adib234 in
[#19467](https://github.com/google-gemini/gemini-cli/pull/19467)
- fix(core): ripgrep fails when pattern looks like ripgrep flag by @syvb in
[#18858](https://github.com/google-gemini/gemini-cli/pull/18858)
- fix(cli): disable auto-completion on Shift+Tab to preserve mode cycling by
@NTaylorMullen in
[#19451](https://github.com/google-gemini/gemini-cli/pull/19451)
- use issuer instead of authorization_endpoint for oauth discovery by
@garrettsparks in
[#17332](https://github.com/google-gemini/gemini-cli/pull/17332)
- feat(cli): include `/dir add` directories in @ autocomplete suggestions by
@jasmeetsb in [#19246](https://github.com/google-gemini/gemini-cli/pull/19246)
- feat(admin): Admin settings should only apply if adminControlsApplicable =
true and fetch errors should be fatal by @skeshive in
[#19453](https://github.com/google-gemini/gemini-cli/pull/19453)
- Format strict-development-rules command by @g-samroberts in
[#19484](https://github.com/google-gemini/gemini-cli/pull/19484)
- feat(core): centralize compatibility checks and add TrueColor detection by
@spencer426 in
[#20232](https://github.com/google-gemini/gemini-cli/pull/20232)
- docs: fix spelling typos in installation guide by @campox747 in
[#20579](https://github.com/google-gemini/gemini-cli/pull/20579)
- Promote stable tests to CI blocking. by @gundermanc in
[#20581](https://github.com/google-gemini/gemini-cli/pull/20581)
- feat(core): enable contiguous parallel admission for Kind.Agent tools by
@abhipatel12 in
[#20583](https://github.com/google-gemini/gemini-cli/pull/20583)
- Enforce import/no-duplicates as error by @Nixxx19 in
[#19797](https://github.com/google-gemini/gemini-cli/pull/19797)
- fix: merge duplicate imports in sdk and test-utils packages (1/4) by @Nixxx19
in [#19777](https://github.com/google-gemini/gemini-cli/pull/19777)
- fix: merge duplicate imports in a2a-server package (2/4) by @Nixxx19 in
[#19781](https://github.com/google-gemini/gemini-cli/pull/19781)
[#19478](https://github.com/google-gemini/gemini-cli/pull/19478)
- Remove unused files and update index and sidebar. by @g-samroberts in
[#19479](https://github.com/google-gemini/gemini-cli/pull/19479)
- Migrate core render util to use xterm.js as part of the rendering loop. by
@jacob314 in [#19044](https://github.com/google-gemini/gemini-cli/pull/19044)
- Changelog for v0.30.0-preview.1 by @gemini-cli-robot in
[#19496](https://github.com/google-gemini/gemini-cli/pull/19496)
- build: replace deprecated built-in punycode with userland package by @jacob314
in [#19502](https://github.com/google-gemini/gemini-cli/pull/19502)
- Speculative fixes to try to fix react error. by @jacob314 in
[#19508](https://github.com/google-gemini/gemini-cli/pull/19508)
- fix spacing by @jacob314 in
[#19494](https://github.com/google-gemini/gemini-cli/pull/19494)
- fix(core): ensure user rejections update tool outcome for telemetry by
@abhiasap in [#18982](https://github.com/google-gemini/gemini-cli/pull/18982)
- fix(acp): Initialize config (#18897) by @Mervap in
[#18898](https://github.com/google-gemini/gemini-cli/pull/18898)
- fix(core): add error logging for IDE fetch failures by @yuvrajangadsingh in
[#17981](https://github.com/google-gemini/gemini-cli/pull/17981)
- feat(acp): support set_mode interface (#18890) by @Mervap in
[#18891](https://github.com/google-gemini/gemini-cli/pull/18891)
- fix(core): robust workspace-based IDE connection discovery by @ehedlund in
[#18443](https://github.com/google-gemini/gemini-cli/pull/18443)
- Deflake windows tests. by @jacob314 in
[#19511](https://github.com/google-gemini/gemini-cli/pull/19511)
- Fix: Avoid tool confirmation timeout when no UI listeners are present by
@pdHaku0 in [#17955](https://github.com/google-gemini/gemini-cli/pull/17955)
- format md file by @scidomino in
[#19474](https://github.com/google-gemini/gemini-cli/pull/19474)
- feat(cli): add experimental.useOSC52Copy setting by @scidomino in
[#19488](https://github.com/google-gemini/gemini-cli/pull/19488)
- feat(cli): replace loading phrases boolean with enum setting by @LyalinDotCom
in [#19347](https://github.com/google-gemini/gemini-cli/pull/19347)
- Update skill to adjust for generated results. by @g-samroberts in
[#19500](https://github.com/google-gemini/gemini-cli/pull/19500)
- Fix message too large issue. by @gundermanc in
[#19499](https://github.com/google-gemini/gemini-cli/pull/19499)
- fix(core): prevent duplicate tool approval entries in auto-saved.toml by
@Abhijit-2592 in
[#19487](https://github.com/google-gemini/gemini-cli/pull/19487)
- fix(core): resolve crash in ClearcutLogger when os.cpus() is empty by @Adib234
in [#19555](https://github.com/google-gemini/gemini-cli/pull/19555)
- chore(core): improve encapsulation and remove unused exports by @adamfweidman
in [#19556](https://github.com/google-gemini/gemini-cli/pull/19556)
- Revert "Add generic searchable list to back settings and extensions (… by
@chrstnb in [#19434](https://github.com/google-gemini/gemini-cli/pull/19434)
- fix(core): improve error type extraction for telemetry by @yunaseoul in
[#19565](https://github.com/google-gemini/gemini-cli/pull/19565)
- fix: remove extra padding in Composer by @jackwotherspoon in
[#19529](https://github.com/google-gemini/gemini-cli/pull/19529)
- feat(plan): support configuring custom plans storage directory by @jerop in
[#19577](https://github.com/google-gemini/gemini-cli/pull/19577)
- Migrate files to resource or references folder. by @g-samroberts in
[#19503](https://github.com/google-gemini/gemini-cli/pull/19503)
- feat(policy): implement project-level policy support by @Abhijit-2592 in
[#18682](https://github.com/google-gemini/gemini-cli/pull/18682)
- feat(core): Implement parallel FC for read only tools. by @joshualitt in
[#18791](https://github.com/google-gemini/gemini-cli/pull/18791)
- chore(skills): adds pr-address-comments skill to work on PR feedback by
@mbleigh in [#19576](https://github.com/google-gemini/gemini-cli/pull/19576)
- refactor(sdk): introduce session-based architecture by @mbleigh in
[#19180](https://github.com/google-gemini/gemini-cli/pull/19180)
- fix(ci): add fallback JSON extraction to issue triage workflow by @bdmorgan in
[#19593](https://github.com/google-gemini/gemini-cli/pull/19593)
- feat(core): refine Edit and WriteFile tool schemas for Gemini 3 by
@SandyTao520 in
[#19476](https://github.com/google-gemini/gemini-cli/pull/19476)
- Changelog for v0.30.0-preview.3 by @gemini-cli-robot in
[#19585](https://github.com/google-gemini/gemini-cli/pull/19585)
- fix(plan): exclude EnterPlanMode tool from YOLO mode by @Adib234 in
[#19570](https://github.com/google-gemini/gemini-cli/pull/19570)
- chore: resolve build warnings and update dependencies by @mattKorwel in
[#18880](https://github.com/google-gemini/gemini-cli/pull/18880)
- feat(ui): add source indicators to slash commands by @ehedlund in
[#18839](https://github.com/google-gemini/gemini-cli/pull/18839)
- docs: refine Plan Mode documentation structure and workflow by @jerop in
[#19644](https://github.com/google-gemini/gemini-cli/pull/19644)
- Docs: Update release information regarding Gemini 3.1 by @jkcinouye in
[#19568](https://github.com/google-gemini/gemini-cli/pull/19568)
- fix(security): rate limit web_fetch tool to mitigate DDoS via prompt injection
by @mattKorwel in
[#19567](https://github.com/google-gemini/gemini-cli/pull/19567)
- Add initial implementation of /extensions explore command by @chrstnb in
[#19029](https://github.com/google-gemini/gemini-cli/pull/19029)
- fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) by
@maximus12793 in
[#19038](https://github.com/google-gemini/gemini-cli/pull/19038)
- Search updates by @alisa-alisa in
[#19482](https://github.com/google-gemini/gemini-cli/pull/19482)
- feat(cli): add support for numpad SS3 sequences by @scidomino in
[#19659](https://github.com/google-gemini/gemini-cli/pull/19659)
- feat(cli): enhance folder trust with configuration discovery and security
warnings by @galz10 in
[#19492](https://github.com/google-gemini/gemini-cli/pull/19492)
- feat(ui): improve startup warnings UX with dismissal and show-count limits by
@spencer426 in
[#19584](https://github.com/google-gemini/gemini-cli/pull/19584)
- feat(a2a): Add API key authentication provider by @adamfweidman in
[#19548](https://github.com/google-gemini/gemini-cli/pull/19548)
- Send accepted/removed lines with ACCEPT_FILE telemetry. by @gundermanc in
[#19670](https://github.com/google-gemini/gemini-cli/pull/19670)
- feat(models): support Gemini 3.1 Pro Preview and fixes by @sehoon38 in
[#19676](https://github.com/google-gemini/gemini-cli/pull/19676)
- feat(plan): enforce read-only constraints in Plan Mode by @mattKorwel in
[#19433](https://github.com/google-gemini/gemini-cli/pull/19433)
- fix(cli): allow perfect match @scripts/test-windows-paths.js completions to
submit on Enter by @spencer426 in
[#19562](https://github.com/google-gemini/gemini-cli/pull/19562)
- fix(core): treat 503 Service Unavailable as retryable quota error by @sehoon38
in [#19642](https://github.com/google-gemini/gemini-cli/pull/19642)
- Update sidebar.json for to allow top nav tabs. by @g-samroberts in
[#19595](https://github.com/google-gemini/gemini-cli/pull/19595)
- security: strip deceptive Unicode characters from terminal output by @ehedlund
in [#19026](https://github.com/google-gemini/gemini-cli/pull/19026)
- Fixes 'input.on' is not a function error in Gemini CLI by @gundermanc in
[#19691](https://github.com/google-gemini/gemini-cli/pull/19691)
- Revert "feat(ui): add source indicators to slash commands" by @ehedlund in
[#19695](https://github.com/google-gemini/gemini-cli/pull/19695)
- security: implement deceptive URL detection and disclosure in tool
confirmations by @ehedlund in
[#19288](https://github.com/google-gemini/gemini-cli/pull/19288)
- fix(core): restore auth consent in headless mode and add unit tests by
@ehedlund in [#19689](https://github.com/google-gemini/gemini-cli/pull/19689)
- Fix unsafe assertions in code_assist folder. by @gundermanc in
[#19706](https://github.com/google-gemini/gemini-cli/pull/19706)
- feat(cli): make JetBrains warning more specific by @jacob314 in
[#19687](https://github.com/google-gemini/gemini-cli/pull/19687)
- fix(cli): extensions dialog UX polish by @jacob314 in
[#19685](https://github.com/google-gemini/gemini-cli/pull/19685)
- fix(cli): use getDisplayString for manual model selection in dialog by
@sehoon38 in [#19726](https://github.com/google-gemini/gemini-cli/pull/19726)
- feat(policy): repurpose "Always Allow" persistence to workspace level by
@Abhijit-2592 in
[#19707](https://github.com/google-gemini/gemini-cli/pull/19707)
- fix(cli): re-enable CLI banner by @sehoon38 in
[#19741](https://github.com/google-gemini/gemini-cli/pull/19741)
- Disallow and suppress unsafe assignment by @gundermanc in
[#19736](https://github.com/google-gemini/gemini-cli/pull/19736)
- feat(core): migrate read_file to 1-based start_line/end_line parameters by
@adamfweidman in
[#19526](https://github.com/google-gemini/gemini-cli/pull/19526)
- feat(cli): improve CTRL+O experience for both standard and alternate screen
buffer (ASB) modes by @jwhelangoog in
[#19010](https://github.com/google-gemini/gemini-cli/pull/19010)
- Utilize pipelining of grep_search -> read_file to eliminate turns by
@gundermanc in
[#19574](https://github.com/google-gemini/gemini-cli/pull/19574)
- refactor(core): remove unsafe type assertions in error utils (Phase 1.1) by
@mattKorwel in
[#19750](https://github.com/google-gemini/gemini-cli/pull/19750)
- Disallow unsafe returns. by @gundermanc in
[#19767](https://github.com/google-gemini/gemini-cli/pull/19767)
- fix(cli): filter subagent sessions from resume history by @abhipatel12 in
[#19698](https://github.com/google-gemini/gemini-cli/pull/19698)
- chore(lint): fix lint errors seen when running npm run lint by @abhipatel12 in
[#19844](https://github.com/google-gemini/gemini-cli/pull/19844)
- feat(core): remove unnecessary login verbiage from Code Assist auth by
@NTaylorMullen in
[#19861](https://github.com/google-gemini/gemini-cli/pull/19861)
- fix(plan): time share by approval mode dashboard reporting negative time
shares by @Adib234 in
[#19847](https://github.com/google-gemini/gemini-cli/pull/19847)
- fix(core): allow any preview model in quota access check by @bdmorgan in
[#19867](https://github.com/google-gemini/gemini-cli/pull/19867)
- fix(core): prevent omission placeholder deletions in replace/write_file by
@nsalerni in [#19870](https://github.com/google-gemini/gemini-cli/pull/19870)
- fix(core): add uniqueness guard to edit tool by @Shivangisharma4 in
[#19890](https://github.com/google-gemini/gemini-cli/pull/19890)
- refactor(config): remove enablePromptCompletion from settings by @sehoon38 in
[#19974](https://github.com/google-gemini/gemini-cli/pull/19974)
- refactor(core): move session conversion logic to core by @abhipatel12 in
[#19972](https://github.com/google-gemini/gemini-cli/pull/19972)
- Fix: Persist manual model selection on restart #19864 by @Nixxx19 in
[#19891](https://github.com/google-gemini/gemini-cli/pull/19891)
- fix(core): increase default retry attempts and add quota error backoff by
@sehoon38 in [#19949](https://github.com/google-gemini/gemini-cli/pull/19949)
- feat(core): add policy chain support for Gemini 3.1 by @sehoon38 in
[#19991](https://github.com/google-gemini/gemini-cli/pull/19991)
- Updates command reference and /stats command. by @g-samroberts in
[#19794](https://github.com/google-gemini/gemini-cli/pull/19794)
- Fix for silent failures in non-interactive mode by @owenofbrien in
[#19905](https://github.com/google-gemini/gemini-cli/pull/19905)
- fix(plan): allow plan mode writes on Windows and fix prompt paths by @Adib234
in [#19658](https://github.com/google-gemini/gemini-cli/pull/19658)
- fix(core): prevent OAuth server crash on unexpected requests by @reyyanxahmed
in [#19668](https://github.com/google-gemini/gemini-cli/pull/19668)
- feat: Map tool kinds to explicit ACP.ToolKind values and update test … by
@sripasg in [#19547](https://github.com/google-gemini/gemini-cli/pull/19547)
- chore: restrict gemini-automted-issue-triage to only allow echo by @galz10 in
[#20047](https://github.com/google-gemini/gemini-cli/pull/20047)
- Allow ask headers longer than 16 chars by @scidomino in
[#20041](https://github.com/google-gemini/gemini-cli/pull/20041)
- fix(core): prevent state corruption in McpClientManager during collis by @h30s
in [#19782](https://github.com/google-gemini/gemini-cli/pull/19782)
- fix(bundling): copy devtools package to bundle for runtime resolution by
@SandyTao520 in
[#19766](https://github.com/google-gemini/gemini-cli/pull/19766)
- feat(policy): Support MCP Server Wildcards in Policy Engine by @jerop in
[#20024](https://github.com/google-gemini/gemini-cli/pull/20024)
- docs(CONTRIBUTING): update React DevTools version to 6 by @mmgok in
[#20014](https://github.com/google-gemini/gemini-cli/pull/20014)
- feat(core): optimize tool descriptions and schemas for Gemini 3 by
@aishaneeshah in
[#19643](https://github.com/google-gemini/gemini-cli/pull/19643)
- feat(core): implement experimental direct web fetch by @mbleigh in
[#19557](https://github.com/google-gemini/gemini-cli/pull/19557)
- feat(core): replace expected_replacements with allow_multiple in replace tool
by @SandyTao520 in
[#20033](https://github.com/google-gemini/gemini-cli/pull/20033)
- fix(sandbox): harden image packaging integrity checks by @aviralgarg05 in
[#19552](https://github.com/google-gemini/gemini-cli/pull/19552)
- fix(core): allow environment variable expansion and explicit overrides for MCP
servers by @galz10 in
[#18837](https://github.com/google-gemini/gemini-cli/pull/18837)
- feat(policy): Implement Tool Annotation Matching in Policy Engine by @jerop in
[#20029](https://github.com/google-gemini/gemini-cli/pull/20029)
- fix(core): prevent utility calls from changing session active model by
@adamfweidman in
[#20035](https://github.com/google-gemini/gemini-cli/pull/20035)
- fix(cli): skip workspace policy loading when in home directory by
@Abhijit-2592 in
[#20054](https://github.com/google-gemini/gemini-cli/pull/20054)
- fix(scripts): Add Windows (win32/x64) support to lint.js by @ZafeerMahmood in
[#16193](https://github.com/google-gemini/gemini-cli/pull/16193)
- fix(a2a-server): Remove unsafe type assertions in agent by @Nixxx19 in
[#19723](https://github.com/google-gemini/gemini-cli/pull/19723)
- Fix: Handle corrupted token file gracefully when switching auth types (#19845)
by @Nixxx19 in
[#19850](https://github.com/google-gemini/gemini-cli/pull/19850)
- fix critical dep vulnerability by @scidomino in
[#20087](https://github.com/google-gemini/gemini-cli/pull/20087)
- Add new setting to configure maxRetries by @kevinjwang1 in
[#20064](https://github.com/google-gemini/gemini-cli/pull/20064)
- Stabilize tests. by @gundermanc in
[#20095](https://github.com/google-gemini/gemini-cli/pull/20095)
- make windows tests mandatory by @scidomino in
[#20096](https://github.com/google-gemini/gemini-cli/pull/20096)
- Add 3.1 pro preview to behavioral evals. by @gundermanc in
[#20088](https://github.com/google-gemini/gemini-cli/pull/20088)
- feat:PR-rate-limit by @JagjeevanAK in
[#19804](https://github.com/google-gemini/gemini-cli/pull/19804)
- feat(cli): allow expanding full details of MCP tool on approval by @y-okt in
[#19916](https://github.com/google-gemini/gemini-cli/pull/19916)
- feat(security): Introduce Conseca framework by @shrishabh in
[#13193](https://github.com/google-gemini/gemini-cli/pull/13193)
- fix(cli): Remove unsafe type assertions in activityLogger #19713 by @Nixxx19
in [#19745](https://github.com/google-gemini/gemini-cli/pull/19745)
- feat: implement AfterTool tail tool calls by @googlestrobe in
[#18486](https://github.com/google-gemini/gemini-cli/pull/18486)
- ci(actions): fix PR rate limiter excluding maintainers by @scidomino in
[#20117](https://github.com/google-gemini/gemini-cli/pull/20117)
- Shortcuts: Move SectionHeader title below top line and refine styling by
@keithguerin in
[#18721](https://github.com/google-gemini/gemini-cli/pull/18721)
- refactor(ui): Update and simplify use of gray colors in themes by @keithguerin
in [#20141](https://github.com/google-gemini/gemini-cli/pull/20141)
- fix punycode2 by @jacob314 in
[#20154](https://github.com/google-gemini/gemini-cli/pull/20154)
- feat(ide): add GEMINI_CLI_IDE_PID env var to override IDE process detection by
@kiryltech in [#15842](https://github.com/google-gemini/gemini-cli/pull/15842)
- feat(policy): Propagate Tool Annotations for MCP Servers by @jerop in
[#20083](https://github.com/google-gemini/gemini-cli/pull/20083)
- fix(a2a-server): pass allowedTools settings to core Config by @reyyanxahmed in
[#19680](https://github.com/google-gemini/gemini-cli/pull/19680)
- feat(mcp): add progress bar, throttling, and input validation for MCP tool
progress by @jasmeetsb in
[#19772](https://github.com/google-gemini/gemini-cli/pull/19772)
- feat(policy): centralize plan mode tool visibility in policy engine by @jerop
in [#20178](https://github.com/google-gemini/gemini-cli/pull/20178)
- feat(browser): implement experimental browser agent by @gsquared94 in
[#19284](https://github.com/google-gemini/gemini-cli/pull/19284)
- feat(plan): summarize work after executing a plan by @jerop in
[#19432](https://github.com/google-gemini/gemini-cli/pull/19432)
- fix(core): create new McpClient on restart to apply updated config by @h30s in
[#20126](https://github.com/google-gemini/gemini-cli/pull/20126)
- Changelog for v0.30.0-preview.5 by @gemini-cli-robot in
[#20107](https://github.com/google-gemini/gemini-cli/pull/20107)
- Update packages. by @jacob314 in
[#20152](https://github.com/google-gemini/gemini-cli/pull/20152)
- Fix extension env dir loading issue by @chrstnb in
[#20198](https://github.com/google-gemini/gemini-cli/pull/20198)
- restrict /assign to help-wanted issues by @scidomino in
[#20207](https://github.com/google-gemini/gemini-cli/pull/20207)
- feat(plan): inject message when user manually exits Plan mode by @jerop in
[#20203](https://github.com/google-gemini/gemini-cli/pull/20203)
- feat(extensions): enforce folder trust for local extension install by @galz10
in [#19703](https://github.com/google-gemini/gemini-cli/pull/19703)
- feat(hooks): adds support for RuntimeHook functions. by @mbleigh in
[#19598](https://github.com/google-gemini/gemini-cli/pull/19598)
- Docs: Update UI links. by @jkcinouye in
[#20224](https://github.com/google-gemini/gemini-cli/pull/20224)
- feat: prompt users to run /terminal-setup with yes/no by @ishaanxgupta in
[#16235](https://github.com/google-gemini/gemini-cli/pull/16235)
- fix: additional high vulnerabilities (minimatch, cross-spawn) by @adamfweidman
in [#20221](https://github.com/google-gemini/gemini-cli/pull/20221)
- feat(telemetry): Add context breakdown to API response event by @SandyTao520
in [#19699](https://github.com/google-gemini/gemini-cli/pull/19699)
- Docs: Add nested sub-folders for related topics by @g-samroberts in
[#20235](https://github.com/google-gemini/gemini-cli/pull/20235)
- feat(plan): support automatic model switching for Plan Mode by @jerop in
[#20240](https://github.com/google-gemini/gemini-cli/pull/20240)
- fix(patch): cherry-pick 58df1c6 to release/v0.31.0-preview.0-pr-20374 to patch
version v0.31.0-preview.0 and create version 0.31.0-preview.1 by
@gemini-cli-robot in
[#20568](https://github.com/google-gemini/gemini-cli/pull/20568)
- fix(patch): cherry-pick ea48bd9 to release/v0.31.0-preview.1-pr-20577
[CONFLICTS] by @gemini-cli-robot in
[#20592](https://github.com/google-gemini/gemini-cli/pull/20592)
- fix(patch): cherry-pick 32e777f to release/v0.31.0-preview.2-pr-20531 to patch
version v0.31.0-preview.2 and create version 0.31.0-preview.3 by
@gemini-cli-robot in
[#20607](https://github.com/google-gemini/gemini-cli/pull/20607)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.31.0...v0.32.1
https://github.com/google-gemini/gemini-cli/compare/v0.30.1...v0.31.0
+189 -168
View File
@@ -1,6 +1,6 @@
# Preview release: v0.33.0-preview.1
# Preview release: v0.32.0-preview.0
Released: March 04, 2026
Released: February 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,175 +13,196 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Plan Mode Enhancements**: Added support for annotating plans with feedback
for iteration, enabling built-in research subagents in plan mode, and a new
`copy` subcommand.
- **Agent and Skill Improvements**: Introduced the new `github-issue-creator`
skill, implemented HTTP authentication support for A2A remote agents, and
added support for authenticated A2A agent card discovery.
- **CLI UX/UI Updates**: Redesigned the header to be compact with an ASCII icon,
inverted the context window display to show usage, and directly indicate auth
required state for agents.
- **Core and ACP Enhancements**: Implemented slash command handling in ACP (for
`/memory`, `/init`, `/extensions`, and `/restore`), added a set models
interface to ACP, and centralized `read_file` limits while truncating large
MCP tool output.
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including
support for modifying plans in external editors, adaptive workflows based on
task complexity, and new integration tests.
- **Agent and Core Engine Updates**: Enabled the generalist agent, introduced
`Kind.Agent` for sub-agent classification, implemented task tracking
foundation, and improved Agent-to-Agent (A2A) streaming and content
extraction.
- **CLI & User Experience**: Introduced interactive shell autocompletion, added
a new verbosity mode for cleaner error reporting, enabled parallel loading of
extensions, and improved UI hints and shortcut handling.
- **Billing and Security**: Implemented G1 AI credits overage flow with enhanced
billing telemetry, updated the authentication handshake to specification, and
added support for a policy engine in extensions.
- **Stability and Bug Fixes**: Addressed numerous issues including 100% CPU
consumption by orphaned processes, enhanced retry logic for Code Assist,
reduced intrusive MCP errors, and merged duplicate imports across packages.
## What's Changed
- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
@gemini-cli-robot in
[#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
* Docs: Update model docs to remove Preview Features. by @jkcinouye in
[#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
* docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
[#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
* docs: add Windows PowerShell equivalents for environments and scripting by
@scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
* fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
[#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
* chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
* fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
[#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
* chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
@gemini-cli-robot in
[#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
* Changelog for v0.31.0 by @gemini-cli-robot in
[#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
* fix: use full paths for ACP diff payloads by @JagjeevanAK in
[#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
* Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
[#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
* fix: acp/zed race condition between MCP initialisation and prompt by
@kartikangiras in
[#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
* fix(cli): reset themeManager between tests to ensure isolation by
@NTaylorMullen in
[#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
* refactor(core): Extract tool parameter names as constants by @SandyTao520 in
[#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
* fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
mismatches by @sehoon38 in
[#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
* feat(skills): add github-issue-creator skill by @sehoon38 in
[#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
* fix(cli): allow sub-agent confirmation requests in UI while preventing
background flicker by @abhipatel12 in
[#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
* Merge User and Agent Card Descriptions #20849 by @adamfweidman in
[#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
* fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
[#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
* fix(plan): deflake plan mode integration tests by @Adib234 in
[#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
* Add /unassign support by @scidomino in
[#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
* feat(core): implement HTTP authentication support for A2A remote agents by
- feat(plan): add integration tests for plan mode by @Adib234 in
[#20214](https://github.com/google-gemini/gemini-cli/pull/20214)
- fix(acp): update auth handshake to spec by @skeshive in
[#19725](https://github.com/google-gemini/gemini-cli/pull/19725)
- feat(core): implement robust A2A streaming reassembly and fix task continuity
by @adamfweidman in
[#20091](https://github.com/google-gemini/gemini-cli/pull/20091)
- feat(cli): load extensions in parallel by @scidomino in
[#20229](https://github.com/google-gemini/gemini-cli/pull/20229)
- Plumb the maxAttempts setting through Config args by @kevinjwang1 in
[#20239](https://github.com/google-gemini/gemini-cli/pull/20239)
- fix(cli): skip 404 errors in setup-github file downloads by @h30s in
[#20287](https://github.com/google-gemini/gemini-cli/pull/20287)
- fix(cli): expose model.name setting in settings dialog for persistence by
@achaljhawar in
[#19605](https://github.com/google-gemini/gemini-cli/pull/19605)
- docs: remove legacy cmd examples in favor of powershell by @scidomino in
[#20323](https://github.com/google-gemini/gemini-cli/pull/20323)
- feat(core): Enable model steering in workspace. by @joshualitt in
[#20343](https://github.com/google-gemini/gemini-cli/pull/20343)
- fix: remove trailing comma in issue triage workflow settings json by @Nixxx19
in [#20265](https://github.com/google-gemini/gemini-cli/pull/20265)
- feat(core): implement task tracker foundation and service by @anj-s in
[#19464](https://github.com/google-gemini/gemini-cli/pull/19464)
- test: support tests that include color information by @jacob314 in
[#20220](https://github.com/google-gemini/gemini-cli/pull/20220)
- feat(core): introduce Kind.Agent for sub-agent classification by @abhipatel12
in [#20369](https://github.com/google-gemini/gemini-cli/pull/20369)
- Changelog for v0.30.0 by @gemini-cli-robot in
[#20252](https://github.com/google-gemini/gemini-cli/pull/20252)
- Update changelog workflow to reject nightly builds by @g-samroberts in
[#20248](https://github.com/google-gemini/gemini-cli/pull/20248)
- Changelog for v0.31.0-preview.0 by @gemini-cli-robot in
[#20249](https://github.com/google-gemini/gemini-cli/pull/20249)
- feat(cli): hide workspace policy update dialog and auto-accept by default by
@Abhijit-2592 in
[#20351](https://github.com/google-gemini/gemini-cli/pull/20351)
- feat(core): rename grep_search include parameter to include_pattern by
@SandyTao520 in
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
* feat(core): centralize read_file limits and update gemini-3 description by
@aishaneeshah in
[#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
* Do not block CI on evals by @gundermanc in
[#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
* document node limitation for shift+tab by @scidomino in
[#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
* Add install as an option when extension is selected. by @DavidAPierce in
[#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
* Update CODEOWNERS for README.md reviewers by @g-samroberts in
[#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
* feat(core): truncate large MCP tool output by @SandyTao520 in
[#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
* Subagent activity UX. by @gundermanc in
[#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
* style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
[#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
* feat: redesign header to be compact with ASCII icon by @keithguerin in
[#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
* fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
[#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
* feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
[#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
* refactor(cli): fully remove React anti patterns, improve type safety and fix
UX oversights in SettingsDialog.tsx by @psinha40898 in
[#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
* Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
@Nayana-Parameswarappa in
[#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
* feat(core): add tool name validation in TOML policy files by @allenhutchison
in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
* docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
[#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
* refactor(core): replace manual syncPlanModeTools with declarative policy rules
by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
* fix(core): increase default headers timeout to 5 minutes by @gundermanc in
[#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
* feat(admin): enable 30 day default retention for chat history & remove warning
by @skeshive in
[#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
* feat(plan): support annotating plans with feedback for iteration by @Adib234
in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
* Add some dos and don'ts to behavioral evals README. by @gundermanc in
[#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
* fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
[#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
* fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
models by @SandyTao520 in
[#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
* ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
[#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
* Build binary by @aswinashok44 in
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
* Code review fixes as a pr by @jacob314 in
[#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
* fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
[#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
* feat(cli): invert context window display to show usage by @keithguerin in
[#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
* fix(plan): clean up session directories and plans on deletion by @jerop in
[#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
* fix(core): enforce optionality for API response fields in code_assist by
@sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
* feat(extensions): add support for plan directory in extension manifest by
@mahimashanware in
[#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
* feat(plan): enable built-in research subagents in plan mode by @Adib234 in
[#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
* feat(agents): directly indicate auth required state by @adamfweidman in
[#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
* fix(cli): wait for background auto-update before relaunching by @scidomino in
[#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
* fix: pre-load @scripts/copy_files.js references from external editor prompts
by @kartikangiras in
[#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
* feat(evals): add behavioral evals for ask_user tool by @Adib234 in
[#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
* refactor common settings logic for skills,agents by @ishaanxgupta in
[#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
* Update docs-writer skill with new resource by @g-samroberts in
[#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
* fix(cli): pin clipboardy to ~5.2.x by @scidomino in
[#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
* feat: Implement slash command handling in ACP for
`/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
[#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
* Docs/add hooks reference by @AadithyaAle in
[#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
* feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
* fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
* Format the quota/limit style guide. by @g-samroberts in
[#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
* fix(core): send shell output to model on cancel by @devr0306 in
[#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
* remove hardcoded tiername when missing tier by @sehoon38 in
[#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
* feat(acp): add set models interface by @skeshive in
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
[#20328](https://github.com/google-gemini/gemini-cli/pull/20328)
- feat(plan): support opening and modifying plan in external editor by @Adib234
in [#20348](https://github.com/google-gemini/gemini-cli/pull/20348)
- feat(cli): implement interactive shell autocompletion by @mrpmohiburrahman in
[#20082](https://github.com/google-gemini/gemini-cli/pull/20082)
- fix(core): allow /memory add to work in plan mode by @Jefftree in
[#20353](https://github.com/google-gemini/gemini-cli/pull/20353)
- feat(core): add HTTP 499 to retryable errors and map to RetryableQuotaError by
@bdmorgan in [#20432](https://github.com/google-gemini/gemini-cli/pull/20432)
- feat(core): Enable generalist agent by @joshualitt in
[#19665](https://github.com/google-gemini/gemini-cli/pull/19665)
- Updated tests in TableRenderer.test.tsx to use SVG snapshots by @devr0306 in
[#20450](https://github.com/google-gemini/gemini-cli/pull/20450)
- Refactor Github Action per b/485167538 by @google-admin in
[#19443](https://github.com/google-gemini/gemini-cli/pull/19443)
- fix(github): resolve actionlint and yamllint regressions from #19443 by @jerop
in [#20467](https://github.com/google-gemini/gemini-cli/pull/20467)
- fix: action var usage by @galz10 in
[#20492](https://github.com/google-gemini/gemini-cli/pull/20492)
- feat(core): improve A2A content extraction by @adamfweidman in
[#20487](https://github.com/google-gemini/gemini-cli/pull/20487)
- fix(cli): support quota error fallbacks for all authentication types by
@sehoon38 in [#20475](https://github.com/google-gemini/gemini-cli/pull/20475)
- fix(core): flush transcript for pure tool-call responses to ensure BeforeTool
hooks see complete state by @krishdef7 in
[#20419](https://github.com/google-gemini/gemini-cli/pull/20419)
- feat(plan): adapt planning workflow based on complexity of task by @jerop in
[#20465](https://github.com/google-gemini/gemini-cli/pull/20465)
- fix: prevent orphaned processes from consuming 100% CPU when terminal closes
by @yuvrajangadsingh in
[#16965](https://github.com/google-gemini/gemini-cli/pull/16965)
- feat(core): increase fetch timeout and fix [object Object] error
stringification by @bdmorgan in
[#20441](https://github.com/google-gemini/gemini-cli/pull/20441)
- [Gemma x Gemini CLI] Add an Experimental Gemma Router that uses a LiteRT-LM
shim into the Composite Model Classifier Strategy by @sidwan02 in
[#17231](https://github.com/google-gemini/gemini-cli/pull/17231)
- docs(plan): update documentation regarding supporting editing of plan files
during plan approval by @Adib234 in
[#20452](https://github.com/google-gemini/gemini-cli/pull/20452)
- test(cli): fix flaky ToolResultDisplay overflow test by @jwhelangoog in
[#20518](https://github.com/google-gemini/gemini-cli/pull/20518)
- ui(cli): reduce length of Ctrl+O hint by @jwhelangoog in
[#20490](https://github.com/google-gemini/gemini-cli/pull/20490)
- fix(ui): correct styled table width calculations by @devr0306 in
[#20042](https://github.com/google-gemini/gemini-cli/pull/20042)
- Avoid overaggressive unescaping by @scidomino in
[#20520](https://github.com/google-gemini/gemini-cli/pull/20520)
- feat(telemetry) Instrument traces with more attributes and make them available
to OTEL users by @heaventourist in
[#20237](https://github.com/google-gemini/gemini-cli/pull/20237)
- Add support for policy engine in extensions by @chrstnb in
[#20049](https://github.com/google-gemini/gemini-cli/pull/20049)
- Docs: Update to Terms of Service & FAQ by @jkcinouye in
[#20488](https://github.com/google-gemini/gemini-cli/pull/20488)
- Fix bottom border rendering for search and add a regression test. by @jacob314
in [#20517](https://github.com/google-gemini/gemini-cli/pull/20517)
- fix(core): apply retry logic to CodeAssistServer for all users by @bdmorgan in
[#20507](https://github.com/google-gemini/gemini-cli/pull/20507)
- Fix extension MCP server env var loading by @chrstnb in
[#20374](https://github.com/google-gemini/gemini-cli/pull/20374)
- feat(ui): add 'ctrl+o' hint to truncated content message by @jerop in
[#20529](https://github.com/google-gemini/gemini-cli/pull/20529)
- Fix flicker showing message to press ctrl-O again to collapse. by @jacob314 in
[#20414](https://github.com/google-gemini/gemini-cli/pull/20414)
- fix(cli): hide shortcuts hint while model is thinking or the user has typed a
prompt + add debounce to avoid flicker by @jacob314 in
[#19389](https://github.com/google-gemini/gemini-cli/pull/19389)
- feat(plan): update planning workflow to encourage multi-select with
descriptions of options by @Adib234 in
[#20491](https://github.com/google-gemini/gemini-cli/pull/20491)
- refactor(core,cli): useAlternateBuffer read from config by @psinha40898 in
[#20346](https://github.com/google-gemini/gemini-cli/pull/20346)
- fix(cli): ensure dialogs stay scrolled to bottom in alternate buffer mode by
@jacob314 in [#20527](https://github.com/google-gemini/gemini-cli/pull/20527)
- fix(core): revert auto-save of policies to user space by @Abhijit-2592 in
[#20531](https://github.com/google-gemini/gemini-cli/pull/20531)
- Demote unreliable test. by @gundermanc in
[#20571](https://github.com/google-gemini/gemini-cli/pull/20571)
- fix(core): handle optional response fields from code assist API by @sehoon38
in [#20345](https://github.com/google-gemini/gemini-cli/pull/20345)
- fix(cli): keep thought summary when loading phrases are off by @LyalinDotCom
in [#20497](https://github.com/google-gemini/gemini-cli/pull/20497)
- feat(cli): add temporary flag to disable workspace policies by @Abhijit-2592
in [#20523](https://github.com/google-gemini/gemini-cli/pull/20523)
- Disable expensive and scheduled workflows on personal forks by @dewitt in
[#20449](https://github.com/google-gemini/gemini-cli/pull/20449)
- Moved markdown parsing logic to a separate util file by @devr0306 in
[#20526](https://github.com/google-gemini/gemini-cli/pull/20526)
- fix(plan): prevent agent from using ask_user for shell command confirmation by
@Adib234 in [#20504](https://github.com/google-gemini/gemini-cli/pull/20504)
- fix(core): disable retries for code assist streaming requests by @sehoon38 in
[#20561](https://github.com/google-gemini/gemini-cli/pull/20561)
- feat(billing): implement G1 AI credits overage flow with billing telemetry by
@gsquared94 in
[#18590](https://github.com/google-gemini/gemini-cli/pull/18590)
- feat: better error messages by @gsquared94 in
[#20577](https://github.com/google-gemini/gemini-cli/pull/20577)
- fix(ui): persist expansion in AskUser dialog when navigating options by @jerop
in [#20559](https://github.com/google-gemini/gemini-cli/pull/20559)
- fix(cli): prevent sub-agent tool calls from leaking into UI by @abhipatel12 in
[#20580](https://github.com/google-gemini/gemini-cli/pull/20580)
- fix(cli): Shell autocomplete polish by @jacob314 in
[#20411](https://github.com/google-gemini/gemini-cli/pull/20411)
- Changelog for v0.31.0-preview.1 by @gemini-cli-robot in
[#20590](https://github.com/google-gemini/gemini-cli/pull/20590)
- Add slash command for promoting behavioral evals to CI blocking by @gundermanc
in [#20575](https://github.com/google-gemini/gemini-cli/pull/20575)
- Changelog for v0.30.1 by @gemini-cli-robot in
[#20589](https://github.com/google-gemini/gemini-cli/pull/20589)
- Add low/full CLI error verbosity mode for cleaner UI by @LyalinDotCom in
[#20399](https://github.com/google-gemini/gemini-cli/pull/20399)
- Disable Gemini PR reviews on draft PRs. by @gundermanc in
[#20362](https://github.com/google-gemini/gemini-cli/pull/20362)
- Docs: FAQ update by @jkcinouye in
[#20585](https://github.com/google-gemini/gemini-cli/pull/20585)
- fix(core): reduce intrusive MCP errors and deduplicate diagnostics by
@spencer426 in
[#20232](https://github.com/google-gemini/gemini-cli/pull/20232)
- docs: fix spelling typos in installation guide by @campox747 in
[#20579](https://github.com/google-gemini/gemini-cli/pull/20579)
- Promote stable tests to CI blocking. by @gundermanc in
[#20581](https://github.com/google-gemini/gemini-cli/pull/20581)
- feat(core): enable contiguous parallel admission for Kind.Agent tools by
@abhipatel12 in
[#20583](https://github.com/google-gemini/gemini-cli/pull/20583)
- Enforce import/no-duplicates as error by @Nixxx19 in
[#19797](https://github.com/google-gemini/gemini-cli/pull/19797)
- fix: merge duplicate imports in sdk and test-utils packages (1/4) by @Nixxx19
in [#19777](https://github.com/google-gemini/gemini-cli/pull/19777)
- fix: merge duplicate imports in a2a-server package (2/4) by @Nixxx19 in
[#19781](https://github.com/google-gemini/gemini-cli/pull/19781)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.1
https://github.com/google-gemini/gemini-cli/compare/v0.31.0-preview.3...v0.32.0-preview.0
+3 -3
View File
@@ -244,7 +244,7 @@ gemini
You can significantly enhance security by controlling which tools the Gemini
model can use. This is achieved through the `tools.core` setting and the
[Policy Engine](../reference/policy-engine.md). For a list of available tools,
see the [Tools reference](../reference/tools.md).
see the [Tools documentation](../tools/index.md).
### Allowlisting with `coreTools`
@@ -308,8 +308,8 @@ unintended tool execution.
## Managing custom tools (MCP servers)
If your organization uses custom tools via
[Model-Context Protocol (MCP) servers](../tools/mcp-server.md), it is crucial to
understand how server configurations are managed to apply security policies
[Model-Context Protocol (MCP) servers](../reference/tools-api.md), it is crucial
to understand how server configurations are managed to apply security policies
effectively.
### How MCP server configurations are merged
+121 -184
View File
@@ -1,7 +1,7 @@
# Plan Mode (experimental)
Plan Mode is a read-only environment for architecting robust solutions before
implementation. With Plan Mode, you can:
implementation. It allows you to:
- **Research:** Explore the project in a read-only state to prevent accidental
changes.
@@ -12,48 +12,61 @@ implementation. With Plan Mode, you can:
> feedback is invaluable as we refine this feature. If you have ideas,
> suggestions, or encounter issues:
>
> - [Open an issue] on GitHub.
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues) on
> GitHub.
> - Use the **/bug** command within Gemini CLI to file an issue.
## How to enable Plan Mode
- [Enabling Plan Mode](#enabling-plan-mode)
- [How to use Plan Mode](#how-to-use-plan-mode)
- [Entering Plan Mode](#entering-plan-mode)
- [Planning Workflow](#planning-workflow)
- [Exiting Plan Mode](#exiting-plan-mode)
- [Tool Restrictions](#tool-restrictions)
- [Customizing Planning with Skills](#customizing-planning-with-skills)
- [Customizing Policies](#customizing-policies)
- [Example: Allow git commands in Plan Mode](#example-allow-git-commands-in-plan-mode)
- [Example: Enable research subagents in Plan Mode](#example-enable-research-subagents-in-plan-mode)
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
- [Automatic Model Routing](#automatic-model-routing)
Enable Plan Mode in **Settings** or by editing your configuration file.
## Enabling Plan Mode
- **Settings:** Use the `/settings` command and set **Plan** to `true`.
- **Configuration:** Add the following to your `settings.json`:
To use Plan Mode, enable it via **/settings** (search for **Plan**) or add the
following to your `settings.json`:
```json
{
"experimental": {
"plan": true
}
}
```
## How to use Plan Mode
### Entering Plan Mode
You can configure Gemini CLI to start in Plan Mode by default or enter it
manually during a session.
- **Configuration:** 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**.
Alternatively, use the `gemini --approval-mode=plan` CLI flag or manually
update:
```json
{
"experimental": {
"plan": true
"general": {
"defaultApprovalMode": "plan"
}
}
```
## How to enter Plan Mode
Plan Mode integrates seamlessly into your workflow, letting you switch between
planning and execution as needed.
You can either configure Gemini CLI to start in Plan Mode by default or enter
Plan Mode manually during a session.
### Launch in Plan Mode
To start Gemini CLI directly in Plan Mode by default:
1. Use the `/settings` command.
2. Set **Default Approval Mode** to `Plan`.
To launch Gemini CLI in Plan Mode once:
1. Use `gemini --approval-mode=plan` when launching Gemini CLI.
### Enter Plan Mode manually
To start Plan Mode while using Gemini CLI:
- **Keyboard shortcut:** Press `Shift+Tab` to cycle through approval modes
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`).
> **Note:** Plan Mode is automatically removed from the rotation when Gemini
@@ -61,54 +74,55 @@ To start Plan Mode while using Gemini CLI:
- **Command:** Type `/plan` in the input box.
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI then
calls the [`enter_plan_mode`] tool to switch modes.
> **Note:** This tool is not available when Gemini CLI is in [YOLO mode].
## How to use Plan Mode
### Planning Workflow
Plan Mode lets you collaborate with Gemini CLI to design a solution before
Gemini CLI takes action.
Plan Mode uses an adaptive planning workflow where the research depth, plan
structure, and consultation level are proportional to the task's complexity:
1. **Provide a goal:** Start by describing what you want to achieve. Gemini CLI
will then enter Plan Mode (if it's not already) to research the task.
2. **Review research and provide input:** As Gemini CLI analyzes your codebase,
it may ask you questions or present different implementation options using
[`ask_user`]. Provide your preferences to help guide the design.
3. **Review the plan:** Once Gemini CLI has a proposed strategy, it creates a
detailed implementation plan as a Markdown file in your plans directory. You
can open and read this file to understand the proposed changes.
4. **Approve or iterate:** Gemini CLI will present the finalized plan for your
approval.
- **Approve:** If you're satisfied with the plan, approve it to start the
implementation immediately: **Yes, automatically accept edits** or **Yes,
manually accept edits**.
- **Iterate:** If the plan needs adjustments, provide feedback. Gemini CLI
will refine the strategy and update the plan.
- **Cancel:** You can cancel your plan with `Esc`.
1. **Explore & Analyze:** Analyze requirements and use read-only tools to map
affected modules and identify dependencies.
2. **Consult:** The depth of consultation is proportional to the task's
complexity:
- **Simple Tasks:** Proceed directly to drafting.
- **Standard Tasks:** Present a summary of viable approaches via
[`ask_user`] for selection.
- **Complex Tasks:** Present detailed trade-offs for at least two viable
approaches via [`ask_user`] and obtain approval before drafting.
3. **Draft:** Write a detailed implementation plan to the
[plans directory](#custom-plan-directory-and-policies). The plan's structure
adapts to the task:
- **Simple Tasks:** Focused on specific **Changes** and **Verification**
steps.
- **Standard Tasks:** Includes an **Objective**, **Key Files & Context**,
**Implementation Steps**, and **Verification & Testing**.
- **Complex Tasks:** Comprehensive plans including **Background &
Motivation**, **Scope & Impact**, **Proposed Solution**, **Alternatives
Considered**, a phased **Implementation Plan**, **Verification**, and
**Migration & Rollback** strategies.
4. **Review & Approval:** Use the [`exit_plan_mode`] tool to present the plan
and formally request approval.
- **Approve:** Exit Plan Mode and start implementation.
- **Iterate:** Provide feedback to refine the plan.
- **Refine manually:** Press **Ctrl + X** to open the plan file in your
[preferred external editor]. This allows you to manually refine the plan
steps before approval. The CLI will automatically refresh and show the
updated plan after you save and close the editor.
For more complex or specialized planning tasks, you can
[customize the planning workflow with skills](#custom-planning-with-skills).
[customize the planning workflow with skills](#customizing-planning-with-skills).
## How to exit Plan Mode
### Exiting Plan Mode
You can exit Plan Mode at any time, whether you have finalized a plan or want to
switch back to another mode.
To exit Plan Mode, you can:
- **Approve a plan:** When Gemini CLI presents a finalized plan, approving it
automatically exits Plan Mode and starts the implementation.
- **Keyboard shortcut:** Press `Shift+Tab` to cycle to the desired mode.
- **Natural language:** Ask Gemini CLI to "exit plan mode" or "stop planning."
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
## Customization and best practices
Plan Mode is secure by default, but you can adapt it to fit your specific
workflows. You can customize how Gemini CLI plans by using skills, adjusting
safety policies, or changing where plans are stored.
## Commands
- **`/plan copy`**: Copy the currently approved plan to your clipboard.
- **Tool:** Gemini CLI calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
## Tool Restrictions
@@ -118,9 +132,8 @@ These are the only allowed tools:
- **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`]
- **Search:** [`grep_search`], [`google_web_search`]
- **Research Subagents:** [`codebase_investigator`], [`cli_help`]
- **Interaction:** [`ask_user`]
- **MCP tools (Read):** Read-only [MCP tools] (for example, `github_read_issue`,
- **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>/<session-id>/plans/` directory or your
@@ -129,12 +142,12 @@ These are the only allowed tools:
- **Skills:** [`activate_skill`] (allows loading specialized instructions and
resources in a read-only manner)
### Custom planning with skills
### Customizing Planning with Skills
You can use [Agent Skills] to customize how Gemini CLI approaches planning for
specific types of tasks. When a skill is activated during Plan Mode, its
specialized instructions and procedural workflows will guide the research,
design, and planning phases.
You can use [Agent Skills](./skills.md) to customize how Gemini CLI approaches
planning for specific types of tasks. When a skill is activated during Plan
Mode, its specialized instructions and procedural workflows will guide the
research, design and planning phases.
For example:
@@ -149,7 +162,7 @@ To use a skill in Plan Mode, you can explicitly ask Gemini CLI to "use the
`<skill-name>` skill to plan..." or Gemini CLI may autonomously activate it
based on the task description.
### Custom policies
### Customizing Policies
Plan Mode's default tool restrictions are managed by the [policy engine] and
defined in the built-in [`plan.toml`] file. The built-in policy (Tier 1)
@@ -173,13 +186,10 @@ priority = 100
modes = ["plan"]
```
For more information on how the policy engine works, see the [policy engine]
docs.
#### Example: Allow git commands in Plan Mode
This rule lets you check the repository status and see changes while in Plan
Mode.
This rule allows you to check the repository status and see changes while in
Plan Mode.
`~/.gemini/policies/git-research.toml`
@@ -192,17 +202,16 @@ priority = 100
modes = ["plan"]
```
#### Example: Enable custom subagents in Plan Mode
#### Example: Enable research subagents in Plan Mode
Built-in research [subagents] like [`codebase_investigator`] and [`cli_help`]
are enabled by default in Plan Mode. You can enable additional [custom
subagents] by adding a rule to your policy.
You can enable experimental research [subagents] like `codebase_investigator` to
help gather architecture details during the planning phase.
`~/.gemini/policies/research-subagents.toml`
```toml
[[rule]]
toolName = "my_custom_subagent"
toolName = "codebase_investigator"
decision = "allow"
priority = 100
modes = ["plan"]
@@ -211,7 +220,10 @@ modes = ["plan"]
Tell Gemini CLI it can use these tools in your prompt, for example: _"You can
check ongoing changes in git."_
### Custom plan directory and policies
For more information on how the policy engine works, see the [policy engine]
docs.
### Custom Plan Directory and Policies
By default, planning artifacts are stored in a managed temporary directory
outside your project: `~/.gemini/tmp/<project>/<session-id>/plans/`.
@@ -251,59 +263,10 @@ modes = ["plan"]
argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\""
```
## Planning workflows
Plan Mode provides building blocks for structured research and design. These are
implemented as [extensions] using core planning tools like [`enter_plan_mode`],
[`exit_plan_mode`], and [`ask_user`].
### Built-in planning workflow
The built-in planner uses an adaptive workflow to analyze your project, consult
you on trade-offs via [`ask_user`], and draft a plan for your approval.
### Custom planning workflows
You can install or create specialized planners to suit your workflow.
#### Conductor
[Conductor] is designed for spec-driven development. It organizes work into
"tracks" and stores persistent artifacts in your project's `conductor/`
directory:
- **Automate transitions:** Switches to read-only mode via [`enter_plan_mode`].
- **Streamline decisions:** Uses [`ask_user`] for architectural choices.
- **Maintain project context:** Stores artifacts in the project directory using
[custom plan directory and policies](#custom-plan-directory-and-policies).
- **Handoff execution:** Transitions to implementation via [`exit_plan_mode`].
#### Build your own
Since Plan Mode is built on modular building blocks, you can develop your own
custom planning workflow as an [extensions]. By leveraging core tools and
[custom policies](#custom-policies), you can define how Gemini CLI researches
and stores plans for your specific domain.
To build a custom planning workflow, you can use:
- **Tool usage:** Use core tools like [`enter_plan_mode`], [`ask_user`], and
[`exit_plan_mode`] to manage the research and design process.
- **Customization:** Set your own storage locations and policy rules using
[custom plan directories](#custom-plan-directory-and-policies) and
[custom policies](#custom-policies).
> **Note:** Use [Conductor] as a reference when building your own custom
> planning workflow.
By using Plan Mode as its execution environment, your custom methodology can
enforce read-only safety during the design phase while benefiting from
high-reasoning model routing.
## Automatic Model Routing
When using an [auto model], Gemini CLI automatically optimizes [model routing]
based on the current phase of your task:
When using an [**auto model**], Gemini CLI automatically optimizes [**model
routing**] based on the current phase of your task:
1. **Planning Phase:** While in Plan Mode, the CLI routes requests to a
high-reasoning **Pro** model to ensure robust architectural decisions and
@@ -326,50 +289,24 @@ performance. You can disable this automatic switching in your settings:
}
```
## Cleanup
By default, Gemini CLI automatically cleans up old session data, including all
associated plan files and task trackers.
- **Default behavior:** Sessions (and their plans) are retained for **30 days**.
- **Configuration:** You can customize this behavior via the `/settings` command
(search for **Session Retention**) or in your `settings.json` file. See
[session retention] for more details.
Manual deletion also removes all associated artifacts:
- **Command Line:** Use `gemini --delete-session <index|id>`.
- **Session Browser:** Press `/resume`, navigate to a session, and press `x`.
If you use a [custom plans directory](#custom-plan-directory-and-policies),
those files are not automatically deleted and must be managed manually.
[`list_directory`]: ../tools/file-system.md#1-list_directory-readfolder
[`read_file`]: ../tools/file-system.md#2-read_file-readfile
[`grep_search`]: ../tools/file-system.md#5-grep_search-searchtext
[`write_file`]: ../tools/file-system.md#3-write_file-writefile
[`glob`]: ../tools/file-system.md#4-glob-findfiles
[`google_web_search`]: ../tools/web-search.md
[`replace`]: ../tools/file-system.md#6-replace-edit
[MCP tools]: ../tools/mcp-server.md
[`save_memory`]: ../tools/memory.md
[`activate_skill`]: ./skills.md
[`codebase_investigator`]: ../core/subagents.md#codebase_investigator
[`cli_help`]: ../core/subagents.md#cli_help
[subagents]: ../core/subagents.md
[custom subagents]: ../core/subagents.md#creating-custom-subagents
[policy engine]: ../reference/policy-engine.md
[`enter_plan_mode`]: ../tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: ../tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: ../tools/ask-user.md
[YOLO mode]: ../reference/configuration.md#command-line-arguments
[`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
[`save_memory`]: /docs/tools/memory.md
[`activate_skill`]: /docs/cli/skills.md
[subagents]: /docs/core/subagents.md
[policy engine]: /docs/reference/policy-engine.md
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: /docs/tools/ask-user.md
[YOLO mode]: /docs/reference/configuration.md#command-line-arguments
[`plan.toml`]:
https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml
[auto model]: ../reference/configuration.md#model-settings
[model routing]: ./telemetry.md#model-routing
[preferred external editor]: ../reference/configuration.md#general
[session retention]: ./session-management.md#session-retention
[extensions]: ../extensions/index.md
[Conductor]: https://github.com/gemini-cli-extensions/conductor
[open an issue]: https://github.com/google-gemini/gemini-cli/issues
[Agent Skills]: ./skills.md
[auto model]: /docs/reference/configuration.md#model-settings
[model routing]: /docs/cli/telemetry.md#model-routing
[preferred external editor]: /docs/reference/configuration.md#general
+1 -46
View File
@@ -50,50 +50,6 @@ Cross-platform sandboxing with complete process isolation.
**Note**: Requires building the sandbox image locally or using a published image
from your organization's registry.
### 3. LXC/LXD (Linux only, experimental)
Full-system container sandboxing using LXC/LXD. Unlike Docker/Podman, LXC
containers run a complete Linux system with `systemd`, `snapd`, and other system
services. This is ideal for tools that don't work in standard Docker containers,
such as Snapcraft and Rockcraft.
**Prerequisites**:
- Linux only.
- LXC/LXD must be installed (`snap install lxd` or `apt install lxd`).
- A container must be created and running before starting Gemini CLI. Gemini
does **not** create the container automatically.
**Quick setup**:
```bash
# Initialize LXD (first time only)
lxd init --auto
# Create and start an Ubuntu container
lxc launch ubuntu:24.04 gemini-sandbox
# Enable LXC sandboxing
export GEMINI_SANDBOX=lxc
gemini -p "build the project"
```
**Custom container name**:
```bash
export GEMINI_SANDBOX=lxc
export GEMINI_SANDBOX_IMAGE=my-snapcraft-container
gemini -p "build the snap"
```
**Limitations**:
- Linux only (LXC is not available on macOS or Windows).
- The container must already exist and be running.
- The workspace directory is bind-mounted into the container at the same
absolute path — the path must be writable inside the container.
- Used with tools like Snapcraft or Rockcraft that require a full system.
## Quickstart
```bash
@@ -132,8 +88,7 @@ gemini -p "run the test suite"
### Enable sandboxing (in order of precedence)
1. **Command flag**: `-s` or `--sandbox`
2. **Environment variable**:
`GEMINI_SANDBOX=true|docker|podman|sandbox-exec|lxc`
2. **Environment variable**: `GEMINI_SANDBOX=true|docker|podman|sandbox-exec`
3. **Settings file**: `"sandbox": true` in the `tools` object of your
`settings.json` file (e.g., `{"tools": {"sandbox": true}}`).
+7 -16
View File
@@ -121,36 +121,27 @@ session lengths.
### Session retention
By default, Gemini CLI automatically cleans up old session data to prevent your
history from growing indefinitely. When a session is deleted, Gemini CLI also
removes all associated data, including implementation plans, task trackers, tool
outputs, and activity logs.
The default policy is to **retain sessions for 30 days**.
#### Configuration
You can customize these policies using the `/settings` command or by manually
editing your `settings.json` file:
To prevent your history from growing indefinitely, enable automatic cleanup
policies in your settings.
```json
{
"general": {
"sessionRetention": {
"enabled": true,
"maxAge": "30d",
"maxCount": 50
"maxAge": "30d", // Keep sessions for 30 days
"maxCount": 50 // Keep the 50 most recent sessions
}
}
}
```
- **`enabled`**: (boolean) Master switch for session cleanup. Defaults to
`true`.
`false`.
- **`maxAge`**: (string) Duration to keep sessions (for example, "24h", "7d",
"4w"). Sessions older than this are deleted. Defaults to `"30d"`.
"4w"). Sessions older than this are deleted.
- **`maxCount`**: (number) Maximum number of sessions to retain. The oldest
sessions exceeding this count are deleted. Defaults to undefined (unlimited).
sessions exceeding this count are deleted.
- **`minRetention`**: (string) Minimum retention period (safety limit). Defaults
to `"1d"`. Sessions newer than this period are never deleted by automatic
cleanup.
+11 -11
View File
@@ -32,8 +32,8 @@ they appear in the UI.
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
### Output
@@ -57,10 +57,10 @@ they appear in the UI.
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `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 usage percentage. | `true` |
| 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` |
@@ -89,13 +89,13 @@ they appear in the UI.
### Model
| UI Label | Setting | Description | Default |
| ----------------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Context 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` |
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
| 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
+2 -2
View File
@@ -9,8 +9,8 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
- **[Sub-agents (experimental)](./subagents.md):** Learn how to create and use
specialized sub-agents for complex tasks.
- **[Core tools reference](../reference/tools.md):** Information on how tools
are defined, registered, and used by the core.
- **[Core tools API](../reference/tools-api.md):** Information on how tools are
defined, registered, and used by the core.
- **[Memory Import Processor](../reference/memport.md):** Documentation for the
modular GEMINI.md import feature using @file.md syntax.
- **[Policy Engine](../reference/policy-engine.md):** Use the Policy Engine for
+1 -9
View File
@@ -122,10 +122,7 @@ The manifest file defines the extension's behavior and configuration.
}
},
"contextFileName": "GEMINI.md",
"excludeTools": ["run_shell_command"],
"plan": {
"directory": ".gemini/plans"
}
"excludeTools": ["run_shell_command"]
}
```
@@ -160,11 +157,6 @@ The manifest file defines the extension's behavior and configuration.
`"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf`
command. Note that this differs from the MCP server `excludeTools`
functionality, which can be listed in the MCP server config.
- `plan`: Planning features configuration.
- `directory`: The directory where planning artifacts are stored. This serves
as a fallback if the user hasn't specified a plan directory in their
settings. If not specified by either the extension or the user, the default
is `~/.gemini/tmp/<project>/<session-id>/plans/`.
When Gemini CLI starts, it loads all the extensions and merges their
configurations. If there are any conflicts, the workspace configuration takes
+2 -2
View File
@@ -82,8 +82,8 @@ For `BeforeTool` and `AfterTool` events, the `matcher` field in your settings is
compared against the name of the tool being executed.
- **Built-in Tools**: You can match any built-in tool (e.g., `read_file`,
`run_shell_command`). See the [Tools Reference](/docs/reference/tools) for a
full list of available tool names.
`run_shell_command`). See the [Tools Reference](/docs/tools) for a full list
of available tool names.
- **MCP Tools**: Tools from MCP servers follow the naming pattern
`mcp__<server_name>__<tool_name>`.
- **Regex Support**: Matchers support regular expressions (e.g.,
+2 -2
View File
@@ -108,8 +108,8 @@ Deep technical documentation and API specifications.
processes memory from various sources.
- **[Policy engine](./reference/policy-engine.md):** Fine-grained execution
control.
- **[Tools reference](./reference/tools.md):** Information on how tools are
defined, registered, and used.
- **[Tools API](./reference/tools-api.md):** The API for defining and using
tools.
## Resources
+1 -39
View File
@@ -113,45 +113,7 @@ process.
ensure every issue is eventually categorized, even if the initial triage
fails.
### 5. Automatic unassignment of inactive contributors: `Unassign Inactive Issue Assignees`
To keep the list of open `help wanted` issues accessible to all contributors,
this workflow automatically removes **external contributors** who have not
opened a linked pull request within **7 days** of being assigned. Maintainers,
org members, and repo collaborators with write access or above are always exempt
and will never be auto-unassigned.
- **Workflow File**: `.github/workflows/unassign-inactive-assignees.yml`
- **When it runs**: Every day at 09:00 UTC, and can be triggered manually with
an optional `dry_run` mode.
- **What it does**:
1. Finds every open issue labeled `help wanted` that has at least one
assignee.
2. Identifies privileged users (team members, repo collaborators with write+
access, maintainers) and skips them entirely.
3. For each remaining (external) assignee it reads the issue's timeline to
determine:
- The exact date they were assigned (using `assigned` timeline events).
- Whether they have opened a PR that is already linked/cross-referenced to
the issue.
4. Each cross-referenced PR is fetched to verify it is **ready for review**:
open and non-draft, or already merged. Draft PRs do not count.
5. If an assignee has been assigned for **more than 7 days** and no qualifying
PR is found, they are automatically unassigned and a comment is posted
explaining the reason and how to re-claim the issue.
6. Assignees who have a non-draft, open or merged PR linked to the issue are
**never** unassigned by this workflow.
- **What you should do**:
- **Open a real PR, not a draft**: Within 7 days of being assigned, open a PR
that is ready for review and include `Fixes #<issue-number>` in the
description. Draft PRs do not satisfy the requirement and will not prevent
auto-unassignment.
- **Re-assign if unassigned by mistake**: Comment `/assign` on the issue to
assign yourself again.
- **Unassign yourself** if you can no longer work on the issue by commenting
`/unassign`, so other contributors can pick it up right away.
### 6. Release automation
### 5. Release automation
This workflow handles the process of packaging and publishing new versions of
the Gemini CLI.
+1 -2
View File
@@ -8,8 +8,7 @@
"/docs/core/concepts": "/docs",
"/docs/core/memport": "/docs/reference/memport",
"/docs/core/policy-engine": "/docs/reference/policy-engine",
"/docs/core/tools-api": "/docs/reference/tools",
"/docs/reference/tools-api": "/docs/reference/tools",
"/docs/core/tools-api": "/docs/reference/tools-api",
"/docs/faq": "/docs/resources/faq",
"/docs/get-started/configuration": "/docs/reference/configuration",
"/docs/get-started/configuration-v1": "/docs/reference/configuration",
-3
View File
@@ -270,9 +270,6 @@ Slash commands provide meta-level control over the CLI itself.
one has been generated.
- **Note:** This feature requires the `experimental.plan` setting to be
enabled in your configuration.
- **Sub-commands:**
- **`copy`**:
- **Description:** Copy the currently approved plan to your clipboard.
### `/policies`
+10 -21
View File
@@ -159,12 +159,12 @@ their corresponding top-level category object in your `settings.json` file.
- **`general.sessionRetention.enabled`** (boolean):
- **Description:** Enable automatic session cleanup
- **Default:** `true`
- **Default:** `false`
- **`general.sessionRetention.maxAge`** (string):
- **Description:** Automatically delete chats older than this time period
(e.g., "30d", "7d", "24h", "1w")
- **Default:** `"30d"`
- **Default:** `undefined`
- **`general.sessionRetention.maxCount`** (number):
- **Description:** Alternative: Maximum number of sessions to keep (most
@@ -175,6 +175,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Minimum retention period (safety limit, defaults to "1d")
- **Default:** `"1d"`
- **`general.sessionRetention.warningAcknowledged`** (boolean):
- **Description:** INTERNAL: Whether the user has acknowledged the session
retention warning
- **Default:** `false`
#### `output`
- **`output.format`** (enum):
@@ -250,18 +255,8 @@ their corresponding top-level category object in your `settings.json` file.
input.
- **Default:** `false`
- **`ui.footer.items`** (array):
- **Description:** List of item IDs to display in the footer. Rendered in
order
- **Default:** `undefined`
- **`ui.footer.showLabels`** (boolean):
- **Description:** Display a second line above the footer items with
descriptive headers (e.g., /model).
- **Default:** `true`
- **`ui.footer.hideCWD`** (boolean):
- **Description:** Hide the current working directory in the footer.
- **Description:** Hide the current working directory path in the footer.
- **Default:** `false`
- **`ui.footer.hideSandboxStatus`** (boolean):
@@ -273,7 +268,7 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **`ui.footer.hideContextPercentage`** (boolean):
- **Description:** Hides the context window usage percentage.
- **Description:** Hides the context window remaining percentage.
- **Default:** `true`
- **`ui.hideFooter`** (boolean):
@@ -757,8 +752,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`tools.sandbox`** (boolean | string):
- **Description:** Sandbox execution environment. Set to a boolean to enable
or disable the sandbox, provide a string path to a sandbox profile, or
specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
or disable the sandbox, or provide a string path to a sandbox profile.
- **Default:** `undefined`
- **Requires restart:** Yes
@@ -1025,11 +1019,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.taskTracker`** (boolean):
- **Description:** Enable task tracker tools.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.modelSteering`** (boolean):
- **Description:** Enable model steering (user hints) to guide the model
during tool execution.
-10
View File
@@ -152,13 +152,3 @@ available combinations.
inline when the cursor is over the placeholder.
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
view full content inline. Double-click again to collapse.
## Limitations
- On [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal):
- `shift+enter` is not supported.
- `shift+tab`
[is not supported](https://github.com/google-gemini/gemini-cli/issues/20314)
on Node 20 and earlier versions of Node 22.
- On macOS's [Terminal](<https://en.wikipedia.org/wiki/Terminal_(macOS)>):
- `shift+enter` is not supported.
+131
View File
@@ -0,0 +1,131 @@
# Gemini CLI core: Tools API
The Gemini CLI core (`packages/core`) features a robust system for defining,
registering, and executing tools. These tools extend the capabilities of the
Gemini model, allowing it to interact with the local environment, fetch web
content, and perform various actions beyond simple text generation.
## Core concepts
- **Tool (`tools.ts`):** An interface and base class (`BaseTool`) that defines
the contract for all tools. Each tool must have:
- `name`: A unique internal name (used in API calls to Gemini).
- `displayName`: A user-friendly name.
- `description`: A clear explanation of what the tool does, which is provided
to the Gemini model.
- `parameterSchema`: A JSON schema defining the parameters that the tool
accepts. This is crucial for the Gemini model to understand how to call the
tool correctly.
- `validateToolParams()`: A method to validate incoming parameters.
- `getDescription()`: A method to provide a human-readable description of what
the tool will do with specific parameters before execution.
- `shouldConfirmExecute()`: A method to determine if user confirmation is
required before execution (e.g., for potentially destructive operations).
- `execute()`: The core method that performs the tool's action and returns a
`ToolResult`.
- **`ToolResult` (`tools.ts`):** An interface defining the structure of a tool's
execution outcome:
- `llmContent`: The factual content to be included in the history sent back to
the LLM for context. This can be a simple string or a `PartListUnion` (an
array of `Part` objects and strings) for rich content.
- `returnDisplay`: A user-friendly string (often Markdown) or a special object
(like `FileDiff`) for display in the CLI.
- **Returning rich content:** Tools are not limited to returning simple text.
The `llmContent` can be a `PartListUnion`, which is an array that can contain
a mix of `Part` objects (for images, audio, etc.) and `string`s. This allows a
single tool execution to return multiple pieces of rich content.
- **Tool registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible
for:
- **Registering tools:** Holding a collection of all available built-in tools
(e.g., `ReadFileTool`, `ShellTool`).
- **Discovering tools:** It can also discover tools dynamically:
- **Command-based discovery:** If `tools.discoveryCommand` is configured in
settings, this command is executed. It's expected to output JSON
describing custom tools, which are then registered as `DiscoveredTool`
instances.
- **MCP-based discovery:** If `mcp.serverCommand` is configured, the
registry can connect to a Model Context Protocol (MCP) server to list and
register tools (`DiscoveredMCPTool`).
- **Providing schemas:** Exposing the `FunctionDeclaration` schemas of all
registered tools to the Gemini model, so it knows what tools are available
and how to use them.
- **Retrieving tools:** Allowing the core to get a specific tool by name for
execution.
## Built-in tools
The core comes with a suite of pre-defined tools, typically found in
`packages/core/src/tools/`. These include:
- **File system tools:**
- `LSTool` (`ls.ts`): Lists directory contents.
- `ReadFileTool` (`read-file.ts`): Reads the content of a single file.
- `WriteFileTool` (`write-file.ts`): Writes content to a file.
- `GrepTool` (`grep.ts`): Searches for patterns in files.
- `GlobTool` (`glob.ts`): Finds files matching glob patterns.
- `EditTool` (`edit.ts`): Performs in-place modifications to files (often
requiring confirmation).
- `ReadManyFilesTool` (`read-many-files.ts`): Reads and concatenates content
from multiple files or glob patterns (used by the `@` command in CLI).
- **Execution tools:**
- `ShellTool` (`shell.ts`): Executes arbitrary shell commands (requires
careful sandboxing and user confirmation).
- **Web tools:**
- `WebFetchTool` (`web-fetch.ts`): Fetches content from a URL.
- `WebSearchTool` (`web-search.ts`): Performs a web search.
- **Memory tools:**
- `MemoryTool` (`memoryTool.ts`): Interacts with the AI's memory.
Each of these tools extends `BaseTool` and implements the required methods for
its specific functionality.
## Tool execution flow
1. **Model request:** The Gemini model, based on the user's prompt and the
provided tool schemas, decides to use a tool and returns a `FunctionCall`
part in its response, specifying the tool name and arguments.
2. **Core receives request:** The core parses this `FunctionCall`.
3. **Tool retrieval:** It looks up the requested tool in the `ToolRegistry`.
4. **Parameter validation:** The tool's `validateToolParams()` method is
called.
5. **Confirmation (if needed):**
- The tool's `shouldConfirmExecute()` method is called.
- If it returns details for confirmation, the core communicates this back to
the CLI, which prompts the user.
- The user's decision (e.g., proceed, cancel) is sent back to the core.
6. **Execution:** If validated and confirmed (or if no confirmation is needed),
the core calls the tool's `execute()` method with the provided arguments and
an `AbortSignal` (for potential cancellation).
7. **Result processing:** The `ToolResult` from `execute()` is received by the
core.
8. **Response to model:** The `llmContent` from the `ToolResult` is packaged as
a `FunctionResponse` and sent back to the Gemini model so it can continue
generating a user-facing response.
9. **Display to user:** The `returnDisplay` from the `ToolResult` is sent to
the CLI to show the user what the tool did.
## Extending with custom tools
While direct programmatic registration of new tools by users isn't explicitly
detailed as a primary workflow in the provided files for typical end-users, the
architecture supports extension through:
- **Command-based discovery:** Advanced users or project administrators can
define a `tools.discoveryCommand` in `settings.json`. This command, when run
by the Gemini CLI core, should output a JSON array of `FunctionDeclaration`
objects. The core will then make these available as `DiscoveredTool`
instances. The corresponding `tools.callCommand` would then be responsible for
actually executing these custom tools.
- **MCP server(s):** For more complex scenarios, one or more MCP servers can be
set up and configured via the `mcpServers` setting in `settings.json`. The
Gemini CLI core can then discover and use tools exposed by these servers. As
mentioned, if you have multiple MCP servers, the tool names will be prefixed
with the server name from your configuration (e.g.,
`serverAlias__actualToolName`).
This tool system provides a flexible and powerful way to augment the Gemini
model's capabilities, making the Gemini CLI a versatile assistant for a wide
range of tasks.
-106
View File
@@ -1,106 +0,0 @@
# Tools reference
Gemini CLI uses tools to interact with your local environment, access
information, and perform actions on your behalf. These tools extend the model's
capabilities beyond text generation, letting it read files, execute commands,
and search the web.
## How to use Gemini CLI's tools
Tools are generally invoked automatically by Gemini CLI when it needs to perform
an action. However, you can also trigger specific tools manually using shorthand
syntax.
### Automatic execution and security
When the model wants to use a tool, Gemini CLI evaluates the request against its
security policies.
- **User confirmation:** You must manually approve tools that modify files or
execute shell commands (mutators). The CLI shows you a diff or the exact
command before you confirm.
- **Sandboxing:** You can run tool executions in secure, containerized
environments to isolate changes from your host system. For more details, see
the [Sandboxing](../cli/sandbox.md) guide.
- **Trusted folders:** You can configure which directories allow the model to
use system tools. For more details, see the
[Trusted folders](../cli/trusted-folders.md) guide.
Review confirmation prompts carefully before allowing a tool to execute.
### How to use manually-triggered tools
You can directly trigger key tools using special syntax in your prompt:
- **[File access](../tools/file-system.md#read_many_files) (`@`):** Use the `@`
symbol followed by a file or directory path to include its content in your
prompt. This triggers the `read_many_files` tool.
- **[Shell commands](../tools/shell.md) (`!`):** Use the `!` symbol followed by
a system command to execute it directly. This triggers the `run_shell_command`
tool.
## How to manage tools
Using built-in commands, you can inspect available tools and configure how they
behave.
### Tool discovery
Use the `/tools` command to see what tools are currently active in your session.
- **`/tools`**: Lists all registered tools with their display names.
- **`/tools desc`**: Lists all tools with their full descriptions.
This is especially useful for verifying that
[MCP servers](../tools/mcp-server.md) or custom tools are loaded correctly.
### Tool configuration
You can enable, disable, or configure specific tools in your settings. For
example, you can set a specific pager for shell commands or configure the
browser used for web searches. See the [Settings](../cli/settings.md) guide for
details.
## Available tools
The following table lists all available tools, categorized by their primary
function.
| Category | Tool | Kind | Description |
| :---------- | :----------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Execution | [`run_shell_command`](../tools/shell.md) | `Execute` | Executes arbitrary shell commands. Supports interactive sessions and background processes. Requires manual confirmation.<br><br>**Parameters:** `command`, `description`, `dir_path`, `is_background` |
| File System | [`glob`](../tools/file-system.md) | `Search` | Finds files matching specific glob patterns across the workspace.<br><br>**Parameters:** `pattern`, `dir_path`, `case_sensitive`, `respect_git_ignore`, `respect_gemini_ignore` |
| File System | [`grep_search`](../tools/file-system.md) | `Search` | Searches for a regular expression pattern within file contents. Legacy alias: `search_file_content`.<br><br>**Parameters:** `pattern`, `dir_path`, `include`, `exclude_pattern`, `names_only`, `max_matches_per_file`, `total_max_matches` |
| File System | [`list_directory`](../tools/file-system.md) | `Read` | Lists the names of files and subdirectories within a specified path.<br><br>**Parameters:** `dir_path`, `ignore`, `file_filtering_options` |
| File System | [`read_file`](../tools/file-system.md) | `Read` | Reads the content of a specific file. Supports text, images, audio, and PDF.<br><br>**Parameters:** `file_path`, `start_line`, `end_line` |
| File System | [`read_many_files`](../tools/file-system.md) | `Read` | Reads and concatenates content from multiple files. Often triggered by the `@` symbol in your prompt.<br><br>**Parameters:** `include`, `exclude`, `recursive`, `useDefaultExcludes`, `file_filtering_options` |
| File System | [`replace`](../tools/file-system.md) | `Edit` | Performs precise text replacement within a file. Requires manual confirmation.<br><br>**Parameters:** `file_path`, `instruction`, `old_string`, `new_string`, `allow_multiple` |
| File System | [`write_file`](../tools/file-system.md) | `Edit` | Creates or overwrites a file with new content. Requires manual confirmation.<br><br>**Parameters:** `file_path`, `content` |
| Interaction | [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog.<br><br>**Parameters:** `questions` |
| Interaction | [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress and display it to you.<br><br>**Parameters:** `todos` |
| Memory | [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise for specific tasks from the `.gemini/skills` directory.<br><br>**Parameters:** `name` |
| Memory | [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation to provide more accurate answers about its capabilities.<br><br>**Parameters:** `path` |
| Memory | [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file to retain context.<br><br>**Parameters:** `fact` |
| Planning | [`enter_plan_mode`](../tools/planning.md) | `Plan` | Switches the CLI to a safe, read-only "Plan Mode" for researching complex changes.<br><br>**Parameters:** `reason` |
| Planning | [`exit_plan_mode`](../tools/planning.md) | `Plan` | Finalizes a plan, presents it for review, and requests approval to start implementation.<br><br>**Parameters:** `plan` |
| System | `complete_task` | `Other` | Finalizes a subagent's mission and returns the result to the parent agent. This tool is not available to the user.<br><br>**Parameters:** `result` |
| Web | [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information.<br><br>**Parameters:** `query` |
| Web | [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts.<br><br>**Parameters:** `prompt` |
## Under the hood
For developers, the tool system is designed to be extensible and robust. The
`ToolRegistry` class manages all available tools.
You can extend Gemini CLI with custom tools by configuring
`tools.discoveryCommand` in your settings or by connecting to MCP servers.
> **Note:** For a deep dive into the internal Tool API and how to implement your
> own tools in the codebase, see the `packages/core/src/tools/` directory in
> GitHub.
## Next steps
- Learn how to [Set up an MCP server](../tools/mcp-server.md).
- Explore [Agent Skills](../cli/skills.md) for specialized expertise.
- See the [Command reference](./commands.md) for slash commands.
+2 -9
View File
@@ -94,14 +94,7 @@
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{ "label": "Headless mode", "slug": "docs/cli/headless" },
{
"label": "Hooks",
"collapsed": true,
"items": [
{ "label": "Overview", "slug": "docs/hooks" },
{ "label": "Reference", "slug": "docs/hooks/reference" }
]
},
{ "label": "Hooks", "slug": "docs/hooks" },
{ "label": "IDE integration", "slug": "docs/ide-integration" },
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
@@ -188,7 +181,7 @@
"slug": "docs/reference/memport"
},
{ "label": "Policy engine", "slug": "docs/reference/policy-engine" },
{ "label": "Tools reference", "slug": "docs/reference/tools" }
{ "label": "Tools API", "slug": "docs/reference/tools-api" }
]
}
]
+105
View File
@@ -0,0 +1,105 @@
# Gemini CLI tools
Gemini CLI uses tools to interact with your local environment, access
information, and perform actions on your behalf. These tools extend the model's
capabilities beyond text generation, letting it read files, execute commands,
and search the web.
## User-triggered tools
You can directly trigger these tools using special syntax in your prompts.
- **[File access](./file-system.md#read_many_files) (`@`):** Use the `@` symbol
followed by a file or directory path to include its content in your prompt.
This triggers the `read_many_files` tool.
- **[Shell commands](./shell.md) (`!`):** Use the `!` symbol followed by a
system command to execute it directly. This triggers the `run_shell_command`
tool.
## Model-triggered tools
The Gemini model automatically requests these tools when it needs to perform
specific actions or gather information to fulfill your requests. You do not call
these tools manually.
### File management
These tools let the model explore and modify your local codebase.
- **[Directory listing](./file-system.md#list_directory) (`list_directory`):**
Lists files and subdirectories.
- **[File reading](./file-system.md#read_file) (`read_file`):** Reads the
content of a specific file.
- **[File writing](./file-system.md#write_file) (`write_file`):** Creates or
overwrites a file with new content.
- **[File search](./file-system.md#glob) (`glob`):** Finds files matching a glob
pattern.
- **[Text search](./file-system.md#search_file_content)
(`search_file_content`):** Searches for text within files using grep or
ripgrep.
- **[Text replacement](./file-system.md#replace) (`replace`):** Performs precise
edits within a file.
### Agent coordination
These tools help the model manage its plan and interact with you.
- **Ask user (`ask_user`):** Requests clarification or missing information from
you via an interactive dialog.
- **[Memory](./memory.md) (`save_memory`):** Saves important facts to your
long-term memory (`GEMINI.md`).
- **[Todos](./todos.md) (`write_todos`):** Manages a list of subtasks for
complex plans.
- **[Agent Skills](../cli/skills.md) (`activate_skill`):** Loads specialized
procedural expertise when needed.
- **[Browser agent](../core/subagents.md#browser-agent-experimental)
(`browser_agent`):** Automates web browser tasks through the accessibility
tree.
- **Internal docs (`get_internal_docs`):** Accesses Gemini CLI's own
documentation to help answer your questions.
### Information gathering
These tools provide the model with access to external data.
- **[Web fetch](./web-fetch.md) (`web_fetch`):** Retrieves and processes content
from specific URLs.
- **[Web search](./web-search.md) (`google_web_search`):** Performs a Google
Search to find up-to-date information.
## How to use tools
You use tools indirectly by providing natural language prompts to Gemini CLI.
1. **Prompt:** You enter a request or use syntax like `@` or `!`.
2. **Request:** The model analyzes your request and identifies if a tool is
required.
3. **Validation:** If a tool is needed, the CLI validates the parameters and
checks your security settings.
4. **Confirmation:** For sensitive operations (like writing files), the CLI
prompts you for approval.
5. **Execution:** The tool runs, and its output is sent back to the model.
6. **Response:** The model uses the results to generate a final, grounded
answer.
## Security and confirmation
Safety is a core part of the tool system. To protect your system, Gemini CLI
implements several safeguards.
- **User confirmation:** You must manually approve tools that modify files or
execute shell commands. The CLI shows you a diff or the exact command before
you confirm.
- **Sandboxing:** You can run tool executions in secure, containerized
environments to isolate changes from your host system. For more details, see
the [Sandboxing](../cli/sandbox.md) guide.
- **Trusted folders:** You can configure which directories allow the model to
use system tools.
Always review confirmation prompts carefully before allowing a tool to execute.
## Next steps
- Learn how to [Provide context](../cli/gemini-md.md) to guide tool use.
- Explore the [Command reference](../reference/commands.md) for tool-related
slash commands.
+6 -7
View File
@@ -1,8 +1,8 @@
# Gemini CLI planning tools
Planning tools let Gemini CLI switch into a safe, read-only "Plan Mode" for
researching and planning complex changes, and to signal the finalization of a
plan to the user.
Planning tools allow the Gemini model to switch into a safe, read-only "Plan
Mode" for researching and planning complex changes, and to signal the
finalization of a plan to the user.
## 1. `enter_plan_mode` (EnterPlanMode)
@@ -18,12 +18,11 @@ and planning.
- **File:** `enter-plan-mode.ts`
- **Parameters:**
- `reason` (string, optional): A short reason explaining why the agent is
entering plan mode (for example, "Starting a complex feature
implementation").
entering plan mode (e.g., "Starting a complex feature implementation").
- **Behavior:**
- Switches the CLI's approval mode to `PLAN`.
- Notifies the user that the agent has entered Plan Mode.
- **Output (`llmContent`):** A message indicating the switch, for example,
- **Output (`llmContent`):** A message indicating the switch, e.g.,
`Switching to Plan mode.`
- **Confirmation:** Yes. The user is prompted to confirm entering Plan Mode.
@@ -38,7 +37,7 @@ finalized plan to the user and requests approval to start the implementation.
- **Parameters:**
- `plan_path` (string, required): The path to the finalized Markdown plan
file. This file MUST be located within the project's temporary plans
directory (for example, `~/.gemini/tmp/<project>/plans/`).
directory (e.g., `~/.gemini/tmp/<project>/plans/`).
- **Behavior:**
- Validates that the `plan_path` is within the allowed directory and that the
file exists and has content.
-3
View File
@@ -88,9 +88,6 @@ const cliConfig = {
outfile: 'bundle/gemini.js',
define: {
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
'process.env.GEMINI_SANDBOX_IMAGE_DEFAULT': JSON.stringify(
pkg.config?.sandboxImageUri,
),
},
plugins: createWasmPlugins(),
alias: {
+12 -35
View File
@@ -25,18 +25,6 @@ const __dirname = path.dirname(__filename);
const projectRoot = __dirname;
const currentYear = new Date().getFullYear();
const commonRestrictedSyntaxRules = [
{
selector: 'CallExpression[callee.name="require"]',
message: 'Avoid using require(). Use ES6 imports instead.',
},
{
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
message:
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
},
];
export default tseslint.config(
{
// Global ignores
@@ -132,7 +120,18 @@ export default tseslint.config(
'no-cond-assign': 'error',
'no-debugger': 'error',
'no-duplicate-case': 'error',
'no-restricted-syntax': ['error', ...commonRestrictedSyntaxRules],
'no-restricted-syntax': [
'error',
{
selector: 'CallExpression[callee.name="require"]',
message: 'Avoid using require(). Use ES6 imports instead.',
},
{
selector: 'ThrowStatement > Literal:not([value=/^\\w+Error:/])',
message:
'Do not throw string literals or non-Error objects. Throw new Error("...") instead.',
},
],
'no-unsafe-finally': 'error',
'no-unused-expressions': 'off', // Disable base rule
'@typescript-eslint/no-unused-expressions': [
@@ -172,28 +171,6 @@ export default tseslint.config(
],
},
},
{
// API Response Optionality enforcement for Code Assist
files: ['packages/core/src/code_assist/**/*.{ts,tsx}'],
rules: {
'no-restricted-syntax': [
'error',
...commonRestrictedSyntaxRules,
{
selector:
'TSInterfaceDeclaration[id.name=/.+Response$/] TSPropertySignature:not([optional=true])',
message:
'All fields in API response interfaces (*Response) must be marked as optional (?) to prevent developers from accidentally assuming a field will always be present based on current backend behavior.',
},
{
selector:
'TSTypeAliasDeclaration[id.name=/.+Response$/] TSPropertySignature:not([optional=true])',
message:
'All fields in API response types (*Response) must be marked as optional (?) to prevent developers from accidentally assuming a field will always be present based on current backend behavior.',
},
],
},
},
{
// Rules that only apply to product code
files: ['packages/*/src/**/*.{ts,tsx}'],
+1 -44
View File
@@ -3,8 +3,7 @@
Behavioral evaluations (evals) are tests designed to validate the agent's
behavior in response to specific prompts. They serve as a critical feedback loop
for changes to system prompts, tool definitions, and other model-steering
mechanisms, and as a tool for assessing feature reliability by model, and
preventing regressions.
mechanisms.
## Why Behavioral Evals?
@@ -31,48 +30,6 @@ CLI's features.
those that are generally reliable but might occasionally vary
(`USUALLY_PASSES`).
## Best Practices
When designing behavioral evals, aim for scenarios that accurately reflect
real-world usage while remaining small and maintainable.
- **Realistic Complexity**: Evals should be complicated enough to be
"realistic." They should operate on actual files and a source directory,
mirroring how a real agent interacts with a workspace. Remember that the agent
may behave differently in a larger codebase, so we want to avoid scenarios
that are too simple to be realistic.
- _Good_: An eval that provides a small, functional React component and asks
the agent to add a specific feature, requiring it to read the file,
understand the context, and write the correct changes.
- _Bad_: An eval that simply asks the agent a trivia question or asks it to
write a generic script without providing any local workspace context.
- **Maintainable Size**: Evals should be small enough to reason about and
maintain. We probably can't check in an entire repo as a test case, though
over time we will want these evals to mature into more and more realistic
scenarios.
- _Good_: A test setup with 2-3 files (e.g., a source file, a config file, and
a test file) that isolates the specific behavior being evaluated.
- _Bad_: A test setup containing dozens of files from a complex framework
where the setup logic itself is prone to breaking.
- **Unambiguous and Reliable Assertions**: Assertions must be clear and specific
to ensure the test passes for the right reason.
- _Good_: Checking that a modified file contains a specific AST node or exact
string, or verifying that a tool was called with with the right parameters.
- _Bad_: Only checking for a tool call, which could happen for an unrelated
reason. Expecting specific LLM output.
- **Fail First**: Have tests that failed before your prompt or tool change. We
want to be sure the test fails before your "fix". It's pretty easy to
accidentally create a passing test that asserts behaviors we get for free. In
general, every eval should be accompanied by prompt change, and most prompt
changes should be accompanied by an eval.
- _Good_: Observing a failure, writing an eval that reliably reproduces the
failure, modifying the prompt/tool, and then verifying the eval passes.
- _Bad_: Writing an eval that passes on the first run and assuming your new
prompt change was responsible.
- **Less is More**: Prefer fewer, more realistic tests that assert the major
paths vs. more tests that are more unit-test like. These are evals, so the
value is in testing how the agent works in a semi-realistic scenario.
## Creating an Evaluation
Evaluations are located in the `evals` directory. Each evaluation is a Vitest
-92
View File
@@ -1,92 +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('ask_user', () => {
evalTest('USUALLY_PASSES', {
name: 'Agent uses AskUser tool to present multiple choice options',
prompt: `Use the ask_user tool to ask me what my favorite color is. Provide 3 options: red, green, or blue.`,
assert: async (rig) => {
const wasToolCalled = await rig.waitForToolCall('ask_user');
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
},
});
evalTest('USUALLY_PASSES', {
name: 'Agent uses AskUser tool to clarify ambiguous requirements',
files: {
'package.json': JSON.stringify({ name: 'my-app', version: '1.0.0' }),
},
prompt: `I want to build a new feature in this app. Ask me questions to clarify the requirements before proceeding.`,
assert: async (rig) => {
const wasToolCalled = await rig.waitForToolCall('ask_user');
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
},
});
evalTest('USUALLY_PASSES', {
name: 'Agent uses AskUser tool before performing significant ambiguous rework',
files: {
'packages/core/src/index.ts': '// index\nexport const version = "1.0.0";',
'packages/core/src/util.ts': '// util\nexport function help() {}',
'packages/core/package.json': JSON.stringify({
name: '@google/gemini-cli-core',
}),
'README.md': '# Gemini CLI',
},
prompt: `Refactor the entire core package to be better.`,
assert: async (rig) => {
const wasPlanModeCalled = await rig.waitForToolCall('enter_plan_mode');
expect(wasPlanModeCalled, 'Expected enter_plan_mode to be called').toBe(
true,
);
const wasAskUserCalled = await rig.waitForToolCall('ask_user');
expect(
wasAskUserCalled,
'Expected ask_user tool to be called to clarify the significant rework',
).toBe(true);
},
});
// --- Regression Tests for Recent Fixes ---
// Regression test for issue #20177: Ensure the agent does not use `ask_user` to
// confirm shell commands. Fixed via prompt refinements and tool definition
// updates to clarify that shell command confirmation is handled by the UI.
// See fix: https://github.com/google-gemini/gemini-cli/pull/20504
evalTest('USUALLY_PASSES', {
name: 'Agent does NOT use AskUser to confirm shell commands',
files: {
'package.json': JSON.stringify({
scripts: { build: 'echo building' },
}),
},
prompt: `Run 'npm run build' in the current directory.`,
assert: async (rig) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const wasShellCalled = toolLogs.some(
(log) => log.toolRequest.name === 'run_shell_command',
);
const wasAskUserCalled = toolLogs.some(
(log) => log.toolRequest.name === 'ask_user',
);
expect(
wasShellCalled,
'Expected run_shell_command tool to be called',
).toBe(true);
expect(
wasAskUserCalled,
'ask_user should not be called to confirm shell commands',
).toBe(false);
},
});
});
+2 -3
View File
@@ -165,15 +165,14 @@ describe('Hooks Agent Flow', () => {
// BeforeModel hook to track message counts across LLM calls
const messageCountFile = join(rig.testDir!, 'message-counts.json');
const escapedPath = JSON.stringify(messageCountFile);
const beforeModelScript = `
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
const messageCount = input.llm_request?.contents?.length || 0;
let counts = [];
try { counts = JSON.parse(fs.readFileSync(${escapedPath}, 'utf-8')); } catch (e) {}
try { counts = JSON.parse(fs.readFileSync(${JSON.stringify(messageCountFile)}, 'utf-8')); } catch (e) {}
counts.push(messageCount);
fs.writeFileSync(${escapedPath}, JSON.stringify(counts));
fs.writeFileSync(${JSON.stringify(messageCountFile)}, JSON.stringify(counts));
console.log(JSON.stringify({ decision: 'allow' }));
`;
const beforeModelScriptPath = rig.createScript(
+1 -3
View File
@@ -81,9 +81,7 @@ describe('JSON output', () => {
const message = (thrown as Error).message;
// Use a regex to find the first complete JSON object in the string
// We expect the JSON to start with a quote (e.g. {"error": ...}) to avoid
// matching random error objects printed to stderr (like ENOENT).
const jsonMatch = message.match(/{\s*"[\s\S]*}/);
const jsonMatch = message.match(/{[\s\S]*}/);
// Fail if no JSON-like text was found
expect(
+32 -85
View File
@@ -4,10 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, checkModelOutputContent, GEMINI_DIR } from './test-helper.js';
import { TestRig, checkModelOutputContent } from './test-helper.js';
describe('Plan Mode', () => {
let rig: TestRig;
@@ -64,98 +62,50 @@ describe('Plan Mode', () => {
});
});
it('should allow write_file to the plans directory in plan mode', async () => {
const plansDir = '.gemini/tmp/foo/123/plans';
const testName =
'should allow write_file to the plans directory in plan mode';
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
it.skip('should allow write_file only in the plans directory in plan mode', async () => {
await rig.setup(
'should allow write_file only in the plans directory in plan mode',
{
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
allowed: ['write_file'],
},
general: { defaultApprovalMode: 'plan' },
},
},
});
// Disable the interactive terminal setup prompt in tests
writeFileSync(
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
);
const run = await rig.runInteractive({
// We ask the agent to create a plan for a feature, which should trigger a write_file in the plans directory.
// Verify that write_file outside of plan directory fails
await rig.run({
approvalMode: 'plan',
stdin:
'Create a file called plan.md in the plans directory. Then create a file called hello.txt in the current directory',
});
await run.type('Create a file called plan.md in the plans directory.');
await run.type('\r');
await rig.expectToolCallSuccess(['write_file'], 30000, (args) =>
args.includes('plan.md'),
);
const toolLogs = rig.readToolLogs();
const planWrite = toolLogs.find(
const writeLogs = toolLogs.filter(
(l) => l.toolRequest.name === 'write_file',
);
const planWrite = writeLogs.find(
(l) =>
l.toolRequest.name === 'write_file' &&
l.toolRequest.args.includes('plans') &&
l.toolRequest.args.includes('plan.md'),
);
expect(planWrite?.toolRequest.success).toBe(true);
});
it('should deny write_file to non-plans directory in plan mode', async () => {
const plansDir = '.gemini/tmp/foo/123/plans';
const testName =
'should deny write_file to non-plans directory in plan mode';
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
},
},
},
});
// Disable the interactive terminal setup prompt in tests
writeFileSync(
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
const blockedWrite = writeLogs.find((l) =>
l.toolRequest.args.includes('hello.txt'),
);
const run = await rig.runInteractive({
approvalMode: 'plan',
});
await run.type('Create a file called hello.txt in the current directory.');
await run.type('\r');
const toolLogs = rig.readToolLogs();
const writeLog = toolLogs.find(
(l) =>
l.toolRequest.name === 'write_file' &&
l.toolRequest.args.includes('hello.txt'),
);
// In Plan Mode, writes outside the plans directory should be blocked.
// Model is undeterministic, sometimes it doesn't even try, but if it does, it must fail.
if (writeLog) {
expect(writeLog.toolRequest.success).toBe(false);
// Model is undeterministic, sometimes a blocked write appears in tool logs and sometimes it doesn't
if (blockedWrite) {
expect(blockedWrite?.toolRequest.success).toBe(false);
}
expect(planWrite?.toolRequest.success).toBe(true);
});
it('should be able to enter plan mode from default mode', async () => {
@@ -169,12 +119,6 @@ describe('Plan Mode', () => {
},
});
// Disable the interactive terminal setup prompt in tests
writeFileSync(
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
);
// Start in default mode and ask to enter plan mode.
await rig.run({
approvalMode: 'default',
@@ -182,7 +126,10 @@ describe('Plan Mode', () => {
'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
});
const enterPlanCallFound = await rig.waitForToolCall('enter_plan_mode');
const enterPlanCallFound = await rig.waitForToolCall(
'enter_plan_mode',
10000,
);
expect(enterPlanCallFound, 'Expected enter_plan_mode to be called').toBe(
true,
);
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I will read the content of the file to identify its"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7969,"candidatesTokenCount":11,"totalTokenCount":8061,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7969}],"thoughtsTokenCount":81}},{"candidates":[{"content":{"parts":[{"text":" language.\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7969,"candidatesTokenCount":14,"totalTokenCount":8064,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7969}],"thoughtsTokenCount":81}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_file","args":{"file_path":"test.txt"}},"thoughtSignature":"EvkCCvYCAb4+9vt8mJ/o45uuuAJtfjaZ3YzkJzqXHZBttRE+Om0ahcr1S5RDFp50KpgHtJtbAH1pwEXampOnDV3WKiWwA+e3Jnyk4CNQegz7ZMKsl55Nem2XDViP8BZKnJVqGmSFuMoKJLFmbVIxKejtWcblfn3httbGsrUUNbHwdPjPHo1qY043lF63g0kWx4v68gPSsJpNhxLrSugKKjiyRFN+J0rOIBHI2S9MdZoHEKhJxvGMtXiJquxmhPmKcNEsn+hMdXAZB39hmrRrGRHDQPVYVPhfJthVc73ufzbn+5KGJpaMQyKY5hqrc2ea8MHz+z6BSx+tFz4NZBff1tJQOiUp09/QndxQRZHSQZr1ALGy0O1Qw4JqsX94x81IxtXqYkSRo3zgm2vl/xPMC5lKlnK5xoKJmoWaHkUNeXs/sopu3/Waf1a5Csoh9ImnKQsW0rJ6GRyDQvky1FwR6Aa98bgfNdcXOPHml/BtghaqRMXTiG6vaPJ8UFs="}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7969,"candidatesTokenCount":64,"totalTokenCount":8114,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7969}],"thoughtsTokenCount":81}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":7969,"candidatesTokenCount":64,"totalTokenCount":8114,"cachedContentTokenCount":6082,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7969}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":6082}],"thoughtsTokenCount":81}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The language of the file is Latin."}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":8054,"candidatesTokenCount":8,"totalTokenCount":8078,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8054}],"thoughtsTokenCount":16}},{"candidates":[{"content":{"parts":[{"text":"","thoughtSignature":"EnIKcAG+Pvb7vnRBJVz3khx1oArQQqTNvXOXkliNQS7NvYw94dq5m+wGKRmSj3egO3GVp7pacnAtLn9NT1ABKBGpa7MpRhiAe3bbPZfkqOuveeyC19LKQ9fzasCywiYqg5k5qSxfjs5okk+O0NLOvTjN/tg="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":8135,"candidatesTokenCount":8,"totalTokenCount":8159,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8135}],"thoughtsTokenCount":16}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I will run the requested"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":5,"totalTokenCount":8092,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"thoughtsTokenCount":138}},{"candidates":[{"content":{"parts":[{"text":" shell command to verify the policy configuration.\n"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":14,"totalTokenCount":8101,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"thoughtsTokenCount":138}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"run_shell_command","args":{"command":"echo POLICY_TEST_ECHO_COMMAND","description":"Echo the test string to verify policy settings."}},"thoughtSignature":"EpwFCpkFAb4+9vulXgVj96CAm2eMFbDEGHz9B37GwI8N1KOvu9AHwdYWiita7yS4RKAdeBui22B5320XBaxOtZGnMo2E9pG0Pcus2WsBiecRaHUTxTmhx1BvURevrs+5m4UJeLRGMfP94+ncha4DeIQod3PKBnK8xeIJTyZBFB7+hmHbHvem2VwZh/v14e4fXlpEkkdntJbzrA1nUdctIGdEmdm0sL8PaFnMqWLUnkZvGdfq7ctFt9EYk2HW2SrHVhk3HdsyWhoxNz2MU0sRWzAgiSQY/heSSAbU7Jdgg0RjwB9o3SkCIHxqnVpkH8PQsARwnah5I5s7pW6EHr3D4f1/UVl0n26hyI2xBqF/n4aZKhtX55U4h/DIhxooZa2znstt6BS8vRcdzflFrX7OV86WQxHE4JHjQecP2ciBRimm8pL3Od3pXnRcx32L8JbrWm6dPyWlo5h5uCRy0qXye2+3SuHs5wtxOjD9NETR4TwzqFe+m0zThpxsR1ZKQeKlO7lN/s3pWih/TjbZQEQs9xr72UnlE8ZtJ4bOKj8GNbemvsrbYAO98NzJwvdil0FhblaXmReP1uYjucmLC0jCJHShqNz2KzAkDTvKs4tmio13IuCRjTZ3E5owqCUn7djDqOSDwrg235RIVJkiDIaPlHemOR15lbVQD1VOzytzT8TZLEzTV750oyHq/IhLMQHYixO8jJ2GkVvUp7bxz9oQ4UeTqT5lTF4s40H2Rlkb6trF4hKXoFhzILy1aOJTC9W3fCoop7VJLIMNulgHLWxiq65Uas6sIep87yiD4xLfbGfMm6HS4JTRhPlfxeckn/SzUfu1afg1nAvW3vBlR/YNREf0N28/PnRC08VYqA3mqCRiyPqPWsf3a0jyio0dD9A="}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":54,"totalTokenCount":8141,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"thoughtsTokenCount":138}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":54,"totalTokenCount":8141,"cachedContentTokenCount":6082,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":6082}],"thoughtsTokenCount":138}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"POLICY_TEST_"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":8042,"candidatesTokenCount":4,"totalTokenCount":8046,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8042}]}},{"candidates":[{"content":{"parts":[{"text":"ECHO_COMMAND"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":8042,"candidatesTokenCount":8,"totalTokenCount":8050,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8042}]}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":8180,"candidatesTokenCount":8,"totalTokenCount":8188,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8180}]}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Assessing Command Execution**\n\nOkay, I'm currently assessing the feasibility of executing `echo POLICY_TEST_ECHO_COMMAND` using the `run_shell_command` function. Restrictions are being evaluated; the prompt is specifically geared towards a successful command output: \"POLICY_TEST_ECHO_COMMAND\".\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"totalTokenCount":7949,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}]}},{"candidates":[{"content":{"parts":[{"text":"I will execute the requested echo"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":6,"totalTokenCount":8161,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"thoughtsTokenCount":206}},{"candidates":[{"content":{"parts":[{"text":" command to verify the policy."}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":12,"totalTokenCount":8167,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"thoughtsTokenCount":206}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"run_shell_command","args":{"description":"Execute the echo command as requested.","command":"echo POLICY_TEST_ECHO_COMMAND"}},"thoughtSignature":"EvkGCvYGAb4+9vucYbmJ8DrNCca9c0C8o4qKQ6V2WnzmT4mbCw8V7s0+2I/PoxrgnsxZJIIRM8y5E4bW7Jbs46GjbJ2cefY9Q3iC45eiGS5Gqvq0eAG04N3GZRwizyDOp+wJlBsaPu1cNB1t6CnMk/ZHDAHEIQUpYfYWmPudbHOQMspGMu3bX23YSI1+Q5vPVdOtM16J3EFbk3dCp+RnPa/8tVC+5AqFlLveuDbJXtrLN9wAyf4SjnPhn9BPfD0bgas3+gF03qRJvWoNcnnJiYxL3DNQtjsAYJ7IWRzciYYZSTm99blD730bn3NzvSObhlHDtb3hFpApYvG396+3prsgJg0Yjef54B4KxHfZaQbE2ndSP5zGrwLtVD5y7XJAYskvhiUqwPFHNVykqroEMzPn8wWQSGvonNR6ezcMIsUV5xwnxZDaPhvrDdIwF4NR1F5DeriJRu27+fwtCApeYkx9mPx4LqnyxOuVsILjzdSPHE6Bqf690VJSXpo67lCN4F3DRRYIuCD4UOlf8V3dvUO6BKjvChDDWnIq7KPoByDQT9VhVlZvS3/nYlkeDuhi0rk2jpByN1NdgD2YSvOlpJcka8JqKQ+lnO/7Swunij2ISUfpL2hkx6TEHjebPU2dBQkub5nSl9J1EhZn4sUGG5r6Zdv1lYcpIcO4ZYeMqZZ4uNvTvSpGdT4Jj1+qS88taKgYq7uN1RgQSTsT5wcpmlubIpgIycNwAIRFvN+DjkQjiUC6hSqdeOx3dc7LWgC/O/+PRog7kuFrD2nzih+oIP0YxXrLA9CMVPlzeAgPUi9b75HAJQ92GRHxfQ163tjZY+4bWmJtcU4NBqGH0x/jLEU9xCojTeh+mZoUDGsb3N+bVcGJftRIet7IBYveD29Z+XHtKhf7s/YIkFW8lgsG8Q0EtNchCxqIQxf9UjYEO52RhCx7i7zScB1knovt2HAotACKqDdPqg18PmpDv8Frw6Y66XeCCJzBCmNcSUTETq3K05gwkU8nyANQtjbJT0wF4LS9h5vPE+Vc7/dGH6pi1TgxWB/n4q1IXfNqilo/h2Pyw01VPsHKthNtKKq1/nSW/WuEU0rimqu7wHplMqU2nwRDCTNE9pPO59RtTHMfUxxd8yEgKBj9L8MiQGM5isIYl/lJtvucee4HD9iLpbYADlrQAlUCd0rg/z+5sQ=="}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":50,"totalTokenCount":8205,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"thoughtsTokenCount":206}},{"candidates":[{"content":{"parts":[{"text":""}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":7949,"candidatesTokenCount":50,"totalTokenCount":8205,"cachedContentTokenCount":6082,"promptTokensDetails":[{"modality":"TEXT","tokenCount":7949}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":6082}],"thoughtsTokenCount":206}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"AR NAR"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":8020,"candidatesTokenCount":2,"totalTokenCount":8049,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8020}],"thoughtsTokenCount":27}},{"candidates":[{"content":{"parts":[{"text":"","thoughtSignature":"Er8BCrwBAb4+9vv6KGeMf6yopmPBE/az7Kjdp+Pe5a/R6wgXcyCZzGNwkwKFW3i3ro0j26bRrVeHD1zRfWFTIGdOSZKV6OMPWLqFC/RU6CNJ88B1xY7hbCVwA7EchYPzgd3YZRVNwmFu52j86/9qXf/zaqTFN+WQ0mUESJXh2O2YX8E7imAvxhmRdobVkxvEt4ZX3dW5skDhXHMDZOxbLpX0nkK7cWWS7iEc+qBFP0yinlA/eiG2ZdKpuTiDl76a9ik="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":8226,"candidatesTokenCount":2,"totalTokenCount":8255,"promptTokensDetails":[{"modality":"TEXT","tokenCount":8226}],"thoughtsTokenCount":27}}]}
-192
View File
@@ -1,192 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { join } from 'node:path';
import { TestRig } from './test-helper.js';
interface PromptCommand {
prompt: (testFile: string) => string;
tool: string;
command: string;
expectedSuccessResult: string;
expectedFailureResult: string;
}
const ECHO_PROMPT: PromptCommand = {
command: 'echo',
prompt: () =>
`Use the \`echo POLICY_TEST_ECHO_COMMAND\` shell command. On success, ` +
`your final response must ONLY be "POLICY_TEST_ECHO_COMMAND". If the ` +
`command fails output AR NAR and stop.`,
tool: 'run_shell_command',
expectedSuccessResult: 'POLICY_TEST_ECHO_COMMAND',
expectedFailureResult: 'AR NAR',
};
const READ_FILE_PROMPT: PromptCommand = {
prompt: (testFile: string) =>
`Read the file ${testFile} and tell me what language it is, if the ` +
`read_file tool fails output AR NAR and stop.`,
tool: 'read_file',
command: '',
expectedSuccessResult: 'Latin',
expectedFailureResult: 'AR NAR',
};
async function waitForToolCallLog(
rig: TestRig,
tool: string,
command: string,
timeout: number = 15000,
) {
const foundToolCall = await rig.waitForToolCall(tool, timeout, (args) =>
args.toLowerCase().includes(command.toLowerCase()),
);
expect(foundToolCall).toBe(true);
const toolLogs = rig
.readToolLogs()
.filter((toolLog) => toolLog.toolRequest.name === tool);
const log = toolLogs.find(
(toolLog) =>
!command ||
toolLog.toolRequest.args.toLowerCase().includes(command.toLowerCase()),
);
// The policy engine should have logged the tool call
expect(log).toBeTruthy();
return log;
}
async function verifyToolExecution(
rig: TestRig,
promptCommand: PromptCommand,
result: string,
expectAllowed: boolean,
) {
const log = await waitForToolCallLog(
rig,
promptCommand.tool,
promptCommand.command,
);
if (expectAllowed) {
expect(log!.toolRequest.success).toBe(true);
expect(result).not.toContain('Tool execution denied by policy');
expect(result).toContain(promptCommand.expectedSuccessResult);
} else {
expect(log!.toolRequest.success).toBe(false);
expect(result).toContain('Tool execution denied by policy');
expect(result).toContain(promptCommand.expectedFailureResult);
}
}
interface TestCase {
name: string;
responsesFile: string;
promptCommand: PromptCommand;
policyContent?: string;
expectAllowed: boolean;
}
describe('Policy Engine Headless Mode', () => {
let rig: TestRig;
let testFile: string;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
if (rig) {
await rig.cleanup();
}
});
const runTestCase = async (tc: TestCase) => {
const fakeResponsesPath = join(import.meta.dirname, tc.responsesFile);
rig.setup(tc.name, { fakeResponsesPath });
testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
const args = ['-p', tc.promptCommand.prompt(testFile)];
if (tc.policyContent) {
const policyPath = rig.createFile('test-policy.toml', tc.policyContent);
args.push('--policy', policyPath);
}
const result = await rig.run({
args,
approvalMode: 'default',
});
await verifyToolExecution(rig, tc.promptCommand, result, tc.expectAllowed);
};
const testCases = [
{
name: 'should deny ASK_USER tools by default in headless mode',
responsesFile: 'policy-headless-shell-denied.responses',
promptCommand: ECHO_PROMPT,
expectAllowed: false,
},
{
name: 'should allow ASK_USER tools in headless mode if explicitly allowed via policy file',
responsesFile: 'policy-headless-shell-allowed.responses',
promptCommand: ECHO_PROMPT,
policyContent: `
[[rule]]
toolName = "run_shell_command"
decision = "allow"
priority = 100
`,
expectAllowed: true,
},
{
name: 'should allow read-only tools by default in headless mode',
responsesFile: 'policy-headless-readonly.responses',
promptCommand: READ_FILE_PROMPT,
expectAllowed: true,
},
{
name: 'should allow specific shell commands in policy file',
responsesFile: 'policy-headless-shell-allowed.responses',
promptCommand: ECHO_PROMPT,
policyContent: `
[[rule]]
toolName = "run_shell_command"
commandPrefix = "${ECHO_PROMPT.command}"
decision = "allow"
priority = 100
`,
expectAllowed: true,
},
{
name: 'should deny other shell commands in policy file',
responsesFile: 'policy-headless-shell-denied.responses',
promptCommand: ECHO_PROMPT,
policyContent: `
[[rule]]
toolName = "run_shell_command"
commandPrefix = "node"
decision = "allow"
priority = 100
`,
expectAllowed: false,
},
];
it.each(testCases)(
'$name',
async (tc) => {
await runTestCase(tc);
},
// Large timeout for regeneration
process.env['REGENERATE_MODEL_GOLDENS'] === 'true' ? 120000 : undefined,
);
});
+1 -6
View File
@@ -18,7 +18,6 @@ const { shell } = getShellConfiguration();
function getLineCountCommand(): { command: string; tool: string } {
switch (shell) {
case 'powershell':
return { command: `Measure-Object -Line`, tool: 'Measure-Object' };
case 'cmd':
return { command: `find /c /v`, tool: 'find' };
case 'bash':
@@ -239,12 +238,8 @@ describe('run_shell_command', () => {
});
it('should succeed in yolo mode', async () => {
const isWindows = process.platform === 'win32';
await rig.setup('should succeed in yolo mode', {
settings: {
tools: { core: ['run_shell_command'] },
shell: isWindows ? { enableInteractiveShell: false } : undefined,
},
settings: { tools: { core: ['run_shell_command'] } },
});
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
+23 -270
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"workspaces": [
"packages/*"
],
@@ -6298,36 +6298,16 @@
"node": ">= 12"
}
},
"node_modules/clipboard-image": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/clipboard-image/-/clipboard-image-0.1.0.tgz",
"integrity": "sha512-SWk7FgaXLNFld19peQ/rTe0n97lwR1WbkqxV6JKCAOh7U52AKV/PeMFCyt/8IhBdqyDA8rdyewQMKZqvWT5Akg==",
"license": "MIT",
"dependencies": {
"run-jxa": "^3.0.0"
},
"bin": {
"clipboard-image": "cli.js"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clipboardy": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-5.2.1.tgz",
"integrity": "sha512-RWp4E/ivQAzgF4QSWA9sjeW+Bjo+U2SvebkDhNIfO7y65eGdXPUxMTdIKYsn+bxM3ItPHGm3e68Bv3fgQ3mARw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-5.0.0.tgz",
"integrity": "sha512-MQfKHaD09eP80Pev4qBxZLbxJK/ONnqfSYAPlCmPh+7BDboYtO/3BmB6HGzxDIT0SlTRc2tzS8lQqfcdLtZ0Kg==",
"license": "MIT",
"dependencies": {
"clipboard-image": "^0.1.0",
"execa": "^9.6.1",
"execa": "^9.6.0",
"is-wayland": "^0.1.0",
"is-wsl": "^3.1.0",
"is64bit": "^2.0.0",
"powershell-utils": "^0.2.0"
"is64bit": "^2.0.0"
},
"engines": {
"node": ">=20"
@@ -6583,9 +6563,6 @@
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">=18"
},
@@ -6752,33 +6729,6 @@
"node": ">= 8"
}
},
"node_modules/crypto-random-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
"integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==",
"license": "MIT",
"dependencies": {
"type-fest": "^1.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crypto-random-string/node_modules/type-fest": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -8469,9 +8419,9 @@
}
},
"node_modules/execa": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
"integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
@@ -8589,15 +8539,6 @@
"express": ">= 4.11"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -8849,24 +8790,13 @@
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 0.8"
"node": ">= 18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -11741,21 +11671,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/macos-version": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/macos-version/-/macos-version-6.0.0.tgz",
"integrity": "sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -11891,12 +11806,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -13447,18 +13356,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/powershell-utils": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.0.tgz",
"integrity": "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -14341,107 +14238,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-jxa": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-jxa/-/run-jxa-3.0.0.tgz",
"integrity": "sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA==",
"license": "MIT",
"dependencies": {
"execa": "^5.1.1",
"macos-version": "^6.0.0",
"subsume": "^4.0.0",
"type-fest": "^2.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-jxa/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/run-jxa/node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-jxa/node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.17.0"
}
},
"node_modules/run-jxa/node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"license": "MIT",
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/run-jxa/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/run-jxa/node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/run-jxa/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -15378,34 +15174,6 @@
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
"license": "MIT"
},
"node_modules/subsume": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/subsume/-/subsume-4.0.0.tgz",
"integrity": "sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg==",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^5.0.0",
"unique-string": "^3.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/subsume/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/superagent": {
"version": "10.2.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
@@ -16388,21 +16156,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unique-string": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz",
"integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==",
"license": "MIT",
"dependencies": {
"crypto-random-string": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
@@ -17303,7 +17056,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
@@ -17361,7 +17114,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -17373,7 +17126,7 @@
"ansi-regex": "^6.2.2",
"chalk": "^4.1.2",
"cli-spinners": "^2.9.2",
"clipboardy": "~5.2.0",
"clipboardy": "^5.0.0",
"color-convert": "^2.0.1",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
@@ -17444,7 +17197,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
@@ -17709,7 +17462,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -17724,7 +17477,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17741,7 +17494,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17758,7 +17511,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+4 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260304.28af4e127"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.33.0-nightly.20260228.1ca5c05d0"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -37,12 +37,10 @@
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"build:binary": "node scripts/build_binary.js",
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present && npm run test:sea-launch",
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts && npm run test:sea-launch",
"test": "npm run test --workspaces --if-present",
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
"test:sea-launch": "vitest run sea/sea-launch.test.js",
"test:always_passing_evals": "vitest run --config evals/vitest.config.ts",
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+4 -15
View File
@@ -27,11 +27,7 @@ import {
type ToolCallConfirmationDetails,
type Config,
type UserTierId,
type ToolLiveOutput,
type AnsiLine,
type AnsiOutput,
type AnsiToken,
isSubagentProgress,
EDIT_TOOL_NAMES,
processRestorableToolCalls,
} from '@google/gemini-cli-core';
@@ -340,22 +336,15 @@ export class Task {
private _schedulerOutputUpdate(
toolCallId: string,
outputChunk: ToolLiveOutput,
outputChunk: string | AnsiOutput,
): void {
let outputAsText: string;
if (typeof outputChunk === 'string') {
outputAsText = outputChunk;
} else if (isSubagentProgress(outputChunk)) {
outputAsText = JSON.stringify(outputChunk);
} else if (Array.isArray(outputChunk)) {
const ansiOutput: AnsiOutput = outputChunk;
outputAsText = ansiOutput
.map((line: AnsiLine) =>
line.map((token: AnsiToken) => token.text).join(''),
)
.join('\n');
} else {
outputAsText = String(outputChunk);
outputAsText = outputChunk
.map((line) => line.map((token) => token.text).join(''))
.join('\n');
}
logger.info(
+7 -225
View File
@@ -16,9 +16,6 @@ import {
ExperimentFlags,
fetchAdminControlsOnce,
type FetchAdminControlsResponse,
AuthType,
isHeadlessMode,
FatalAuthenticationError,
} from '@google/gemini-cli-core';
// Mock dependencies
@@ -53,7 +50,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
startupProfiler: {
flush: vi.fn(),
},
isHeadlessMode: vi.fn().mockReturnValue(false),
FileDiscoveryService: vi.fn(),
getCodeAssistServer: vi.fn(),
fetchAdminControlsOnce: vi.fn(),
@@ -66,7 +62,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
vi.mock('../utils/logger.js', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
@@ -78,11 +73,12 @@ describe('loadConfig', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv('GEMINI_API_KEY', 'test-key');
process.env['GEMINI_API_KEY'] = 'test-key';
});
afterEach(() => {
vi.unstubAllEnvs();
delete process.env['CUSTOM_IGNORE_FILE_PATHS'];
delete process.env['GEMINI_API_KEY'];
});
describe('admin settings overrides', () => {
@@ -203,7 +199,7 @@ describe('loadConfig', () => {
it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => {
const testPath = '/tmp/ignore';
vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath);
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
@@ -228,7 +224,7 @@ describe('loadConfig', () => {
it('should merge customIgnoreFilePaths from settings and env var', async () => {
const envPath = '/env/ignore';
const settingsPath = '/settings/ignore';
vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', envPath);
process.env['CUSTOM_IGNORE_FILE_PATHS'] = envPath;
const settings: Settings = {
fileFiltering: {
customIgnoreFilePaths: [settingsPath],
@@ -244,7 +240,7 @@ describe('loadConfig', () => {
it('should split CUSTOM_IGNORE_FILE_PATHS using system delimiter', async () => {
const paths = ['/path/one', '/path/two'];
vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', paths.join(path.delimiter));
process.env['CUSTOM_IGNORE_FILE_PATHS'] = paths.join(path.delimiter);
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual(paths);
@@ -258,7 +254,7 @@ describe('loadConfig', () => {
it('should initialize FileDiscoveryService with correct options', async () => {
const testPath = '/tmp/ignore';
vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath);
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
const settings: Settings = {
fileFiltering: {
respectGitIgnore: false,
@@ -315,219 +311,5 @@ describe('loadConfig', () => {
}),
);
});
describe('interactivity', () => {
it('should set interactive true when not headless', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(false);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
interactive: true,
enableInteractiveShell: true,
}),
);
});
it('should set interactive false when headless', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(true);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
interactive: false,
enableInteractiveShell: false,
}),
);
});
});
describe('authentication fallback', () => {
beforeEach(() => {
vi.stubEnv('USE_CCPA', 'true');
vi.stubEnv('GEMINI_API_KEY', '');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should fall back to COMPUTE_ADC in Cloud Shell if LOGIN_WITH_GOOGLE fails', async () => {
vi.stubEnv('CLOUD_SHELL', 'true');
vi.mocked(isHeadlessMode).mockReturnValue(false);
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
throw new FatalAuthenticationError('Non-interactive session');
}
return Promise.resolve();
});
// Update the mock implementation for this test
vi.mocked(Config).mockImplementation(
(params: unknown) =>
({
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: refreshAuthMock,
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
}) as unknown as Config,
);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(refreshAuthMock).toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
});
it('should not fall back to COMPUTE_ADC if not in cloud environment', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(false);
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
throw new FatalAuthenticationError('Non-interactive session');
}
return Promise.resolve();
});
vi.mocked(Config).mockImplementation(
(params: unknown) =>
({
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: refreshAuthMock,
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
}) as unknown as Config,
);
await expect(
loadConfig(mockSettings, mockExtensionLoader, taskId),
).rejects.toThrow('Non-interactive session');
expect(refreshAuthMock).toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
);
expect(refreshAuthMock).not.toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
});
it('should skip LOGIN_WITH_GOOGLE and use COMPUTE_ADC directly in headless Cloud Shell', async () => {
vi.stubEnv('CLOUD_SHELL', 'true');
vi.mocked(isHeadlessMode).mockReturnValue(true);
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
vi.mocked(Config).mockImplementation(
(params: unknown) =>
({
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: refreshAuthMock,
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
}) as unknown as Config,
);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(refreshAuthMock).not.toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
});
it('should skip LOGIN_WITH_GOOGLE and use COMPUTE_ADC directly if GEMINI_CLI_USE_COMPUTE_ADC is true', async () => {
vi.stubEnv('GEMINI_CLI_USE_COMPUTE_ADC', 'true');
vi.mocked(isHeadlessMode).mockReturnValue(false); // Even if not headless
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
vi.mocked(Config).mockImplementation(
(params: unknown) =>
({
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: refreshAuthMock,
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
}) as unknown as Config,
);
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(refreshAuthMock).not.toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.COMPUTE_ADC);
});
it('should throw FatalAuthenticationError in headless mode if no ADC fallback available', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(true);
const refreshAuthMock = vi.fn().mockResolvedValue(undefined);
vi.mocked(Config).mockImplementation(
(params: unknown) =>
({
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: refreshAuthMock,
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
}) as unknown as Config,
);
await expect(
loadConfig(mockSettings, mockExtensionLoader, taskId),
).rejects.toThrow(
'Interactive terminal required for LOGIN_WITH_GOOGLE. Run in an interactive terminal or set GEMINI_CLI_USE_COMPUTE_ADC=true to use Application Default Credentials.',
);
expect(refreshAuthMock).not.toHaveBeenCalled();
});
it('should include both original and fallback error when COMPUTE_ADC fallback fails', async () => {
vi.stubEnv('CLOUD_SHELL', 'true');
vi.mocked(isHeadlessMode).mockReturnValue(false);
const refreshAuthMock = vi.fn().mockImplementation((authType) => {
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
throw new FatalAuthenticationError('OAuth failed');
}
if (authType === AuthType.COMPUTE_ADC) {
throw new Error('ADC failed');
}
return Promise.resolve();
});
vi.mocked(Config).mockImplementation(
(params: unknown) =>
({
...(params as object),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
refreshAuth: refreshAuthMock,
getExperiments: vi.fn().mockReturnValue({ flags: {} }),
getRemoteAdminSettings: vi.fn(),
setRemoteAdminSettings: vi.fn(),
}) as unknown as Config,
);
await expect(
loadConfig(mockSettings, mockExtensionLoader, taskId),
).rejects.toThrow(
'OAuth failed. Fallback to COMPUTE_ADC also failed: ADC failed',
);
});
});
});
});
+3 -60
View File
@@ -23,9 +23,6 @@ import {
fetchAdminControlsOnce,
getCodeAssistServer,
ExperimentFlags,
isHeadlessMode,
FatalAuthenticationError,
isCloudShell,
type TelemetryTarget,
type ConfigParameters,
type ExtensionLoader,
@@ -106,8 +103,8 @@ export async function loadConfig(
trustedFolder: true,
extensionLoader,
checkpointing,
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
interactive: true,
enableInteractiveShell: true,
ptyInfo: 'auto',
};
@@ -258,61 +255,7 @@ async function refreshAuthentication(
`[${logPrefix}] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`,
);
}
const useComputeAdc = process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true';
const isHeadless = isHeadlessMode();
const shouldSkipOauth = isHeadless || useComputeAdc;
if (shouldSkipOauth) {
if (isCloudShell() || useComputeAdc) {
logger.info(
`[${logPrefix}] Skipping LOGIN_WITH_GOOGLE due to ${isHeadless ? 'headless mode' : 'GEMINI_CLI_USE_COMPUTE_ADC'}. Attempting COMPUTE_ADC.`,
);
try {
await config.refreshAuth(AuthType.COMPUTE_ADC);
logger.info(`[${logPrefix}] COMPUTE_ADC successful.`);
} catch (adcError) {
const adcMessage =
adcError instanceof Error ? adcError.message : String(adcError);
throw new FatalAuthenticationError(
`COMPUTE_ADC failed: ${adcMessage}. (Skipped LOGIN_WITH_GOOGLE due to ${isHeadless ? 'headless mode' : 'GEMINI_CLI_USE_COMPUTE_ADC'})`,
);
}
} else {
throw new FatalAuthenticationError(
`Interactive terminal required for LOGIN_WITH_GOOGLE. Run in an interactive terminal or set GEMINI_CLI_USE_COMPUTE_ADC=true to use Application Default Credentials.`,
);
}
} else {
try {
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
} catch (e) {
if (
e instanceof FatalAuthenticationError &&
(isCloudShell() || useComputeAdc)
) {
logger.warn(
`[${logPrefix}] LOGIN_WITH_GOOGLE failed. Attempting COMPUTE_ADC fallback.`,
);
try {
await config.refreshAuth(AuthType.COMPUTE_ADC);
logger.info(`[${logPrefix}] COMPUTE_ADC fallback successful.`);
} catch (adcError) {
logger.error(
`[${logPrefix}] COMPUTE_ADC fallback failed: ${adcError}`,
);
const originalMessage = e instanceof Error ? e.message : String(e);
const adcMessage =
adcError instanceof Error ? adcError.message : String(adcError);
throw new FatalAuthenticationError(
`${originalMessage}. Fallback to COMPUTE_ADC also failed: ${adcMessage}`,
);
}
} else {
throw e;
}
}
}
await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
logger.info(
`[${logPrefix}] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`,
);
+5 -44
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import express, { type Request } from 'express';
import express from 'express';
import type { AgentCard, Message } from '@a2a-js/sdk';
import {
@@ -13,9 +13,8 @@ import {
InMemoryTaskStore,
DefaultExecutionEventBus,
type AgentExecutionEvent,
UnauthenticatedUser,
} from '@a2a-js/sdk/server';
import { A2AExpressApp, type UserBuilder } from '@a2a-js/sdk/server/express'; // Import server components
import { A2AExpressApp } from '@a2a-js/sdk/server/express'; // Import server components
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../utils/logger.js';
import type { AgentSettings } from '../types.js';
@@ -56,17 +55,8 @@ const coderAgentCard: AgentCard = {
pushNotifications: false,
stateTransitionHistory: true,
},
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
basicAuth: {
type: 'http',
scheme: 'basic',
},
},
security: [{ bearerAuth: [] }, { basicAuth: [] }],
securitySchemes: undefined,
security: undefined,
defaultInputModes: ['text'],
defaultOutputModes: ['text'],
skills: [
@@ -91,35 +81,6 @@ export function updateCoderAgentCardUrl(port: number) {
coderAgentCard.url = `http://localhost:${port}/`;
}
const customUserBuilder: UserBuilder = async (req: Request) => {
const auth = req.headers['authorization'];
if (auth) {
const scheme = auth.split(' ')[0];
logger.info(
`[customUserBuilder] Received Authorization header with scheme: ${scheme}`,
);
}
if (!auth) return new UnauthenticatedUser();
// 1. Bearer Auth
if (auth.startsWith('Bearer ')) {
const token = auth.substring(7);
if (token === 'valid-token') {
return { userName: 'bearer-user', isAuthenticated: true };
}
}
// 2. Basic Auth
if (auth.startsWith('Basic ')) {
const credentials = Buffer.from(auth.substring(6), 'base64').toString();
if (credentials === 'admin:password') {
return { userName: 'basic-user', isAuthenticated: true };
}
}
return new UnauthenticatedUser();
};
async function handleExecuteCommand(
req: express.Request,
res: express.Response,
@@ -243,7 +204,7 @@ export async function createApp() {
requestStorage.run({ req }, next);
});
const appBuilder = new A2AExpressApp(requestHandler, customUserBuilder);
const appBuilder = new A2AExpressApp(requestHandler);
expressApp = appBuilder.setupRoutes(expressApp, '');
expressApp.use(express.json());
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260304.28af4e127",
"version": "0.33.0-nightly.20260228.1ca5c05d0",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -26,7 +26,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260304.28af4e127"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.33.0-nightly.20260228.1ca5c05d0"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -38,7 +38,7 @@
"ansi-regex": "^6.2.2",
"chalk": "^4.1.2",
"cli-spinners": "^2.9.2",
"clipboardy": "~5.2.0",
"clipboardy": "^5.0.0",
"color-convert": "^2.0.1",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
@@ -5,7 +5,6 @@
*/
import type { CommandModule } from 'yargs';
import * as path from 'node:path';
import chalk from 'chalk';
import {
debugLogger,
@@ -52,13 +51,12 @@ export async function handleInstall(args: InstallArgs) {
const settings = loadSettings(workspaceDir).merged;
if (installMetadata.type === 'local' || installMetadata.type === 'link') {
const absolutePath = path.resolve(source);
const realPath = getRealPath(absolutePath);
installMetadata.source = absolutePath;
const trustResult = isWorkspaceTrusted(settings, absolutePath);
const resolvedPath = getRealPath(source);
installMetadata.source = resolvedPath;
const trustResult = isWorkspaceTrusted(settings, resolvedPath);
if (trustResult.isTrusted !== true) {
const discoveryResults =
await FolderTrustDiscoveryService.discover(realPath);
await FolderTrustDiscoveryService.discover(resolvedPath);
const hasDiscovery =
discoveryResults.commands.length > 0 ||
@@ -71,7 +69,7 @@ export async function handleInstall(args: InstallArgs) {
'',
chalk.bold('Do you trust the files in this folder?'),
'',
`The extension source at "${absolutePath}" is not trusted.`,
`The extension source at "${resolvedPath}" is not trusted.`,
'',
'Trusting a folder allows Gemini CLI to load its local configurations,',
'including custom commands, hooks, MCP servers, agent skills, and',
@@ -129,10 +127,10 @@ export async function handleInstall(args: InstallArgs) {
);
if (confirmed) {
const trustedFolders = loadTrustedFolders();
await trustedFolders.setValue(realPath, TrustLevel.TRUST_FOLDER);
await trustedFolders.setValue(resolvedPath, TrustLevel.TRUST_FOLDER);
} else {
throw new Error(
`Installation aborted: Folder "${absolutePath}" is not trusted.`,
`Installation aborted: Folder "${resolvedPath}" is not trusted.`,
);
}
}
+27 -119
View File
@@ -19,8 +19,6 @@ import {
debugLogger,
ApprovalMode,
type MCPServerConfig,
type GeminiCLIExtension,
Storage,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import {
@@ -953,6 +951,12 @@ describe('mergeMcpServers', () => {
});
describe('mergeExcludeTools', () => {
const defaultExcludes = new Set([
SHELL_TOOL_NAME,
EDIT_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
]);
const originalIsTTY = process.stdin.isTTY;
beforeEach(() => {
@@ -1074,7 +1078,9 @@ describe('mergeExcludeTools', () => {
process.argv = ['node', 'script.js', '-p', 'test'];
const argv = await parseArguments(createTestMergedSettings());
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getExcludeTools()).toEqual(new Set([ASK_USER_TOOL_NAME]));
expect(config.getExcludeTools()).toEqual(
new Set([...defaultExcludes, ASK_USER_TOOL_NAME]),
);
});
it('should handle settings with excludeTools but no extensions', async () => {
@@ -1155,9 +1161,9 @@ describe('Approval mode tool exclusion logic', () => {
const config = await loadCliConfig(settings, 'test-session', argv);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(SHELL_TOOL_NAME);
expect(excludedTools).not.toContain(EDIT_TOOL_NAME);
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(SHELL_TOOL_NAME);
expect(excludedTools).toContain(EDIT_TOOL_NAME);
expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
});
@@ -1176,9 +1182,9 @@ describe('Approval mode tool exclusion logic', () => {
const config = await loadCliConfig(settings, 'test-session', argv);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(SHELL_TOOL_NAME);
expect(excludedTools).not.toContain(EDIT_TOOL_NAME);
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(SHELL_TOOL_NAME);
expect(excludedTools).toContain(EDIT_TOOL_NAME);
expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
});
@@ -1197,7 +1203,7 @@ describe('Approval mode tool exclusion logic', () => {
const config = await loadCliConfig(settings, 'test-session', argv);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(SHELL_TOOL_NAME);
expect(excludedTools).toContain(SHELL_TOOL_NAME);
expect(excludedTools).not.toContain(EDIT_TOOL_NAME);
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
@@ -1243,9 +1249,9 @@ describe('Approval mode tool exclusion logic', () => {
const config = await loadCliConfig(settings, 'test-session', argv);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(SHELL_TOOL_NAME);
expect(excludedTools).not.toContain(EDIT_TOOL_NAME);
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(SHELL_TOOL_NAME);
expect(excludedTools).toContain(EDIT_TOOL_NAME);
expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
});
@@ -1307,10 +1313,9 @@ describe('Approval mode tool exclusion logic', () => {
const excludedTools = config.getExcludeTools();
expect(excludedTools).toContain('custom_tool'); // From settings
expect(excludedTools).not.toContain(SHELL_TOOL_NAME); // No longer from approval mode
expect(excludedTools).toContain(SHELL_TOOL_NAME); // From approval mode
expect(excludedTools).not.toContain(EDIT_TOOL_NAME); // Should be allowed in auto_edit
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME); // Should be allowed in auto_edit
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
});
it('should throw an error if YOLO mode is attempted when disableYoloMode is true', async () => {
@@ -2157,9 +2162,9 @@ describe('loadCliConfig tool exclusions', () => {
'test-session',
argv,
);
expect(config.getExcludeTools()).not.toContain('run_shell_command');
expect(config.getExcludeTools()).not.toContain('replace');
expect(config.getExcludeTools()).not.toContain('write_file');
expect(config.getExcludeTools()).toContain('run_shell_command');
expect(config.getExcludeTools()).toContain('replace');
expect(config.getExcludeTools()).toContain('write_file');
expect(config.getExcludeTools()).toContain('ask_user');
});
@@ -2197,7 +2202,7 @@ describe('loadCliConfig tool exclusions', () => {
expect(config.getExcludeTools()).not.toContain(SHELL_TOOL_NAME);
});
it('should not exclude web-fetch in non-interactive mode at config level', async () => {
it('should exclude web-fetch in non-interactive mode when not allowed', async () => {
process.stdin.isTTY = false;
process.argv = ['node', 'script.js', '-p', 'test'];
const argv = await parseArguments(createTestMergedSettings());
@@ -2206,7 +2211,7 @@ describe('loadCliConfig tool exclusions', () => {
'test-session',
argv,
);
expect(config.getExcludeTools()).not.toContain(WEB_FETCH_TOOL_NAME);
expect(config.getExcludeTools()).toContain(WEB_FETCH_TOOL_NAME);
});
it('should not exclude web-fetch in non-interactive mode when allowed', async () => {
@@ -3319,11 +3324,11 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
// In non-interactive mode, only ask_user is excluded by default
// In non-interactive mode, ShellTool, etc. are excluded
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
exclude: expect.arrayContaining([ASK_USER_TOOL_NAME]),
exclude: expect.arrayContaining([SHELL_TOOL_NAME]),
}),
}),
expect.anything(),
@@ -3519,101 +3524,4 @@ describe('loadCliConfig mcpEnabled', () => {
expect(config.getAllowedMcpServers()).toEqual(['serverA']);
expect(config.getBlockedMcpServers()).toEqual(['serverB']);
});
describe('extension plan settings', () => {
beforeEach(() => {
vi.spyOn(Storage.prototype, 'getProjectTempDir').mockReturnValue(
'/mock/home/user/.gemini/tmp/test-project',
);
});
it('should use plan directory from active extension when user has not specified one', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
});
const argv = await parseArguments(settings);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
name: 'ext-plan',
isActive: true,
plan: { directory: 'ext-plans-dir' },
} as unknown as GeminiCLIExtension,
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getPlansDir()).toContain('ext-plans-dir');
});
it('should NOT use plan directory from active extension when user has specified one', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
general: {
plan: { directory: 'user-plans-dir' },
},
});
const argv = await parseArguments(settings);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
name: 'ext-plan',
isActive: true,
plan: { directory: 'ext-plans-dir' },
} as unknown as GeminiCLIExtension,
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getPlansDir()).toContain('user-plans-dir');
expect(config.storage.getPlansDir()).not.toContain('ext-plans-dir');
});
it('should NOT use plan directory from inactive extension', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
});
const argv = await parseArguments(settings);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
name: 'ext-plan',
isActive: false,
plan: { directory: 'ext-plans-dir-inactive' },
} as unknown as GeminiCLIExtension,
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getPlansDir()).not.toContain(
'ext-plans-dir-inactive',
);
});
it('should use default path if neither user nor extension settings provide a plan directory', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
});
const argv = await parseArguments(settings);
// No extensions providing plan directory
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
const config = await loadCliConfig(settings, 'test-session', argv);
// Should return the default managed temp directory path
expect(config.storage.getPlansDir()).toBe(
path.join(
'/mock',
'home',
'user',
'.gemini',
'tmp',
'test-project',
'test-session',
'plans',
),
);
});
});
});
+74 -11
View File
@@ -19,11 +19,16 @@ import {
DEFAULT_FILE_FILTERING_OPTIONS,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
FileDiscoveryService,
WRITE_FILE_TOOL_NAME,
SHELL_TOOL_NAMES,
SHELL_TOOL_NAME,
resolveTelemetrySettings,
FatalConfigError,
getPty,
EDIT_TOOL_NAME,
debugLogger,
loadServerHierarchicalMemory,
WEB_FETCH_TOOL_NAME,
ASK_USER_TOOL_NAME,
getVersion,
PREVIEW_GEMINI_MODEL_AUTO,
@@ -390,6 +395,36 @@ export async function parseArguments(
return result as unknown as CliArgs;
}
/**
* Creates a filter function to determine if a tool should be excluded.
*
* In non-interactive mode, we want to disable tools that require user
* interaction to prevent the CLI from hanging. This function creates a predicate
* that returns `true` if a tool should be excluded.
*
* A tool is excluded if it's not in the `allowedToolsSet`. The shell tool
* has a special case: it's not excluded if any of its subcommands
* are in the `allowedTools` list.
*
* @param allowedTools A list of explicitly allowed tool names.
* @param allowedToolsSet A set of explicitly allowed tool names for quick lookups.
* @returns A function that takes a tool name and returns `true` if it should be excluded.
*/
function createToolExclusionFilter(
allowedTools: string[],
allowedToolsSet: Set<string>,
) {
return (tool: string): boolean => {
if (tool === SHELL_TOOL_NAME) {
// If any of the allowed tools is ShellTool (even with subcommands), don't exclude it.
return !allowedTools.some((allowed) =>
SHELL_TOOL_NAMES.some((shellName) => allowed.startsWith(shellName)),
);
}
return !allowedToolsSet.has(tool);
};
}
export function isDebugMode(argv: CliArgs): boolean {
return (
argv.debug ||
@@ -476,10 +511,6 @@ export async function loadCliConfig(
});
await extensionManager.loadExtensions();
const extensionPlanSettings = extensionManager
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const experimentalJitContext = settings.experimental?.jitContext ?? false;
let memoryContent: string | HierarchicalMemory = '';
@@ -602,14 +633,49 @@ export async function loadCliConfig(
!argv.isCommand);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
// In non-interactive mode, exclude tools that require a prompt.
const extraExcludes: string[] = [];
if (!interactive) {
// The Policy Engine natively handles headless safety by translating ASK_USER
// decisions to DENY. However, we explicitly block ask_user here to guarantee
// it can never be allowed via a high-priority policy rule when no human is present.
// ask_user requires user interaction and must be excluded in all
// non-interactive modes, regardless of the approval mode.
extraExcludes.push(ASK_USER_TOOL_NAME);
const defaultExcludes = [
SHELL_TOOL_NAME,
EDIT_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
];
const autoEditExcludes = [SHELL_TOOL_NAME];
const toolExclusionFilter = createToolExclusionFilter(
allowedTools,
allowedToolsSet,
);
switch (approvalMode) {
case ApprovalMode.PLAN:
// In plan non-interactive mode, all tools that require approval are excluded.
// TODO(#16625): Replace this default exclusion logic with specific rules for plan mode.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
break;
case ApprovalMode.DEFAULT:
// In default non-interactive mode, all tools that require approval are excluded.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
break;
case ApprovalMode.AUTO_EDIT:
// In auto-edit non-interactive mode, only tools that still require a prompt are excluded.
extraExcludes.push(...autoEditExcludes.filter(toolExclusionFilter));
break;
case ApprovalMode.YOLO:
// No extra excludes for YOLO mode.
break;
default:
// This should never happen due to validation earlier, but satisfies the linter
break;
}
}
const excludeTools = mergeExcludeTools(settings, extraExcludes);
@@ -760,11 +826,8 @@ export async function loadCliConfig(
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
plan: settings.experimental?.plan,
tracker: settings.experimental?.taskTracker,
directWebFetch: settings.experimental?.directWebFetch,
planSettings: settings.general?.plan?.directory
? settings.general.plan
: (extensionPlanSettings ?? settings.general?.plan),
planSettings: settings.general?.plan,
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
@@ -12,13 +12,6 @@ import { ExtensionManager } from './extension-manager.js';
import { createTestMergedSettings } from './settings.js';
import { createExtension } from '../test-utils/createExtension.js';
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
import {
TrustLevel,
loadTrustedFolders,
isWorkspaceTrusted,
} from './trustedFolders.js';
import { getRealPath } from '@google/gemini-cli-core';
import type { MergedSettings } from './settings.js';
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
@@ -192,157 +185,4 @@ describe('ExtensionManager', () => {
fs.rmSync(externalDir, { recursive: true, force: true });
});
});
describe('symlink handling', () => {
let extensionDir: string;
let symlinkDir: string;
beforeEach(() => {
extensionDir = path.join(tempHomeDir, 'extension');
symlinkDir = path.join(tempHomeDir, 'symlink-ext');
fs.mkdirSync(extensionDir, { recursive: true });
fs.writeFileSync(
path.join(extensionDir, 'gemini-extension.json'),
JSON.stringify({ name: 'test-ext', version: '1.0.0' }),
);
fs.symlinkSync(extensionDir, symlinkDir, 'dir');
});
it('preserves symlinks in installMetadata.source when linking', async () => {
const manager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
settings: {
security: {
folderTrust: { enabled: false }, // Disable trust for simplicity in this test
},
experimental: { extensionConfig: false },
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
hooksConfig: { enabled: true },
} as unknown as MergedSettings,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
});
// Trust the workspace to allow installation
const trustedFolders = loadTrustedFolders();
await trustedFolders.setValue(tempWorkspaceDir, TrustLevel.TRUST_FOLDER);
const installMetadata = {
source: symlinkDir,
type: 'link' as const,
};
await manager.loadExtensions();
const extension = await manager.installOrUpdateExtension(installMetadata);
// Desired behavior: it preserves symlinks (if they were absolute or relative as provided)
expect(extension.installMetadata?.source).toBe(symlinkDir);
});
it('works with the new install command logic (preserves symlink but trusts real path)', async () => {
// This simulates the logic in packages/cli/src/commands/extensions/install.ts
const absolutePath = path.resolve(symlinkDir);
const realPath = getRealPath(absolutePath);
const settings = {
security: {
folderTrust: { enabled: true },
},
experimental: { extensionConfig: false },
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
hooksConfig: { enabled: true },
} as unknown as MergedSettings;
// Trust the REAL path
const trustedFolders = loadTrustedFolders();
await trustedFolders.setValue(realPath, TrustLevel.TRUST_FOLDER);
// Check trust of the symlink path
const trustResult = isWorkspaceTrusted(settings, absolutePath);
expect(trustResult.isTrusted).toBe(true);
const manager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
settings,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
});
const installMetadata = {
source: absolutePath,
type: 'link' as const,
};
await manager.loadExtensions();
const extension = await manager.installOrUpdateExtension(installMetadata);
expect(extension.installMetadata?.source).toBe(absolutePath);
expect(extension.installMetadata?.source).not.toBe(realPath);
});
it('enforces allowedExtensions using the real path', async () => {
const absolutePath = path.resolve(symlinkDir);
const realPath = getRealPath(absolutePath);
const settings = {
security: {
folderTrust: { enabled: false },
// Only allow the real path, not the symlink path
allowedExtensions: [realPath.replace(/\\/g, '\\\\')],
},
experimental: { extensionConfig: false },
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
hooksConfig: { enabled: true },
} as unknown as MergedSettings;
const manager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
settings,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
});
const installMetadata = {
source: absolutePath,
type: 'link' as const,
};
await manager.loadExtensions();
// This should pass because realPath is allowed
const extension = await manager.installOrUpdateExtension(installMetadata);
expect(extension.name).toBe('test-ext');
// Now try with a settings that only allows the symlink path string
const settingsOnlySymlink = {
security: {
folderTrust: { enabled: false },
// Only allow the symlink path string explicitly
allowedExtensions: [absolutePath.replace(/\\/g, '\\\\')],
},
experimental: { extensionConfig: false },
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
hooksConfig: { enabled: true },
} as unknown as MergedSettings;
const manager2 = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
settings: settingsOnlySymlink,
requestConsent: () => Promise.resolve(true),
requestSetting: null,
});
// This should FAIL because it checks the real path against the pattern
// (Unless symlinkDir === extensionDir, which shouldn't happen in this test setup)
if (absolutePath !== realPath) {
await expect(
manager2.installOrUpdateExtension(installMetadata),
).rejects.toThrow(
/is not allowed by the "allowedExtensions" security setting/,
);
}
});
});
});
+8 -11
View File
@@ -161,9 +161,7 @@ export class ExtensionManager extends ExtensionLoader {
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(
getRealPath(installMetadata.source),
);
return new RegExp(pattern).test(installMetadata.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
@@ -212,9 +210,11 @@ export class ExtensionManager extends ExtensionLoader {
await fs.promises.mkdir(extensionsDir, { recursive: true });
if (installMetadata.type === 'local' || installMetadata.type === 'link') {
installMetadata.source = path.isAbsolute(installMetadata.source)
? installMetadata.source
: path.resolve(this.workspaceDir, installMetadata.source);
installMetadata.source = getRealPath(
path.isAbsolute(installMetadata.source)
? installMetadata.source
: path.resolve(this.workspaceDir, installMetadata.source),
);
}
let tempDir: string | undefined;
@@ -262,7 +262,7 @@ Would you like to attempt to install via "git clone" instead?`,
installMetadata.type === 'local' ||
installMetadata.type === 'link'
) {
localSourcePath = getRealPath(installMetadata.source);
localSourcePath = installMetadata.source;
} else {
throw new Error(`Unsupported install type: ${installMetadata.type}`);
}
@@ -638,9 +638,7 @@ Would you like to attempt to install via "git clone" instead?`,
const extensionAllowed = this.settings.security?.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(
getRealPath(installMetadata?.source ?? ''),
);
return new RegExp(pattern).test(installMetadata?.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
@@ -888,7 +886,6 @@ Would you like to attempt to install via "git clone" instead?`,
themes: config.themes,
rules,
checkers,
plan: config.plan,
};
} catch (e) {
debugLogger.error(
-9
View File
@@ -33,15 +33,6 @@ export interface ExtensionConfig {
* These themes will be registered when the extension is activated.
*/
themes?: CustomTheme[];
/**
* Planning features configuration contributed by this extension.
*/
plan?: {
/**
* The directory where planning artifacts are stored.
*/
directory?: string;
};
}
export interface ExtensionUpdateInfo {
@@ -10,15 +10,11 @@
<text x="0" y="53" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * server2 (remote): https://remote.com </text>
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will append info to your gemini.md context using my-context.md </text>
<text x="0" y="87" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will exclude the following core tools: tool1,tool2 </text>
<text x="0" y="121" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-weight="bold">Agent Skills:</text>
<text x="0" y="121" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Agent Skills: </text>
<text x="0" y="155" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will install the following agent skills: </text>
<text x="0" y="189" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
<text x="36" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">skill1</text>
<text x="90" y="189" fill="#ffffff" textLength="810" lengthAdjust="spacingAndGlyphs">: desc1 </text>
<text x="0" y="189" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * skill1: desc1 </text>
<text x="0" y="206" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory) </text>
<text x="0" y="240" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
<text x="36" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">skill2</text>
<text x="90" y="240" fill="#ffffff" textLength="810" lengthAdjust="spacingAndGlyphs">: desc2 </text>
<text x="0" y="240" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * skill2: desc2 </text>
<text x="0" y="257" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory) </text>
<text x="0" y="308" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">The extension you are about to install may have been created by a third-party developer and sourced</text>
<text x="0" y="325" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">from a public repository. Google does not vet, endorse, or guarantee the functionality or security</text>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -5,11 +5,9 @@
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing extension &quot;test-ext&quot;. </text>
<text x="0" y="36" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-weight="bold">Agent Skills:</text>
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Agent Skills: </text>
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will install the following agent skills: </text>
<text x="0" y="104" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
<text x="36" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs" font-weight="bold">locked-skill</text>
<text x="144" y="104" fill="#ffffff" textLength="756" lengthAdjust="spacingAndGlyphs">: A skill in a locked dir </text>
<text x="0" y="104" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * locked-skill: A skill in a locked dir </text>
<text x="0" y="121" fill="#ffffff" textLength="405" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/locked/SKILL.md) </text>
<text x="405" y="121" fill="#cd0000" textLength="342" lengthAdjust="spacingAndGlyphs">⚠️ (Could not count items in directory)</text>
<text x="0" y="172" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">The extension you are about to install may have been created by a third-party developer and sourced</text>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -6,9 +6,7 @@
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing agent skill(s) from &quot;https://example.com/repo.git&quot;. </text>
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">The following agent skill(s) will be installing: </text>
<text x="0" y="70" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
<text x="36" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">skill1</text>
<text x="90" y="70" fill="#ffffff" textLength="810" lengthAdjust="spacingAndGlyphs">: desc1 </text>
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * skill1: desc1 </text>
<text x="0" y="87" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory) </text>
<text x="0" y="121" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Install Destination: /mock/target/dir </text>
<text x="0" y="155" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">Agent skills inject specialized instructions and domain-specific knowledge into the agent&apos;s system</text>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -1,91 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { deriveItemsFromLegacySettings } from './footerItems.js';
import { createMockSettings } from '../test-utils/settings.js';
describe('deriveItemsFromLegacySettings', () => {
it('returns defaults when no legacy settings are customized', () => {
const settings = createMockSettings({
ui: { footer: { hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toEqual([
'workspace',
'git-branch',
'sandbox',
'model-name',
'quota',
]);
});
it('removes workspace when hideCWD is true', () => {
const settings = createMockSettings({
ui: { footer: { hideCWD: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('workspace');
});
it('removes sandbox when hideSandboxStatus is true', () => {
const settings = createMockSettings({
ui: { footer: { hideSandboxStatus: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('sandbox');
});
it('removes model-name, context-used, and quota when hideModelInfo is true', () => {
const settings = createMockSettings({
ui: { footer: { hideModelInfo: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('model-name');
expect(items).not.toContain('context-used');
expect(items).not.toContain('quota');
});
it('includes context-used when hideContextPercentage is false', () => {
const settings = createMockSettings({
ui: { footer: { hideContextPercentage: false } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toContain('context-used');
// Should be after model-name
const modelIdx = items.indexOf('model-name');
const contextIdx = items.indexOf('context-used');
expect(contextIdx).toBe(modelIdx + 1);
});
it('includes memory-usage when showMemoryUsage is true', () => {
const settings = createMockSettings({
ui: { showMemoryUsage: true, footer: { hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toContain('memory-usage');
});
it('handles combination of settings', () => {
const settings = createMockSettings({
ui: {
showMemoryUsage: true,
footer: {
hideCWD: true,
hideModelInfo: true,
hideContextPercentage: false,
},
},
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toEqual([
'git-branch',
'sandbox',
'context-used',
'memory-usage',
]);
});
});
-132
View File
@@ -1,132 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { MergedSettings } from './settings.js';
export const ALL_ITEMS = [
{
id: 'workspace',
header: 'workspace (/directory)',
description: 'Current working directory',
},
{
id: 'git-branch',
header: 'branch',
description: 'Current git branch name (not shown when unavailable)',
},
{
id: 'sandbox',
header: 'sandbox',
description: 'Sandbox type and trust indicator',
},
{
id: 'model-name',
header: '/model',
description: 'Current model identifier',
},
{
id: 'context-used',
header: 'context',
description: 'Percentage of context window used',
},
{
id: 'quota',
header: '/stats',
description: 'Remaining usage on daily limit (not shown when unavailable)',
},
{
id: 'memory-usage',
header: 'memory',
description: 'Memory used by the application',
},
{
id: 'session-id',
header: 'session',
description: 'Unique identifier for the current session',
},
{
id: 'code-changes',
header: 'diff',
description: 'Lines added/removed in the session (not shown when zero)',
},
{
id: 'token-count',
header: 'tokens',
description: 'Total tokens used in the session (not shown when zero)',
},
] as const;
export type FooterItemId = (typeof ALL_ITEMS)[number]['id'];
export const DEFAULT_ORDER = [
'workspace',
'git-branch',
'sandbox',
'model-name',
'context-used',
'quota',
'memory-usage',
'session-id',
'code-changes',
'token-count',
];
export function deriveItemsFromLegacySettings(
settings: MergedSettings,
): string[] {
const defaults = [
'workspace',
'git-branch',
'sandbox',
'model-name',
'quota',
];
const items = [...defaults];
const remove = (arr: string[], id: string) => {
const idx = arr.indexOf(id);
if (idx !== -1) arr.splice(idx, 1);
};
if (settings.ui.footer.hideCWD) remove(items, 'workspace');
if (settings.ui.footer.hideSandboxStatus) remove(items, 'sandbox');
if (settings.ui.footer.hideModelInfo) {
remove(items, 'model-name');
remove(items, 'context-used');
remove(items, 'quota');
}
if (
!settings.ui.footer.hideContextPercentage &&
!items.includes('context-used')
) {
const modelIdx = items.indexOf('model-name');
if (modelIdx !== -1) items.splice(modelIdx + 1, 0, 'context-used');
else items.push('context-used');
}
if (settings.ui.showMemoryUsage) items.push('memory-usage');
return items;
}
const VALID_IDS: Set<string> = new Set(ALL_ITEMS.map((i) => i.id));
/**
* Resolves the ordered list and selected set of footer items from settings.
* Used by FooterConfigDialog to initialize and reset state.
*/
export function resolveFooterState(settings: MergedSettings): {
orderedIds: string[];
selectedIds: Set<string>;
} {
const source = (
settings.ui?.footer?.items ?? deriveItemsFromLegacySettings(settings)
).filter((id: string) => VALID_IDS.has(id));
const others = DEFAULT_ORDER.filter((id) => !source.includes(id));
return {
orderedIds: [...source, ...others],
selectedIds: new Set(source),
};
}
+1 -17
View File
@@ -97,7 +97,7 @@ describe('loadSandboxConfig', () => {
it('should throw if GEMINI_SANDBOX is an invalid command', async () => {
process.env['GEMINI_SANDBOX'] = 'invalid-command';
await expect(loadSandboxConfig({}, {})).rejects.toThrow(
"Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec, lxc",
"Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec",
);
});
@@ -108,22 +108,6 @@ describe('loadSandboxConfig', () => {
"Missing sandbox command 'docker' (from GEMINI_SANDBOX)",
);
});
it('should use lxc if GEMINI_SANDBOX=lxc and it exists', async () => {
process.env['GEMINI_SANDBOX'] = 'lxc';
mockedCommandExistsSync.mockReturnValue(true);
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'lxc', image: 'default/image' });
expect(mockedCommandExistsSync).toHaveBeenCalledWith('lxc');
});
it('should throw if GEMINI_SANDBOX=lxc but lxc command does not exist', async () => {
process.env['GEMINI_SANDBOX'] = 'lxc';
mockedCommandExistsSync.mockReturnValue(false);
await expect(loadSandboxConfig({}, {})).rejects.toThrow(
"Missing sandbox command 'lxc' (from GEMINI_SANDBOX)",
);
});
});
describe('with sandbox: true', () => {
+1 -7
View File
@@ -27,7 +27,6 @@ const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
'docker',
'podman',
'sandbox-exec',
'lxc',
];
function isSandboxCommand(value: string): value is SandboxConfig['command'] {
@@ -92,9 +91,6 @@ function getSandboxCommand(
}
return '';
// Note: 'lxc' is intentionally not auto-detected because it requires a
// pre-existing, running container managed by the user. Use
// GEMINI_SANDBOX=lxc or sandbox: "lxc" in settings to enable it.
}
export async function loadSandboxConfig(
@@ -106,9 +102,7 @@ export async function loadSandboxConfig(
const packageJson = await getPackageJson(__dirname);
const image =
process.env['GEMINI_SANDBOX_IMAGE'] ??
process.env['GEMINI_SANDBOX_IMAGE_DEFAULT'] ??
packageJson?.config?.sandboxImageUri;
process.env['GEMINI_SANDBOX_IMAGE'] ?? packageJson?.config?.sandboxImageUri;
return command && image ? { command, image } : undefined;
}
+17 -32
View File
@@ -2162,7 +2162,7 @@ describe('Settings Loading and Merging', () => {
}
});
it('should remove deprecated settings by default and prioritize new ones', () => {
it('should prioritize new settings over deprecated ones and respect removeDeprecated flag', () => {
const userSettingsContent = {
general: {
disableAutoUpdate: true,
@@ -2177,41 +2177,13 @@ describe('Settings Loading and Merging', () => {
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
// Default is now removeDeprecated = true
// 1. removeDeprecated = false (default)
migrateDeprecatedSettings(loadedSettings);
// Should remove disableAutoUpdate and trust enableAutoUpdate: true
expect(setValueSpy).toHaveBeenCalledWith(SettingScope.User, 'general', {
enableAutoUpdate: true,
});
// Should remove disableFuzzySearch and trust enableFuzzySearch: false
expect(setValueSpy).toHaveBeenCalledWith(SettingScope.User, 'context', {
fileFiltering: { enableFuzzySearch: false },
});
});
it('should preserve deprecated settings when removeDeprecated is explicitly false', () => {
const userSettingsContent = {
general: {
disableAutoUpdate: true,
enableAutoUpdate: true,
},
context: {
fileFiltering: {
disableFuzzySearch: false,
enableFuzzySearch: false,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
migrateDeprecatedSettings(loadedSettings, false);
// Should still have old settings since removeDeprecated = false
// Should still have old settings
expect(
loadedSettings.forScope(SettingScope.User).settings.general,
).toHaveProperty('disableAutoUpdate');
@@ -2222,6 +2194,19 @@ describe('Settings Loading and Merging', () => {
}
).fileFiltering,
).toHaveProperty('disableFuzzySearch');
// 2. removeDeprecated = true
migrateDeprecatedSettings(loadedSettings, true);
// Should remove disableAutoUpdate and trust enableAutoUpdate: true
expect(setValueSpy).toHaveBeenCalledWith(SettingScope.User, 'general', {
enableAutoUpdate: true,
});
// Should remove disableFuzzySearch and trust enableFuzzySearch: false
expect(setValueSpy).toHaveBeenCalledWith(SettingScope.User, 'context', {
fileFiltering: { enableFuzzySearch: false },
});
});
it('should trigger migration automatically during loadSettings', () => {
+6 -2
View File
@@ -185,6 +185,9 @@ export interface SessionRetentionSettings {
/** Minimum retention period (safety limit, defaults to "1d") */
minRetention?: string;
/** INTERNAL: Whether the user has acknowledged the session retention warning */
warningAcknowledged?: boolean;
}
export interface SettingsError {
@@ -796,13 +799,14 @@ export function loadSettings(
/**
* Migrates deprecated settings to their new counterparts.
*
* Deprecated settings are removed from settings files by default.
* TODO: After a couple of weeks (around early Feb 2026), we should start removing
* the deprecated settings from the settings files by default.
*
* @returns true if any changes were made and need to be saved.
*/
export function migrateDeprecatedSettings(
loadedSettings: LoadedSettings,
removeDeprecated = true,
removeDeprecated = false,
): boolean {
let anyModified = false;
const systemWarnings: Map<LoadableSettingScope, string[]> = new Map();
+17 -42
View File
@@ -117,10 +117,6 @@ export interface SettingDefinition {
* For map-like objects without explicit `properties`, describes the shape of the values.
*/
additionalProperties?: SettingCollectionDefinition;
/**
* Optional unit to display after the value (e.g. '%').
*/
unit?: string;
/**
* Optional reference identifier for generators that emit a `$ref`.
*/
@@ -343,7 +339,7 @@ const SETTINGS_SCHEMA = {
label: 'Enable Session Cleanup',
category: 'General',
requiresRestart: false,
default: true as boolean,
default: false,
description: 'Enable automatic session cleanup',
showInDialog: true,
},
@@ -352,7 +348,7 @@ const SETTINGS_SCHEMA = {
label: 'Keep chat history',
category: 'General',
requiresRestart: false,
default: '30d' as string,
default: undefined as string | undefined,
description:
'Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w")',
showInDialog: true,
@@ -376,6 +372,16 @@ const SETTINGS_SCHEMA = {
description: `Minimum retention period (safety limit, defaults to "${DEFAULT_MIN_RETENTION}")`,
showInDialog: false,
},
warningAcknowledged: {
type: 'boolean',
label: 'Warning Acknowledged',
category: 'General',
requiresRestart: false,
default: false,
showInDialog: false,
description:
'INTERNAL: Whether the user has acknowledged the session retention warning',
},
},
description: 'Settings for automatic session cleanup.',
},
@@ -565,34 +571,14 @@ const SETTINGS_SCHEMA = {
description: 'Settings for the footer.',
showInDialog: false,
properties: {
items: {
type: 'array',
label: 'Footer Items',
category: 'UI',
requiresRestart: false,
default: undefined as string[] | undefined,
description:
'List of item IDs to display in the footer. Rendered in order',
showInDialog: false,
items: { type: 'string' },
},
showLabels: {
type: 'boolean',
label: 'Show Footer Labels',
category: 'UI',
requiresRestart: false,
default: true,
description:
'Display a second line above the footer items with descriptive headers (e.g., /model).',
showInDialog: false,
},
hideCWD: {
type: 'boolean',
label: 'Hide CWD',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Hide the current working directory in the footer.',
description:
'Hide the current working directory path in the footer.',
showInDialog: true,
},
hideSandboxStatus: {
@@ -619,7 +605,7 @@ const SETTINGS_SCHEMA = {
category: 'UI',
requiresRestart: false,
default: true,
description: 'Hides the context window usage percentage.',
description: 'Hides the context window remaining percentage.',
showInDialog: true,
},
},
@@ -937,14 +923,13 @@ const SETTINGS_SCHEMA = {
},
compressionThreshold: {
type: 'number',
label: 'Context Compression Threshold',
label: 'Compression Threshold',
category: 'Model',
requiresRestart: true,
default: 0.5 as number,
description:
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
showInDialog: true,
unit: '%',
},
disableLoopDetection: {
type: 'boolean',
@@ -1256,8 +1241,7 @@ const SETTINGS_SCHEMA = {
ref: 'BooleanOrString',
description: oneLine`
Sandbox execution environment.
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
Set to a boolean to enable or disable the sandbox, or provide a string path to a sandbox profile.
`,
showInDialog: false,
},
@@ -1827,15 +1811,6 @@ const SETTINGS_SCHEMA = {
description: 'Enable planning features (Plan Mode and tools).',
showInDialog: true,
},
taskTracker: {
type: 'boolean',
label: 'Task Tracker',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable task tracker tools.',
showInDialog: false,
},
modelSteering: {
type: 'boolean',
label: 'Model Steering',
@@ -506,7 +506,7 @@ describe('Trusted Folders', () => {
const realDir = path.join(tempDir, 'real');
const symlinkDir = path.join(tempDir, 'symlink');
fs.mkdirSync(realDir);
fs.symlinkSync(realDir, symlinkDir, 'dir');
fs.symlinkSync(realDir, symlinkDir);
// Rule uses realpath
const config = { [realDir]: TrustLevel.TRUST_FOLDER };
-19
View File
@@ -9,7 +9,6 @@ import { performInitialAuth } from './auth.js';
import {
type Config,
ValidationRequiredError,
ProjectIdRequiredError,
AuthType,
} from '@google/gemini-cli-core';
@@ -117,22 +116,4 @@ describe('auth', () => {
AuthType.LOGIN_WITH_GOOGLE,
);
});
it('should return ProjectIdRequiredError message without "Failed to login" prefix', async () => {
const projectIdError = new ProjectIdRequiredError();
vi.mocked(mockConfig.refreshAuth).mockRejectedValue(projectIdError);
const result = await performInitialAuth(
mockConfig,
AuthType.LOGIN_WITH_GOOGLE,
);
expect(result).toEqual({
authError:
'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca',
accountSuspensionInfo: null,
});
expect(result.authError).not.toContain('Failed to login');
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
);
});
});
-9
View File
@@ -10,7 +10,6 @@ import {
getErrorMessage,
ValidationRequiredError,
isAccountSuspendedError,
ProjectIdRequiredError,
} from '@google/gemini-cli-core';
import type { AccountSuspensionInfo } from '../ui/contexts/UIStateContext.js';
@@ -55,14 +54,6 @@ export async function performInitialAuth(
},
};
}
if (e instanceof ProjectIdRequiredError) {
// OAuth succeeded but account setup requires project ID
// Show the error message directly without "Failed to login" prefix
return {
authError: getErrorMessage(e),
accountSuspensionInfo: null,
};
}
return {
authError: `Failed to login. Message: ${getErrorMessage(e)}`,
accountSuspensionInfo: null,
+1 -1
View File
@@ -243,7 +243,7 @@ export async function startInteractiveUI(
<ScrollProvider>
<OverflowProvider>
<SessionStatsProvider>
<VimModeProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
@@ -65,6 +65,10 @@ describe('Model Steering Integration', () => {
// Resolve list_directory (Proceed)
await rig.resolveTool('ReadFolder');
// Wait for the model to process the hint and output the next action
// Based on steering.responses, it should first acknowledge the hint
await rig.waitForOutput('ACK: I will focus on .txt files now.');
// Then it should proceed with the next action
await rig.waitForOutput(
/Since you want me to focus on .txt files,[\s\S]*I will read file1.txt/,
@@ -31,7 +31,6 @@ import { docsCommand } from '../ui/commands/docsCommand.js';
import { directoryCommand } from '../ui/commands/directoryCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { footerCommand } from '../ui/commands/footerCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js';
import { shortcutsCommand } from '../ui/commands/shortcutsCommand.js';
import { rewindCommand } from '../ui/commands/rewindCommand.js';
@@ -120,7 +119,6 @@ export class BuiltinCommandLoader implements ICommandLoader {
]
: [extensionsCommand(this.config?.getEnableExtensionReloading())]),
helpCommand,
footerCommand,
shortcutsCommand,
...(this.config?.getEnableHooksUI() ? [hooksCommand] : []),
rewindCommand,
@@ -1,3 +1,4 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"Starting a long task. First, I'll list the files."},{"functionCall":{"name":"list_directory","args":{"dir_path":"."}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContent","response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ACK: I will focus on .txt files now."}]},"finishReason":"STOP"}]}}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I see the files. Since you want me to focus on .txt files, I will read file1.txt."},{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I have read file1.txt. Task complete."}]},"finishReason":"STOP"}]}]}
+4 -23
View File
@@ -17,7 +17,6 @@ import { vi } from 'vitest';
import stripAnsi from 'strip-ansi';
import { act, useState } from 'react';
import os from 'node:os';
import path from 'node:path';
import { LoadedSettings } from '../config/settings.js';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { SettingsContext } from '../ui/contexts/SettingsContext.js';
@@ -503,22 +502,7 @@ const configProxy = new Proxy({} as Config, {
get(_target, prop) {
if (prop === 'getTargetDir') {
return () =>
path.join(
path.parse(process.cwd()).root,
'Users',
'test',
'project',
'foo',
'bar',
'and',
'some',
'more',
'directories',
'to',
'make',
'it',
'long',
);
'/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long';
}
if (prop === 'getUseBackgroundColor') {
return () => true;
@@ -544,13 +528,12 @@ export const mockSettings = new LoadedSettings(
// A minimal mock UIState to satisfy the context provider.
// Tests that need specific UIState values should provide their own.
const baseMockUiState = {
history: [],
renderMarkdown: true,
streamingState: StreamingState.Idle,
terminalWidth: 100,
terminalHeight: 40,
currentModel: 'gemini-pro',
terminalBackgroundColor: 'black' as const,
terminalBackgroundColor: 'black',
cleanUiDetailsVisible: false,
allowPlanMode: true,
activePtyId: undefined,
@@ -569,9 +552,6 @@ const baseMockUiState = {
warningText: '',
},
bannerVisible: false,
nightly: false,
updateInfo: null,
pendingHistoryItems: [],
};
export const mockAppState: AppState = {
@@ -624,6 +604,7 @@ const mockUIActions: UIActions = {
revealCleanUiDetailsTemporarily: vi.fn(),
handleWarning: vi.fn(),
setEmbeddedShellFocused: vi.fn(),
setActivePtyId: vi.fn(),
dismissBackgroundShell: vi.fn(),
setActiveBackgroundShellPid: vi.fn(),
setIsBackgroundShellListOpen: vi.fn(),
@@ -772,7 +753,7 @@ export const renderWithProviders = (
<ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider>
<VimModeProvider settings={finalSettings}>
<ShellFocusContext.Provider value={shellFocus}>
<SessionStatsProvider>
<StreamingContext.Provider
+3 -26
View File
@@ -89,7 +89,6 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
break;
}
}
if (contentRows === 0) contentRows = 1; // Minimum 1 row
const width = terminal.cols * charWidth + padding * 2;
@@ -114,9 +113,6 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
let currentFgHex: string | null = null;
let currentBgHex: string | null = null;
let currentIsBold = false;
let currentIsItalic = false;
let currentIsUnderline = false;
let currentBlockStartCol = -1;
let currentBlockText = '';
let currentBlockNumCells = 0;
@@ -132,20 +128,12 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
svg += ` <rect x="${xPos}" y="${yPos}" width="${rectWidth}" height="${charHeight}" fill="${currentBgHex}" />
`;
}
if (currentBlockText.trim().length > 0 || currentIsUnderline) {
if (currentBlockText.trim().length > 0) {
const fill = currentFgHex || '#ffffff'; // Default text color
const textWidth = currentBlockNumCells * charWidth;
let extraAttrs = '';
if (currentIsBold) extraAttrs += ' font-weight="bold"';
if (currentIsItalic) extraAttrs += ' font-style="italic"';
if (currentIsUnderline)
extraAttrs += ' text-decoration="underline"';
// Use textLength to ensure the block fits exactly into its designated cells
const textElement = `<text x="${xPos}" y="${yPos + 2}" fill="${fill}" textLength="${textWidth}" lengthAdjust="spacingAndGlyphs"${extraAttrs}>${escapeXml(currentBlockText)}</text>`;
svg += ` ${textElement}\n`;
svg += ` <text x="${xPos}" y="${yPos + 2}" fill="${fill}" textLength="${textWidth}" lengthAdjust="spacingAndGlyphs">${escapeXml(currentBlockText)}</text>
`;
}
}
}
@@ -176,27 +164,17 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
bgHex = tempFgHex || '#ffffff';
}
const isBold = !!cell.isBold();
const isItalic = !!cell.isItalic();
const isUnderline = !!cell.isUnderline();
let chars = cell.getChars();
if (chars === '') chars = ' '.repeat(cellWidth);
if (
fgHex !== currentFgHex ||
bgHex !== currentBgHex ||
isBold !== currentIsBold ||
isItalic !== currentIsItalic ||
isUnderline !== currentIsUnderline ||
currentBlockStartCol === -1
) {
finalizeBlock(x);
currentFgHex = fgHex;
currentBgHex = bgHex;
currentIsBold = isBold;
currentIsItalic = isItalic;
currentIsUnderline = isUnderline;
currentBlockStartCol = x;
currentBlockText = chars;
currentBlockNumCells = cellWidth;
@@ -207,7 +185,6 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
}
finalizeBlock(line.length);
}
svg += ` </g>\n</svg>`;
return svg;
};
+130
View File
@@ -2544,6 +2544,136 @@ describe('AppContainer State Management', () => {
});
});
describe('Expansion Persistence', () => {
let rerender: () => void;
let unmount: () => void;
let stdin: ReturnType<typeof render>['stdin'];
const setupExpansionPersistenceTest = async (
HighPriorityChild?: React.FC,
) => {
const getTree = () => (
<SettingsContext.Provider value={mockSettings}>
<KeypressProvider config={mockConfig}>
<OverflowProvider>
<AppContainer
config={mockConfig}
version="1.0.0"
initializationResult={mockInitResult}
/>
{HighPriorityChild && <HighPriorityChild />}
</OverflowProvider>
</KeypressProvider>
</SettingsContext.Provider>
);
const renderResult = render(getTree());
stdin = renderResult.stdin;
await act(async () => {
vi.advanceTimersByTime(100);
});
rerender = () => renderResult.rerender(getTree());
unmount = () => renderResult.unmount();
};
const writeStdin = async (sequence: string) => {
await act(async () => {
stdin.write(sequence);
// Advance timers to allow escape sequence parsing and broadcasting
vi.advanceTimersByTime(100);
});
rerender();
};
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('should reset expansion when a key is NOT handled by anyone', async () => {
await setupExpansionPersistenceTest();
// Expand first
act(() => capturedUIActions.setConstrainHeight(false));
rerender();
expect(capturedUIState.constrainHeight).toBe(false);
// Press a random key that no one handles (hits Low priority fallback)
await writeStdin('x');
// Should be reset to true (collapsed)
expect(capturedUIState.constrainHeight).toBe(true);
unmount();
});
it('should toggle expansion when Ctrl+O is pressed', async () => {
await setupExpansionPersistenceTest();
// Initial state is collapsed
expect(capturedUIState.constrainHeight).toBe(true);
// Press Ctrl+O to expand (Ctrl+O is sequence \x0f)
await writeStdin('\x0f');
expect(capturedUIState.constrainHeight).toBe(false);
// Press Ctrl+O again to collapse
await writeStdin('\x0f');
expect(capturedUIState.constrainHeight).toBe(true);
unmount();
});
it('should NOT collapse when a high-priority component handles the key (e.g., up/down arrows)', async () => {
const NavigationHandler = () => {
// use real useKeypress
useKeypress(
(key: Key) => {
if (key.name === 'up' || key.name === 'down') {
return true; // Handle navigation
}
return false;
},
{ isActive: true, priority: true }, // High priority
);
return null;
};
await setupExpansionPersistenceTest(NavigationHandler);
// Expand first
act(() => capturedUIActions.setConstrainHeight(false));
rerender();
expect(capturedUIState.constrainHeight).toBe(false);
// 1. Simulate Up arrow (handled by high priority child)
// CSI A is Up arrow
await writeStdin('\u001b[A');
// Should STILL be expanded
expect(capturedUIState.constrainHeight).toBe(false);
// 2. Simulate Down arrow (handled by high priority child)
// CSI B is Down arrow
await writeStdin('\u001b[B');
// Should STILL be expanded
expect(capturedUIState.constrainHeight).toBe(false);
// 3. Sanity check: press an unhandled key
await writeStdin('x');
// Should finally collapse
expect(capturedUIState.constrainHeight).toBe(true);
unmount();
});
});
describe('Shortcuts Help Visibility', () => {
let handleGlobalKeypress: (key: Key) => boolean;
let mockedUseKeypress: Mock;
+52 -14
View File
@@ -80,8 +80,8 @@ import {
type ConsentRequestPayload,
type AgentsDiscoveredPayload,
ChangeAuthRequestedError,
ProjectIdRequiredError,
CoreToolCallStatus,
generateSteeringAckMessage,
buildUserSteeringHintPrompt,
logBillingEvent,
ApiKeyUpdatedEvent,
@@ -129,7 +129,7 @@ import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { relaunchApp } from '../utils/processUtils.js';
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import type { SessionInfo } from '../utils/sessionUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useMcpStatus } from './hooks/useMcpStatus.js';
@@ -146,6 +146,7 @@ import { requestConsentInteractive } from '../config/extensions/consent.js';
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
import { useSessionResume } from './hooks/useSessionResume.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
import { useSettings } from './contexts/SettingsContext.js';
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
@@ -771,12 +772,6 @@ export const AppContainer = (props: AppContainerProps) => {
if (e instanceof ChangeAuthRequestedError) {
return;
}
if (e instanceof ProjectIdRequiredError) {
// OAuth succeeded but account setup requires project ID
// Show the error message directly without "Failed to authenticate" prefix
onAuthError(getErrorMessage(e));
return;
}
onAuthError(
`Failed to authenticate: ${e instanceof Error ? e.message : String(e)}`,
);
@@ -787,12 +782,13 @@ export const AppContainer = (props: AppContainerProps) => {
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()
) {
await runExitCleanup();
writeToStdout(`
----------------------------------------------------------------
Logging in with Google... Restarting Gemini CLI to continue.
----------------------------------------------------------------
`);
await relaunchApp();
process.exit(RELAUNCH_EXIT_CODE);
}
}
setAuthState(AuthState.Authenticated);
@@ -1113,6 +1109,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
dismissBackgroundShell,
retryStatus,
setActivePtyId,
} = useGeminiStream(
config.getGeminiClient(),
historyManager.history,
@@ -1552,6 +1549,28 @@ Logging in with Google... Restarting Gemini CLI to continue.
useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog);
const handleAutoEnableRetention = useCallback(() => {
const userSettings = settings.forScope(SettingScope.User).settings;
const currentRetention = userSettings.general?.sessionRetention ?? {};
settings.setValue(SettingScope.User, 'general.sessionRetention', {
...currentRetention,
enabled: true,
maxAge: '30d',
warningAcknowledged: true,
});
}, [settings]);
const {
shouldShowWarning: shouldShowRetentionWarning,
checkComplete: retentionCheckComplete,
sessionsToDeleteCount,
} = useSessionRetentionCheck(
config,
settings.merged,
handleAutoEnableRetention,
);
const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
@@ -1878,7 +1897,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
],
);
useKeypress(handleGlobalKeypress, { isActive: true, priority: true });
useKeypress(handleGlobalKeypress, {
isActive: true,
priority: KeypressPriority.Low,
});
useKeypress(
() => {
@@ -1994,7 +2016,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
const nightly = props.version.includes('nightly');
const dialogsVisible =
shouldShowIdePrompt ||
(shouldShowRetentionWarning && retentionCheckComplete) ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
isPolicyUpdateDialogOpen ||
@@ -2108,6 +2130,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
return;
}
void generateSteeringAckMessage(
config.getBaseLlmClient(),
pendingHint,
).then((ackText) => {
historyManager.addItem({
type: 'info',
text: ackText,
});
});
void submitQuery([{ text: buildUserSteeringHintPrompt(pendingHint) }]);
}, [
config,
@@ -2172,7 +2203,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
history: historyManager.history,
historyManager,
isThemeDialogOpen,
shouldShowRetentionWarning:
shouldShowRetentionWarning && retentionCheckComplete,
sessionsToDeleteCount: sessionsToDeleteCount ?? 0,
themeError,
isAuthenticating,
isConfigInitialized,
@@ -2302,7 +2335,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
}),
[
isThemeDialogOpen,
shouldShowRetentionWarning,
retentionCheckComplete,
sessionsToDeleteCount,
themeError,
isAuthenticating,
isConfigInitialized,
@@ -2475,6 +2510,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
setActivePtyId,
dismissBackgroundShell,
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
@@ -2493,7 +2529,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
});
}
}
await relaunchApp();
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
},
handleNewAgentsSelect: async (choice: NewAgentsChoice) => {
if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) {
@@ -2566,6 +2603,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
setActivePtyId,
dismissBackgroundShell,
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
@@ -2,20 +2,20 @@
exports[`App > Snapshots > renders default layout correctly 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
@@ -47,31 +47,34 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = `
"Notifications
Footer
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
Composer
"
`;
exports[`App > Snapshots > renders with dialogs visible 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
@@ -107,17 +110,20 @@ DialogManager
exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = `
"
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀
▝▀
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
HistoryItemDisplay
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
@@ -140,9 +146,6 @@ HistoryItemDisplay
Notifications
Composer
"
+1 -1
View File
@@ -98,7 +98,7 @@ export function ApiAuthDialog({
return (
<Box
borderStyle="round"
borderColor={theme.ui.focus}
borderColor={theme.border.focused}
flexDirection="column"
padding={1}
width="100%"
+8 -4
View File
@@ -21,8 +21,9 @@ import {
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js';
import { AuthState } from '../types.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { validateAuthMethodWithSettings } from './useAuth.js';
import { relaunchApp } from '../../utils/processUtils.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
interface AuthDialogProps {
config: Config;
@@ -132,7 +133,10 @@ export function AuthDialog({
config.isBrowserLaunchSuppressed()
) {
setExiting(true);
setTimeout(relaunchApp, 100);
setTimeout(async () => {
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
}, 100);
return;
}
@@ -189,7 +193,7 @@ export function AuthDialog({
return (
<Box
borderStyle="round"
borderColor={theme.ui.focus}
borderColor={theme.border.focused}
flexDirection="row"
padding={1}
width="100%"
@@ -205,7 +209,7 @@ export function AuthDialog({
return (
<Box
borderStyle="round"
borderColor={theme.ui.focus}
borderColor={theme.border.focused}
flexDirection="row"
padding={1}
width="100%"
@@ -9,10 +9,7 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import {
RELAUNCH_EXIT_CODE,
_resetRelaunchStateForTesting,
} from '../../utils/processUtils.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
import { type Config } from '@google/gemini-cli-core';
// Mocks
@@ -41,7 +38,6 @@ describe('LoginWithGoogleRestartDialog', () => {
vi.clearAllMocks();
exitSpy.mockClear();
vi.useRealTimers();
_resetRelaunchStateForTesting();
});
it('renders correctly', async () => {
@@ -8,7 +8,8 @@ import { type Config } from '@google/gemini-cli-core';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { relaunchApp } from '../../utils/processUtils.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
interface LoginWithGoogleRestartDialogProps {
onDismiss: () => void;
@@ -35,7 +36,8 @@ export const LoginWithGoogleRestartDialog = ({
});
}
}
await relaunchApp();
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
}, 100);
return true;
}
+1 -21
View File
@@ -15,11 +15,7 @@ import {
} from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { useAuthCommand, validateAuthMethodWithSettings } from './useAuth.js';
import {
AuthType,
type Config,
ProjectIdRequiredError,
} from '@google/gemini-cli-core';
import { AuthType, type Config } from '@google/gemini-cli-core';
import { AuthState } from '../types.js';
import type { LoadedSettings } from '../../config/settings.js';
import { waitFor } from '../../test-utils/async.js';
@@ -292,21 +288,5 @@ describe('useAuth', () => {
expect(result.current.authState).toBe(AuthState.Updating);
});
});
it('should handle ProjectIdRequiredError without "Failed to login" prefix', async () => {
const projectIdError = new ProjectIdRequiredError();
(mockConfig.refreshAuth as Mock).mockRejectedValue(projectIdError);
const { result } = renderHook(() =>
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
);
await waitFor(() => {
expect(result.current.authError).toBe(
'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca',
);
expect(result.current.authError).not.toContain('Failed to login');
expect(result.current.authState).toBe(AuthState.Updating);
});
});
});
});
-5
View File
@@ -12,7 +12,6 @@ import {
loadApiKey,
debugLogger,
isAccountSuspendedError,
ProjectIdRequiredError,
} from '@google/gemini-cli-core';
import { getErrorMessage } from '@google/gemini-cli-core';
import { AuthState } from '../types.js';
@@ -144,10 +143,6 @@ export const useAuthCommand = (
appealUrl: suspendedError.appealUrl,
appealLinkText: suspendedError.appealLinkText,
});
} else if (e instanceof ProjectIdRequiredError) {
// OAuth succeeded but account setup requires project ID
// Show the error message directly without "Failed to login" prefix
onAuthError(getErrorMessage(e));
} else {
onAuthError(`Failed to login. Message: ${getErrorMessage(e)}`);
}
@@ -21,10 +21,6 @@ import {
ConfigExtensionDialog,
type ConfigExtensionDialogProps,
} from '../components/ConfigExtensionDialog.js';
import {
ExtensionRegistryView,
type ExtensionRegistryViewProps,
} from '../components/views/ExtensionRegistryView.js';
import { type CommandContext, type SlashCommand } from './types.js';
import {
@@ -43,8 +39,6 @@ import {
} from '../../config/extension-manager.js';
import { SettingScope } from '../../config/settings.js';
import { stat } from 'node:fs/promises';
import { type RegistryExtension } from '../../config/extensionRegistryClient.js';
import { waitFor } from '../../test-utils/async.js';
vi.mock('../../config/extension-manager.js', async (importOriginal) => {
const actual =
@@ -173,7 +167,6 @@ describe('extensionsCommand', () => {
},
ui: {
dispatchExtensionStateUpdate: mockDispatchExtensionState,
removeComponent: vi.fn(),
},
});
});
@@ -436,61 +429,6 @@ describe('extensionsCommand', () => {
throw new Error('Explore action not found');
}
it('should return ExtensionRegistryView custom dialog when experimental.extensionRegistry is true', async () => {
mockContext.services.settings.merged.experimental.extensionRegistry = true;
const result = await exploreAction(mockContext, '');
expect(result).toBeDefined();
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const component =
result.component as ReactElement<ExtensionRegistryViewProps>;
expect(component.type).toBe(ExtensionRegistryView);
expect(component.props.extensionManager).toBe(mockExtensionLoader);
});
it('should handle onSelect and onClose in ExtensionRegistryView', async () => {
mockContext.services.settings.merged.experimental.extensionRegistry = true;
const result = await exploreAction(mockContext, '');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const component =
result.component as ReactElement<ExtensionRegistryViewProps>;
const extension = {
extensionName: 'test-ext',
url: 'https://github.com/test/ext.git',
} as RegistryExtension;
vi.mocked(inferInstallMetadata).mockResolvedValue({
source: extension.url,
type: 'git',
});
mockInstallExtension.mockResolvedValue({ name: extension.url });
// Call onSelect
component.props.onSelect?.(extension);
await waitFor(() => {
expect(inferInstallMetadata).toHaveBeenCalledWith(extension.url);
expect(mockInstallExtension).toHaveBeenCalledWith({
source: extension.url,
type: 'git',
});
});
expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(1);
// Call onClose
component.props.onClose?.();
expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(2);
});
it("should add an info message and call 'open' in a non-sandbox environment", async () => {
// Ensure no special environment variables that would affect behavior
vi.stubEnv('NODE_ENV', '');
@@ -280,9 +280,7 @@ async function exploreAction(
type: 'custom_dialog' as const,
component: React.createElement(ExtensionRegistryView, {
onSelect: (extension) => {
debugLogger.log(`Selected extension: ${extension.extensionName}`);
void installAction(context, extension.url);
context.ui.removeComponent();
debugLogger.debug(`Selected extension: ${extension.extensionName}`);
},
onClose: () => context.ui.removeComponent(),
extensionManager,
@@ -1,25 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type SlashCommand,
type CommandContext,
type OpenCustomDialogActionReturn,
CommandKind,
} from './types.js';
import { FooterConfigDialog } from '../components/FooterConfigDialog.js';
export const footerCommand: SlashCommand = {
name: 'footer',
altNames: ['statusline'],
description: 'Configure which items appear in the footer (statusline)',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: (context: CommandContext): OpenCustomDialogActionReturn => ({
type: 'custom_dialog',
component: <FooterConfigDialog onClose={context.ui.removeComponent} />,
}),
};
@@ -7,6 +7,7 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { hooksCommand } from './hooksCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import type { HookRegistryEntry } from '@google/gemini-cli-core';
import { HookType, HookEventName, ConfigSource } from '@google/gemini-cli-core';
import type { CommandContext } from './types.js';
@@ -126,10 +127,13 @@ describe('hooksCommand', () => {
createMockHook('test-hook', HookEventName.BeforeTool, true),
]);
const result = await hooksCommand.action(mockContext, '');
await hooksCommand.action(mockContext, '');
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
}),
);
});
});
@@ -157,7 +161,7 @@ describe('hooksCommand', () => {
});
});
it('should return custom_dialog even when hook system is not enabled', async () => {
it('should display panel even when hook system is not enabled', async () => {
mockConfig.getHookSystem.mockReturnValue(null);
const panelCmd = hooksCommand.subCommands!.find(
@@ -167,13 +171,17 @@ describe('hooksCommand', () => {
throw new Error('panel command must have an action');
}
const result = await panelCmd.action(mockContext, '');
await panelCmd.action(mockContext, '');
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
hooks: [],
}),
);
});
it('should return custom_dialog when no hooks are configured', async () => {
it('should display panel when no hooks are configured', async () => {
mockHookSystem.getAllHooks.mockReturnValue([]);
(mockContext.services.settings.merged as Record<string, unknown>)[
'hooksConfig'
@@ -186,13 +194,17 @@ describe('hooksCommand', () => {
throw new Error('panel command must have an action');
}
const result = await panelCmd.action(mockContext, '');
await panelCmd.action(mockContext, '');
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
hooks: [],
}),
);
});
it('should return custom_dialog when hooks are configured', async () => {
it('should display hooks list when hooks are configured', async () => {
const mockHooks: HookRegistryEntry[] = [
createMockHook('echo-test', HookEventName.BeforeTool, true),
createMockHook('notify', HookEventName.AfterAgent, false),
@@ -210,10 +222,14 @@ describe('hooksCommand', () => {
throw new Error('panel command must have an action');
}
const result = await panelCmd.action(mockContext, '');
await panelCmd.action(mockContext, '');
expect(result).toHaveProperty('type', 'custom_dialog');
expect(result).toHaveProperty('component');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.HOOKS_LIST,
hooks: mockHooks,
}),
);
});
});
+11 -18
View File
@@ -4,13 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { createElement } from 'react';
import type {
SlashCommand,
CommandContext,
OpenCustomDialogActionReturn,
} from './types.js';
import type { SlashCommand, CommandContext } from './types.js';
import { CommandKind } from './types.js';
import { MessageType, type HistoryItemHooksList } from '../types.js';
import type {
HookRegistryEntry,
MessageActionReturn,
@@ -19,14 +15,13 @@ import { getErrorMessage } from '@google/gemini-cli-core';
import { SettingScope, isLoadableSettingScope } from '../../config/settings.js';
import { enableHook, disableHook } from '../../utils/hookSettings.js';
import { renderHookActionFeedback } from '../../utils/hookUtils.js';
import { HooksDialog } from '../components/HooksDialog.js';
/**
* Display a formatted list of hooks with their status in a dialog
* Display a formatted list of hooks with their status
*/
function panelAction(
async function panelAction(
context: CommandContext,
): MessageActionReturn | OpenCustomDialogActionReturn {
): Promise<void | MessageActionReturn> {
const { config } = context.services;
if (!config) {
return {
@@ -39,13 +34,12 @@ function panelAction(
const hookSystem = config.getHookSystem();
const allHooks = hookSystem?.getAllHooks() || [];
return {
type: 'custom_dialog',
component: createElement(HooksDialog, {
hooks: allHooks,
onClose: () => context.ui.removeComponent(),
}),
const hooksListItem: HistoryItemHooksList = {
type: MessageType.HOOKS_LIST,
hooks: allHooks,
};
context.ui.addItem(hooksListItem);
}
/**
@@ -349,7 +343,6 @@ const panelCommand: SlashCommand = {
altNames: ['list', 'show'],
description: 'Display all registered hooks with their status',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: panelAction,
};
@@ -400,5 +393,5 @@ export const hooksCommand: SlashCommand = {
enableAllCommand,
disableAllCommand,
],
action: (context: CommandContext) => panelCommand.action!(context, ''),
action: async (context: CommandContext) => panelCommand.action!(context, ''),
};
@@ -14,9 +14,7 @@ import {
coreEvents,
processSingleFileContent,
type ProcessedFileReadResult,
readFileWithEncoding,
} from '@google/gemini-cli-core';
import { copyToClipboard } from '../utils/commandUtils.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -27,7 +25,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
emitFeedback: vi.fn(),
},
processSingleFileContent: vi.fn(),
readFileWithEncoding: vi.fn(),
partToString: vi.fn((val) => val),
};
});
@@ -38,14 +35,9 @@ vi.mock('node:path', async (importOriginal) => {
...actual,
default: { ...actual },
join: vi.fn((...args) => args.join('/')),
basename: vi.fn((p) => p.split('/').pop()),
};
});
vi.mock('../utils/commandUtils.js', () => ({
copyToClipboard: vi.fn(),
}));
describe('planCommand', () => {
let mockContext: CommandContext;
@@ -123,46 +115,4 @@ describe('planCommand', () => {
text: '# Approved Plan Content',
});
});
describe('copy subcommand', () => {
it('should copy the approved plan to clipboard', async () => {
const mockPlanPath = '/mock/plans/dir/approved-plan.md';
vi.mocked(
mockContext.services.config!.getApprovedPlanPath,
).mockReturnValue(mockPlanPath);
vi.mocked(readFileWithEncoding).mockResolvedValue('# Plan Content');
const copySubCommand = planCommand.subCommands?.find(
(sc) => sc.name === 'copy',
);
if (!copySubCommand?.action) throw new Error('Copy action missing');
await copySubCommand.action(mockContext, '');
expect(readFileWithEncoding).toHaveBeenCalledWith(mockPlanPath);
expect(copyToClipboard).toHaveBeenCalledWith('# Plan Content');
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'info',
'Plan copied to clipboard (approved-plan.md).',
);
});
it('should warn if no approved plan is found', async () => {
vi.mocked(
mockContext.services.config!.getApprovedPlanPath,
).mockReturnValue(undefined);
const copySubCommand = planCommand.subCommands?.find(
(sc) => sc.name === 'copy',
);
if (!copySubCommand?.action) throw new Error('Copy action missing');
await copySubCommand.action(mockContext, '');
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'warning',
'No approved plan found to copy.',
);
});
});
});
+2 -43
View File
@@ -4,54 +4,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
type CommandContext,
CommandKind,
type SlashCommand,
} from './types.js';
import { CommandKind, type SlashCommand } from './types.js';
import {
ApprovalMode,
coreEvents,
debugLogger,
processSingleFileContent,
partToString,
readFileWithEncoding,
} from '@google/gemini-cli-core';
import { MessageType } from '../types.js';
import * as path from 'node:path';
import { copyToClipboard } from '../utils/commandUtils.js';
async function copyAction(context: CommandContext) {
const config = context.services.config;
if (!config) {
debugLogger.debug('Plan copy command: config is not available in context');
return;
}
const planPath = config.getApprovedPlanPath();
if (!planPath) {
coreEvents.emitFeedback('warning', 'No approved plan found to copy.');
return;
}
try {
const content = await readFileWithEncoding(planPath);
await copyToClipboard(content);
coreEvents.emitFeedback(
'info',
`Plan copied to clipboard (${path.basename(planPath)}).`,
);
} catch (error) {
coreEvents.emitFeedback('error', `Failed to copy plan: ${error}`, error);
}
}
export const planCommand: SlashCommand = {
name: 'plan',
description: 'Switch to Plan Mode and view current plan',
kind: CommandKind.BUILT_IN,
autoExecute: false,
autoExecute: true,
action: async (context) => {
const config = context.services.config;
if (!config) {
@@ -94,13 +62,4 @@ export const planCommand: SlashCommand = {
);
}
},
subCommands: [
{
name: 'copy',
description: 'Copy the currently approved plan to your clipboard',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: copyAction,
},
],
};
@@ -19,7 +19,6 @@ import {
BaseSettingsDialog,
type SettingsDialogItem,
} from './shared/BaseSettingsDialog.js';
import { getNestedValue, isRecord } from '../../utils/settingsUtils.js';
/**
* Configuration field definition for agent settings
@@ -112,12 +111,32 @@ interface AgentConfigDialogProps {
onSave?: () => void;
}
/**
* Get a nested value from an object using a path array
*/
function getNestedValue(
obj: Record<string, unknown> | undefined,
path: string[],
): unknown {
if (!obj) return undefined;
let current: unknown = obj;
for (const key of path) {
if (current === null || current === undefined) return undefined;
if (typeof current !== 'object') return undefined;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current = (current as Record<string, unknown>)[key];
}
return current;
}
/**
* Set a nested value in an object using a path array, creating intermediate objects as needed
*/
function setNestedValue(obj: unknown, path: string[], value: unknown): unknown {
if (!isRecord(obj)) return obj;
function setNestedValue(
obj: Record<string, unknown>,
path: string[],
value: unknown,
): Record<string, unknown> {
const result = { ...obj };
let current = result;
@@ -125,17 +144,12 @@ function setNestedValue(obj: unknown, path: string[], value: unknown): unknown {
const key = path[i];
if (current[key] === undefined || current[key] === null) {
current[key] = {};
} else if (isRecord(current[key])) {
current[key] = { ...current[key] };
}
const next = current[key];
if (isRecord(next)) {
current = next;
} else {
// Cannot traverse further through non-objects
return result;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current[key] = { ...(current[key] as Record<string, unknown>) };
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current = current[key] as Record<string, unknown>;
}
const finalKey = path[path.length - 1];
@@ -253,7 +267,11 @@ export function AgentConfigDialog({
const items: SettingsDialogItem[] = useMemo(
() =>
AGENT_CONFIG_FIELDS.map((field) => {
const currentValue = getNestedValue(pendingOverride, field.path);
const currentValue = getNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
);
const defaultValue = getFieldDefaultFromDefinition(field, definition);
const effectiveValue =
currentValue !== undefined ? currentValue : defaultValue;
@@ -306,18 +324,23 @@ export function AgentConfigDialog({
const field = AGENT_CONFIG_FIELDS.find((f) => f.key === key);
if (!field || field.type !== 'boolean') return;
const currentValue = getNestedValue(pendingOverride, field.path);
const currentValue = getNestedValue(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
);
const defaultValue = getFieldDefaultFromDefinition(field, definition);
const effectiveValue =
currentValue !== undefined ? currentValue : defaultValue;
const newValue = !effectiveValue;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newOverride = setNestedValue(
pendingOverride,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
newValue,
) as AgentOverride;
setPendingOverride(newOverride);
setModifiedFields((prev) => new Set(prev).add(key));
@@ -352,9 +375,9 @@ export function AgentConfigDialog({
}
// Update pending override locally
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newOverride = setNestedValue(
pendingOverride,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
parsed,
) as AgentOverride;
@@ -375,9 +398,9 @@ export function AgentConfigDialog({
if (!field) return;
// Remove the override (set to undefined)
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newOverride = setNestedValue(
pendingOverride,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
pendingOverride as Record<string, unknown>,
field.path,
undefined,
) as AgentOverride;
@@ -213,12 +213,6 @@ describe('<AppHeader />', () => {
it('should NOT render Tips when tipsShown is 10 or more', async () => {
const mockConfig = makeFakeConfig();
const uiState = {
bannerData: {
defaultText: '',
warningText: '',
},
};
persistentStateMock.setData({ tipsShown: 10 });
@@ -226,7 +220,6 @@ describe('<AppHeader />', () => {
<AppHeader version="1.0.0" />,
{
config: mockConfig,
uiState,
},
);
await waitUntilReady();

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