mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 08:40:58 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81a1e127ed |
+1
-6
@@ -14,9 +14,4 @@
|
||||
|
||||
# Docs have a dedicated approver group in addition to maintainers
|
||||
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
|
||||
# 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
|
||||
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
@@ -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'
|
||||
|
||||
@@ -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() }}
|
||||
|
||||
@@ -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}`);
|
||||
Vendored
-3
@@ -7,9 +7,6 @@
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
@@ -73,6 +73,10 @@ powerful tool for developers.
|
||||
and `packages/core` (Backend logic).
|
||||
- **Imports:** Use specific imports and avoid restricted relative imports
|
||||
between packages (enforced by ESLint).
|
||||
- **Linting:** Never suppress the `@typescript-eslint/no-unnecessary-condition`
|
||||
warning (e.g., via `eslint-disable`). This rule ensures that optional chaining
|
||||
and truthiness checks are only used when truly necessary according to the type
|
||||
system, keeping the code clean and predictable.
|
||||
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
|
||||
include the Apache-2.0 license header with the current year. (e.g.,
|
||||
`Copyright 2026 Google LLC`). This is enforced by ESLint.
|
||||
|
||||
@@ -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,7 +323,8 @@ 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/reference/tools-api.md) - Create custom
|
||||
tools.
|
||||
- [**Local development**](./docs/local-development.md) - Local development
|
||||
tooling.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+397
-190
@@ -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
|
||||
|
||||
+70
-75
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.33.0-preview.1
|
||||
# Preview release: v0.33.0-preview.0
|
||||
|
||||
Released: March 04, 2026
|
||||
Released: March 03, 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).
|
||||
@@ -29,159 +29,154 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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(core): implement HTTP authentication support for A2A remote agents by
|
||||
@SandyTao520 in
|
||||
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
|
||||
* feat(core): centralize read_file limits and update gemini-3 description by
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- Build binary by @aswinashok44 in
|
||||
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
|
||||
* Code review fixes as a pr by @jacob314 in
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- feat(acp): add set models interface by @skeshive in
|
||||
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
|
||||
|
||||
**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.32.0-preview.0...v0.33.0-preview.0
|
||||
|
||||
@@ -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
|
||||
|
||||
+125
-157
@@ -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,63 @@ 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)
|
||||
- [Commands](#commands)
|
||||
- [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 custom subagents in Plan Mode](#example-enable-custom-subagents-in-plan-mode)
|
||||
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
|
||||
- [Automatic Model Routing](#automatic-model-routing)
|
||||
- [Cleanup](#cleanup)
|
||||
|
||||
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,52 +76,58 @@ 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. If you make any changes and save the file, the CLI
|
||||
will automatically send the updated plan back to the agent for review and
|
||||
iteration.
|
||||
|
||||
For more complex or specialized planning tasks, you can
|
||||
[customize the planning workflow with skills](#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
|
||||
- **Tool:** Gemini CLI calls the [`exit_plan_mode`] tool to present the
|
||||
finalized plan for your approval.
|
||||
|
||||
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
|
||||
### Commands
|
||||
|
||||
- **`/plan copy`**: Copy the currently approved plan to your clipboard.
|
||||
|
||||
@@ -120,7 +141,7 @@ These are the only allowed tools:
|
||||
- **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 +150,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 +170,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 +194,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`
|
||||
|
||||
@@ -211,7 +229,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 +272,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
|
||||
@@ -344,32 +316,28 @@ Manual deletion also removes all associated artifacts:
|
||||
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
|
||||
[`codebase_investigator`]: /docs/core/subagents.md#codebase_investigator
|
||||
[`cli_help`]: /docs/core/subagents.md#cli_help
|
||||
[subagents]: /docs/core/subagents.md
|
||||
[custom subagents]: /docs/core/subagents.md#creating-custom-subagents
|
||||
[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
|
||||
[session retention]: /docs/cli/session-management.md#session-retention
|
||||
|
||||
+1
-46
@@ -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}}`).
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ 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` |
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -250,18 +250,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):
|
||||
@@ -757,8 +747,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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+1
-1
@@ -188,7 +188,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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -202,6 +202,7 @@ export default tseslint.config(
|
||||
'@typescript-eslint/no-unsafe-type-assertion': 'error',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -377,6 +378,213 @@ export default tseslint.config(
|
||||
},
|
||||
// Prettier config must be last
|
||||
prettierConfig,
|
||||
{
|
||||
// Legacy files with many @typescript-eslint/no-unnecessary-condition issues
|
||||
files: [
|
||||
'packages/a2a-server/src/agent/executor.ts',
|
||||
'packages/a2a-server/src/agent/task.ts',
|
||||
'packages/a2a-server/src/config/settings.ts',
|
||||
'packages/a2a-server/src/http/app.ts',
|
||||
'packages/cli/src/commands/extensions/configure.ts',
|
||||
'packages/cli/src/config/config.ts',
|
||||
'packages/cli/src/config/extension-manager.ts',
|
||||
'packages/cli/src/config/extensions/extensionEnablement.ts',
|
||||
'packages/cli/src/config/extensions/github.ts',
|
||||
'packages/cli/src/config/mcp/mcpServerEnablement.ts',
|
||||
'packages/cli/src/config/settings-validation.ts',
|
||||
'packages/cli/src/config/settings.ts',
|
||||
'packages/cli/src/config/trustedFolders.ts',
|
||||
'packages/cli/src/nonInteractiveCli.ts',
|
||||
'packages/cli/src/services/McpPromptLoader.ts',
|
||||
'packages/cli/src/test-utils/AppRig.tsx',
|
||||
'packages/cli/src/test-utils/mockConfig.ts',
|
||||
'packages/cli/src/test-utils/render.tsx',
|
||||
'packages/cli/src/ui/AppContainer.tsx',
|
||||
'packages/cli/src/ui/commands/agentsCommand.ts',
|
||||
'packages/cli/src/ui/commands/chatCommand.ts',
|
||||
'packages/cli/src/ui/commands/directoryCommand.tsx',
|
||||
'packages/cli/src/ui/commands/hooksCommand.ts',
|
||||
'packages/cli/src/ui/commands/mcpCommand.ts',
|
||||
'packages/cli/src/ui/commands/restoreCommand.ts',
|
||||
'packages/cli/src/ui/commands/rewindCommand.tsx',
|
||||
'packages/cli/src/ui/commands/setupGithubCommand.ts',
|
||||
'packages/cli/src/ui/commands/skillsCommand.ts',
|
||||
'packages/cli/src/ui/commands/statsCommand.ts',
|
||||
'packages/cli/src/ui/components/AskUserDialog.tsx',
|
||||
'packages/cli/src/ui/components/ColorsDisplay.tsx',
|
||||
'packages/cli/src/ui/components/Composer.tsx',
|
||||
'packages/cli/src/ui/components/ContextUsageDisplay.tsx',
|
||||
'packages/cli/src/ui/components/DetailedMessagesDisplay.tsx',
|
||||
'packages/cli/src/ui/components/DialogManager.tsx',
|
||||
'packages/cli/src/ui/components/ExitPlanModeDialog.tsx',
|
||||
'packages/cli/src/ui/components/FolderTrustDialog.tsx',
|
||||
'packages/cli/src/ui/components/HooksDialog.tsx',
|
||||
'packages/cli/src/ui/components/IdeTrustChangeDialog.tsx',
|
||||
'packages/cli/src/ui/components/ModelStatsDisplay.tsx',
|
||||
'packages/cli/src/ui/components/MultiFolderTrustDialog.tsx',
|
||||
'packages/cli/src/ui/components/Notifications.tsx',
|
||||
'packages/cli/src/ui/components/QuotaDisplay.tsx',
|
||||
'packages/cli/src/ui/components/RewindViewer.tsx',
|
||||
'packages/cli/src/ui/components/SessionBrowser.tsx',
|
||||
'packages/cli/src/ui/components/SettingsDialog.tsx',
|
||||
'packages/cli/src/ui/components/ShowMoreLines.tsx',
|
||||
'packages/cli/src/ui/components/StatsDisplay.tsx',
|
||||
'packages/cli/src/ui/components/ThemeDialog.tsx',
|
||||
'packages/cli/src/ui/components/UserIdentity.tsx',
|
||||
'packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx',
|
||||
'packages/cli/src/ui/components/messages/Todo.tsx',
|
||||
'packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx',
|
||||
'packages/cli/src/ui/components/messages/ToolGroupMessage.tsx',
|
||||
'packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx',
|
||||
'packages/cli/src/ui/components/shared/EnumSelector.tsx',
|
||||
'packages/cli/src/ui/components/shared/MaxSizedBox.tsx',
|
||||
'packages/cli/src/ui/components/shared/Scrollable.tsx',
|
||||
'packages/cli/src/ui/components/shared/VirtualizedList.tsx',
|
||||
'packages/cli/src/ui/components/shared/text-buffer.ts',
|
||||
'packages/cli/src/ui/components/triage/TriageDuplicates.tsx',
|
||||
'packages/cli/src/ui/components/triage/TriageIssues.tsx',
|
||||
'packages/cli/src/ui/components/views/McpStatus.tsx',
|
||||
'packages/cli/src/ui/contexts/KeypressContext.tsx',
|
||||
'packages/cli/src/ui/contexts/ScrollProvider.tsx',
|
||||
'packages/cli/src/ui/contexts/SessionContext.tsx',
|
||||
'packages/cli/src/ui/hooks/slashCommandProcessor.ts',
|
||||
'packages/cli/src/ui/hooks/useAtCompletion.ts',
|
||||
'packages/cli/src/ui/hooks/useCommandCompletion.tsx',
|
||||
'packages/cli/src/ui/hooks/useConsoleMessages.ts',
|
||||
'packages/cli/src/ui/hooks/useExtensionUpdates.ts',
|
||||
'packages/cli/src/ui/hooks/useGeminiStream.ts',
|
||||
'packages/cli/src/ui/hooks/useIncludeDirsTrust.tsx',
|
||||
'packages/cli/src/ui/hooks/useInputHistory.ts',
|
||||
'packages/cli/src/ui/hooks/useInputHistoryStore.ts',
|
||||
'packages/cli/src/ui/hooks/usePermissionsModifyTrust.ts',
|
||||
'packages/cli/src/ui/hooks/usePromptCompletion.ts',
|
||||
'packages/cli/src/ui/hooks/useQuotaAndFallback.ts',
|
||||
'packages/cli/src/ui/hooks/useSelectionList.ts',
|
||||
'packages/cli/src/ui/hooks/useShellCompletion.ts',
|
||||
'packages/cli/src/ui/hooks/useSlashCompletion.ts',
|
||||
'packages/cli/src/ui/hooks/useThemeCommand.ts',
|
||||
'packages/cli/src/ui/hooks/useToolScheduler.ts',
|
||||
'packages/cli/src/ui/hooks/vim.ts',
|
||||
'packages/cli/src/ui/themes/theme-manager.ts',
|
||||
'packages/cli/src/ui/utils/CodeColorizer.tsx',
|
||||
'packages/cli/src/ui/utils/MarkdownDisplay.tsx',
|
||||
'packages/cli/src/ui/utils/borderStyles.ts',
|
||||
'packages/cli/src/ui/utils/clipboardUtils.ts',
|
||||
'packages/cli/src/ui/utils/highlight.ts',
|
||||
'packages/cli/src/ui/utils/inlineThinkingMode.ts',
|
||||
'packages/cli/src/ui/utils/keybindingUtils.ts',
|
||||
'packages/cli/src/ui/utils/terminalCapabilityManager.ts',
|
||||
'packages/cli/src/ui/utils/terminalSetup.ts',
|
||||
'packages/cli/src/ui/utils/terminalUtils.ts',
|
||||
'packages/cli/src/utils/activityLogger.ts',
|
||||
'packages/cli/src/utils/commentJson.ts',
|
||||
'packages/cli/src/utils/deepMerge.ts',
|
||||
'packages/cli/src/utils/devtoolsService.ts',
|
||||
'packages/cli/src/utils/envVarResolver.ts',
|
||||
'packages/cli/src/utils/sandbox.ts',
|
||||
'packages/cli/src/utils/sessionUtils.ts',
|
||||
'packages/cli/src/utils/settingsUtils.ts',
|
||||
'packages/cli/src/zed-integration/zedIntegration.ts',
|
||||
'packages/core/src/agents/a2a-client-manager.ts',
|
||||
'packages/core/src/agents/a2aUtils.ts',
|
||||
'packages/core/src/agents/acknowledgedAgents.ts',
|
||||
'packages/core/src/agents/browser/browserManager.ts',
|
||||
'packages/core/src/agents/browser/mcpToolWrapper.ts',
|
||||
'packages/core/src/agents/local-executor.ts',
|
||||
'packages/core/src/agents/local-invocation.ts',
|
||||
'packages/core/src/agents/registry.ts',
|
||||
'packages/core/src/agents/subagent-tool.ts',
|
||||
'packages/core/src/availability/modelAvailabilityService.ts',
|
||||
'packages/core/src/availability/policyHelpers.ts',
|
||||
'packages/core/src/billing/billing.ts',
|
||||
'packages/core/src/code_assist/admin/mcpUtils.ts',
|
||||
'packages/core/src/code_assist/converter.ts',
|
||||
'packages/core/src/code_assist/oauth2.ts',
|
||||
'packages/core/src/code_assist/server.ts',
|
||||
'packages/core/src/code_assist/setup.ts',
|
||||
'packages/core/src/code_assist/telemetry.ts',
|
||||
'packages/core/src/commands/memory.ts',
|
||||
'packages/core/src/config/config.ts',
|
||||
'packages/core/src/confirmation-bus/message-bus.ts',
|
||||
'packages/core/src/core/baseLlmClient.ts',
|
||||
'packages/core/src/core/contentGenerator.ts',
|
||||
'packages/core/src/core/coreToolHookTriggers.ts',
|
||||
'packages/core/src/core/fakeContentGenerator.ts',
|
||||
'packages/core/src/core/geminiChat.ts',
|
||||
'packages/core/src/core/logger.ts',
|
||||
'packages/core/src/core/loggingContentGenerator.ts',
|
||||
'packages/core/src/core/turn.ts',
|
||||
'packages/core/src/hooks/hookRegistry.ts',
|
||||
'packages/core/src/hooks/hookRunner.ts',
|
||||
'packages/core/src/hooks/trustedHooks.ts',
|
||||
'packages/core/src/hooks/types.ts',
|
||||
'packages/core/src/ide/ide-client.ts',
|
||||
'packages/core/src/ide/ide-connection-utils.ts',
|
||||
'packages/core/src/ide/process-utils.ts',
|
||||
'packages/core/src/mcp/oauth-provider.ts',
|
||||
'packages/core/src/mcp/token-storage/base-token-storage.ts',
|
||||
'packages/core/src/policy/config.ts',
|
||||
'packages/core/src/prompts/mcp-prompts.ts',
|
||||
'packages/core/src/prompts/promptProvider.ts',
|
||||
'packages/core/src/routing/strategies/classifierStrategy.ts',
|
||||
'packages/core/src/routing/strategies/defaultStrategy.ts',
|
||||
'packages/core/src/routing/strategies/fallbackStrategy.ts',
|
||||
'packages/core/src/routing/strategies/numericalClassifierStrategy.ts',
|
||||
'packages/core/src/routing/strategies/overrideStrategy.ts',
|
||||
'packages/core/src/safety/checker-runner.ts',
|
||||
'packages/core/src/safety/conseca/conseca.ts',
|
||||
'packages/core/src/safety/conseca/policy-enforcer.ts',
|
||||
'packages/core/src/safety/conseca/policy-generator.ts',
|
||||
'packages/core/src/safety/context-builder.ts',
|
||||
'packages/core/src/scheduler/confirmation.ts',
|
||||
'packages/core/src/scheduler/scheduler.ts',
|
||||
'packages/core/src/scheduler/state-manager.ts',
|
||||
'packages/core/src/scheduler/tool-executor.ts',
|
||||
'packages/core/src/services/environmentSanitization.ts',
|
||||
'packages/core/src/services/modelConfigService.ts',
|
||||
'packages/core/src/services/sessionSummaryUtils.ts',
|
||||
'packages/core/src/services/shellExecutionService.ts',
|
||||
'packages/core/src/services/toolOutputMaskingService.ts',
|
||||
'packages/core/src/skills/skillLoader.ts',
|
||||
'packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts',
|
||||
'packages/core/src/telemetry/startupProfiler.ts',
|
||||
'packages/core/src/telemetry/telemetryAttributes.ts',
|
||||
'packages/core/src/telemetry/types.ts',
|
||||
'packages/core/src/telemetry/uiTelemetry.ts',
|
||||
'packages/core/src/tools/grep-utils.ts',
|
||||
'packages/core/src/tools/mcp-client.ts',
|
||||
'packages/core/src/tools/shell.ts',
|
||||
'packages/core/src/utils/bfsFileSearch.ts',
|
||||
'packages/core/src/utils/editCorrector.ts',
|
||||
'packages/core/src/utils/errors.ts',
|
||||
'packages/core/src/utils/fileDiffUtils.ts',
|
||||
'packages/core/src/utils/fileUtils.ts',
|
||||
'packages/core/src/utils/filesearch/crawler.ts',
|
||||
'packages/core/src/utils/filesearch/fileSearch.ts',
|
||||
'packages/core/src/utils/filesearch/result-cache.ts',
|
||||
'packages/core/src/utils/headless.ts',
|
||||
'packages/core/src/utils/ignoreFileParser.ts',
|
||||
'packages/core/src/utils/ignorePatterns.ts',
|
||||
'packages/core/src/utils/llm-edit-fixer.ts',
|
||||
'packages/core/src/utils/nextSpeakerChecker.ts',
|
||||
'packages/core/src/utils/partUtils.ts',
|
||||
'packages/core/src/utils/retry.ts',
|
||||
'packages/core/src/utils/safeJsonStringify.ts',
|
||||
'packages/core/src/utils/sessionUtils.ts',
|
||||
'packages/core/src/utils/shell-utils.ts',
|
||||
'packages/sdk/src/session.ts',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Legacy files with many @typescript-eslint/no-unnecessary-type-assertion issues
|
||||
files: ['packages/core/src/core/client.ts', 'packages/core/src/core/geminiChat.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
},
|
||||
},
|
||||
// extra settings for scripts that we run directly with node
|
||||
{
|
||||
files: ['./integration-tests/**/*.js'],
|
||||
|
||||
@@ -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}}]}
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -28,9 +28,6 @@ import {
|
||||
type Config,
|
||||
type UserTierId,
|
||||
type ToolLiveOutput,
|
||||
type AnsiLine,
|
||||
type AnsiOutput,
|
||||
type AnsiToken,
|
||||
isSubagentProgress,
|
||||
EDIT_TOOL_NAMES,
|
||||
processRestorableToolCalls,
|
||||
@@ -347,15 +344,10 @@ export class Task {
|
||||
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(
|
||||
@@ -801,7 +793,7 @@ export class Task {
|
||||
) {
|
||||
errorEvent = event;
|
||||
}
|
||||
const errorMessage = errorEvent?.value?.error
|
||||
const errorMessage = errorEvent?.value.error
|
||||
? getErrorMessage(errorEvent.value.error)
|
||||
: 'Unknown error from LLM stream';
|
||||
logger.error(
|
||||
@@ -810,7 +802,7 @@ export class Task {
|
||||
);
|
||||
|
||||
let errMessage = `Unknown error from LLM stream: ${JSON.stringify(event)}`;
|
||||
if (errorEvent?.value?.error) {
|
||||
if (errorEvent?.value.error) {
|
||||
errMessage = parseAndFormatApiError(errorEvent.value.error);
|
||||
}
|
||||
this.cancelPendingTools(`LLM stream error: ${errorMessage}`);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,9 +23,6 @@ import {
|
||||
fetchAdminControlsOnce,
|
||||
getCodeAssistServer,
|
||||
ExperimentFlags,
|
||||
isHeadlessMode,
|
||||
FatalAuthenticationError,
|
||||
isCloudShell,
|
||||
type TelemetryTarget,
|
||||
type ConfigParameters,
|
||||
type ExtensionLoader,
|
||||
@@ -106,15 +103,15 @@ export async function loadConfig(
|
||||
trustedFolder: true,
|
||||
extensionLoader,
|
||||
checkpointing,
|
||||
interactive: !isHeadlessMode(),
|
||||
enableInteractiveShell: !isHeadlessMode(),
|
||||
interactive: true,
|
||||
enableInteractiveShell: true,
|
||||
ptyInfo: 'auto',
|
||||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir, {
|
||||
respectGitIgnore: configParams?.fileFiltering?.respectGitIgnore,
|
||||
respectGeminiIgnore: configParams?.fileFiltering?.respectGeminiIgnore,
|
||||
customIgnoreFilePaths: configParams?.fileFiltering?.customIgnoreFilePaths,
|
||||
respectGitIgnore: configParams.fileFiltering?.respectGitIgnore,
|
||||
respectGeminiIgnore: configParams.fileFiltering?.respectGeminiIgnore,
|
||||
customIgnoreFilePaths: configParams.fileFiltering?.customIgnoreFilePaths,
|
||||
});
|
||||
const { memoryContent, fileCount, filePaths } =
|
||||
await loadServerHierarchicalMemory(
|
||||
@@ -215,7 +212,7 @@ export function loadEnvironment(): void {
|
||||
|
||||
function findEnvFile(startDir: string): string | null {
|
||||
let currentDir = path.resolve(startDir);
|
||||
while (true) {
|
||||
for (;;) {
|
||||
// prefer gemini-specific .env under GEMINI_DIR
|
||||
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
|
||||
if (fs.existsSync(geminiEnvPath)) {
|
||||
@@ -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']}`,
|
||||
);
|
||||
|
||||
@@ -148,7 +148,7 @@ function isPersistedStateMetadata(
|
||||
export function getPersistedState(
|
||||
metadata: PersistedTaskMetadata,
|
||||
): PersistedStateMetadata | undefined {
|
||||
const state = metadata?.[METADATA_KEY];
|
||||
const state = metadata[METADATA_KEY];
|
||||
if (isPersistedStateMetadata(state)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export function assertUniqueFinalEventIsLast(
|
||||
expect(finalEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'state-change',
|
||||
});
|
||||
expect(finalEvent.status?.state).toBe('input-required');
|
||||
expect(finalEvent.status.state).toBe('input-required');
|
||||
expect(finalEvent.final).toBe(true);
|
||||
|
||||
// There is only one event with final and its the last
|
||||
|
||||
@@ -45,7 +45,7 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
|
||||
const { name, setting, scope } = args;
|
||||
const settings = loadSettings(process.cwd()).merged;
|
||||
|
||||
if (!(settings.experimental?.extensionConfig ?? true)) {
|
||||
if (!(settings.experimental.extensionConfig ?? true)) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
extensionManager,
|
||||
updateState,
|
||||
() => {},
|
||||
settings.experimental?.extensionReloading,
|
||||
settings.experimental.extensionReloading,
|
||||
))!;
|
||||
if (
|
||||
updatedExtensionInfo.originalVersion !==
|
||||
|
||||
@@ -99,12 +99,6 @@ export async function configureSpecificSetting(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (!extensionConfig) {
|
||||
logger.error(
|
||||
`Could not find configuration for extension "${extensionName}".`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateSetting(
|
||||
extensionConfig,
|
||||
@@ -137,11 +131,7 @@ export async function configureExtension(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
!extensionConfig ||
|
||||
!extensionConfig.settings ||
|
||||
extensionConfig.settings.length === 0
|
||||
) {
|
||||
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
||||
logger.log(`Extension "${extensionName}" has no settings to configure.`);
|
||||
return;
|
||||
}
|
||||
@@ -175,11 +165,7 @@ export async function configureAllExtensions(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
extensionConfig &&
|
||||
extensionConfig.settings &&
|
||||
extensionConfig.settings.length > 0
|
||||
) {
|
||||
if (extensionConfig.settings && extensionConfig.settings.length > 0) {
|
||||
logger.log(`\nConfiguring settings for "${extension.name}"...`);
|
||||
await configureExtensionSettings(
|
||||
extensionConfig,
|
||||
@@ -208,7 +194,7 @@ export async function configureExtensionSettings(
|
||||
process.cwd(),
|
||||
);
|
||||
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string | undefined> = {};
|
||||
if (scope === ExtensionSettingScope.USER) {
|
||||
workspaceSettings = await getScopedEnvContents(
|
||||
extensionConfig,
|
||||
|
||||
@@ -236,10 +236,7 @@ export async function handleMigrateFromClaude() {
|
||||
const settings = loadSettings(workingDir);
|
||||
|
||||
// Merge migrated hooks with existing hooks
|
||||
const existingHooks = (settings.merged?.hooks || {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const existingHooks = settings.merged.hooks as Record<string, unknown>;
|
||||
const mergedHooks = { ...existingHooks, ...migratedHooks };
|
||||
|
||||
// Update settings (setValue automatically saves)
|
||||
|
||||
@@ -117,7 +117,7 @@ async function addMcpServer(
|
||||
const existingSettings = settings.forScope(settingsScope).settings;
|
||||
const mcpServers = existingSettings.mcpServers || {};
|
||||
|
||||
const isExistingServer = !!mcpServers[name];
|
||||
const isExistingServer = Object.hasOwn(mcpServers, name);
|
||||
if (isExistingServer) {
|
||||
debugLogger.log(
|
||||
`MCP server "${name}" is already configured within ${scope} settings.`,
|
||||
@@ -211,11 +211,12 @@ export const addCommand: CommandModule = {
|
||||
})
|
||||
.middleware((argv) => {
|
||||
// Handle -- separator args as server args if present
|
||||
if (argv['--']) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const dashArgs = argv['--'] as string[] | undefined;
|
||||
if (dashArgs && dashArgs.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const existingArgs = (argv['args'] as Array<string | number>) || [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
argv['args'] = [...existingArgs, ...(argv['--'] as string[])];
|
||||
const existingArgs = argv['args'] as Array<string | number>;
|
||||
argv['args'] = [...existingArgs, ...dashArgs];
|
||||
}
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
|
||||
@@ -43,9 +43,9 @@ async function handleEnable(args: Args): Promise<void> {
|
||||
}
|
||||
|
||||
const result = await canLoadServer(name, {
|
||||
adminMcpEnabled: settings.merged.admin?.mcp?.enabled ?? true,
|
||||
allowedList: settings.merged.mcp?.allowed,
|
||||
excludedList: settings.merged.mcp?.excluded,
|
||||
adminMcpEnabled: settings.merged.admin.mcp.enabled,
|
||||
allowedList: settings.merged.mcp.allowed,
|
||||
excludedList: settings.merged.mcp.excluded,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -39,10 +39,12 @@ export async function getMcpServersFromConfig(
|
||||
requestSetting: promptForSetting,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const mcpServers = { ...settings.mcpServers };
|
||||
const mcpServers: Record<string, MCPServerConfig> = {
|
||||
...settings.mcpServers,
|
||||
};
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
if (mcpServers[key]) {
|
||||
if (key in mcpServers) {
|
||||
return;
|
||||
}
|
||||
mcpServers[key] = {
|
||||
@@ -52,7 +54,7 @@ export async function getMcpServersFromConfig(
|
||||
});
|
||||
}
|
||||
|
||||
const adminAllowlist = settings.admin?.mcp?.config;
|
||||
const adminAllowlist = settings.admin.mcp.config;
|
||||
const filteredResult = applyAdminAllowlist(mcpServers, adminAllowlist);
|
||||
|
||||
return filteredResult;
|
||||
|
||||
@@ -24,7 +24,7 @@ async function removeMcpServer(
|
||||
const existingSettings = settings.forScope(settingsScope).settings;
|
||||
const mcpServers = existingSettings.mcpServers || {};
|
||||
|
||||
if (!mcpServers[name]) {
|
||||
if (!(name in mcpServers)) {
|
||||
debugLogger.log(`Server "${name}" not found in ${scope} settings.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -953,6 +953,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 +1080,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 +1163,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 +1184,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 +1205,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 +1251,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 +1315,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 +2164,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 +2204,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 +2213,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 +3326,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(),
|
||||
|
||||
@@ -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,
|
||||
@@ -322,11 +327,11 @@ export async function parseArguments(
|
||||
return true;
|
||||
});
|
||||
|
||||
if (settings.experimental?.extensionManagement) {
|
||||
if (settings.experimental.extensionManagement) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings.skills?.enabled ?? true) {
|
||||
if (settings.skills.enabled) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
// Register hooks command if hooks are enabled
|
||||
@@ -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 ||
|
||||
@@ -421,16 +456,16 @@ export async function loadCliConfig(
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
}
|
||||
|
||||
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
||||
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
|
||||
const memoryImportFormat = settings.context.importFormat || 'tree';
|
||||
const includeDirectoryTree = settings.context.includeDirectoryTree;
|
||||
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
const ideMode = settings.ide.enabled;
|
||||
|
||||
const folderTrust =
|
||||
process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true' ||
|
||||
process.env['VITEST'] === 'true'
|
||||
? false
|
||||
: (settings.security?.folderTrust?.enabled ?? false);
|
||||
: settings.security.folderTrust.enabled;
|
||||
const trustedFolder =
|
||||
isWorkspaceTrusted(settings, cwd, undefined, {
|
||||
prompt: argv.prompt,
|
||||
@@ -441,7 +476,7 @@ export async function loadCliConfig(
|
||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
|
||||
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
|
||||
if (settings.context?.fileName) {
|
||||
if (settings.context.fileName) {
|
||||
setServerGeminiMdFilename(settings.context.fileName);
|
||||
} else {
|
||||
// Reset to default if not provided in settings.
|
||||
@@ -452,15 +487,15 @@ export async function loadCliConfig(
|
||||
|
||||
const memoryFileFiltering = {
|
||||
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
...settings.context?.fileFiltering,
|
||||
...settings.context.fileFiltering,
|
||||
};
|
||||
|
||||
const fileFiltering = {
|
||||
...DEFAULT_FILE_FILTERING_OPTIONS,
|
||||
...settings.context?.fileFiltering,
|
||||
...settings.context.fileFiltering,
|
||||
};
|
||||
|
||||
const includeDirectories = (settings.context?.includeDirectories || [])
|
||||
const includeDirectories = settings.context.includeDirectories
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
@@ -480,7 +515,7 @@ export async function loadCliConfig(
|
||||
.getExtensions()
|
||||
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
|
||||
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
const experimentalJitContext = settings.experimental.jitContext;
|
||||
|
||||
let memoryContent: string | HierarchicalMemory = '';
|
||||
let fileCount = 0;
|
||||
@@ -490,7 +525,7 @@ export async function loadCliConfig(
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false
|
||||
settings.context.loadMemoryFromIncludeDirectories
|
||||
? includeDirectories
|
||||
: [],
|
||||
debugMode,
|
||||
@@ -499,7 +534,7 @@ export async function loadCliConfig(
|
||||
trustedFolder,
|
||||
memoryImportFormat,
|
||||
memoryFileFiltering,
|
||||
settings.context?.discoveryMaxDirs,
|
||||
settings.context.discoveryMaxDirs,
|
||||
);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
@@ -513,8 +548,8 @@ export async function loadCliConfig(
|
||||
const rawApprovalMode =
|
||||
argv.approvalMode ||
|
||||
(argv.yolo ? 'yolo' : undefined) ||
|
||||
((settings.general?.defaultApprovalMode as string) !== 'yolo'
|
||||
? settings.general?.defaultApprovalMode
|
||||
((settings.general.defaultApprovalMode as string) !== 'yolo'
|
||||
? settings.general.defaultApprovalMode
|
||||
: undefined);
|
||||
|
||||
if (rawApprovalMode) {
|
||||
@@ -526,7 +561,7 @@ export async function loadCliConfig(
|
||||
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case 'plan':
|
||||
if (!(settings.experimental?.plan ?? false)) {
|
||||
if (!settings.experimental.plan) {
|
||||
debugLogger.warn(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
||||
);
|
||||
@@ -548,9 +583,9 @@ export async function loadCliConfig(
|
||||
}
|
||||
|
||||
// Override approval mode if disableYoloMode is set.
|
||||
if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
|
||||
if (settings.security.disableYoloMode || settings.admin.secureModeEnabled) {
|
||||
if (approvalMode === ApprovalMode.YOLO) {
|
||||
if (settings.admin?.secureModeEnabled) {
|
||||
if (settings.admin.secureModeEnabled) {
|
||||
debugLogger.error(
|
||||
'YOLO mode is disabled by "secureModeEnabled" setting.',
|
||||
);
|
||||
@@ -601,15 +636,50 @@ export async function loadCliConfig(
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
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);
|
||||
@@ -624,7 +694,7 @@ export async function loadCliConfig(
|
||||
},
|
||||
mcp: {
|
||||
...settings.mcp,
|
||||
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
|
||||
allowed: argv.allowedMcpServerNames ?? settings.mcp.allowed,
|
||||
},
|
||||
policyPaths: argv.policy,
|
||||
};
|
||||
@@ -645,7 +715,7 @@ export async function loadCliConfig(
|
||||
|
||||
const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
|
||||
const specifiedModel =
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model.name;
|
||||
|
||||
const resolvedModel =
|
||||
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
@@ -659,9 +729,9 @@ export async function loadCliConfig(
|
||||
|
||||
const ptyInfo = await getPty();
|
||||
|
||||
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
|
||||
const extensionsEnabled = settings.admin?.extensions?.enabled ?? true;
|
||||
const adminSkillsEnabled = settings.admin?.skills?.enabled ?? true;
|
||||
const mcpEnabled = settings.admin.mcp.enabled;
|
||||
const extensionsEnabled = settings.admin.extensions.enabled;
|
||||
const adminSkillsEnabled = settings.admin.skills.enabled;
|
||||
|
||||
// Create MCP enablement manager and callbacks
|
||||
const mcpEnablementManager = McpServerEnablementManager.getInstance();
|
||||
@@ -669,8 +739,8 @@ export async function loadCliConfig(
|
||||
? mcpEnablementManager.getEnablementCallbacks()
|
||||
: undefined;
|
||||
|
||||
const adminAllowlist = settings.admin?.mcp?.config;
|
||||
let mcpServerCommand = mcpEnabled ? settings.mcp?.serverCommand : undefined;
|
||||
const adminAllowlist = settings.admin.mcp.config;
|
||||
let mcpServerCommand = mcpEnabled ? settings.mcp.serverCommand : undefined;
|
||||
let mcpServers = mcpEnabled ? settings.mcpServers : {};
|
||||
|
||||
if (mcpEnabled && adminAllowlist && Object.keys(adminAllowlist).length > 0) {
|
||||
@@ -678,7 +748,7 @@ export async function loadCliConfig(
|
||||
mcpServers = result.mcpServers;
|
||||
mcpServerCommand = undefined;
|
||||
|
||||
if (result.blockedServerNames && result.blockedServerNames.length > 0) {
|
||||
if (result.blockedServerNames.length > 0) {
|
||||
const message = getAdminBlockedMcpServersMessage(
|
||||
result.blockedServerNames,
|
||||
undefined,
|
||||
@@ -696,17 +766,17 @@ export async function loadCliConfig(
|
||||
includeDirectoryTree,
|
||||
includeDirectories,
|
||||
loadMemoryFromIncludeDirectories:
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false,
|
||||
settings.context.loadMemoryFromIncludeDirectories,
|
||||
debugMode,
|
||||
question,
|
||||
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
coreTools: settings.tools.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
policyEngineConfig,
|
||||
policyUpdateConfirmationRequest,
|
||||
excludeTools,
|
||||
toolDiscoveryCommand: settings.tools?.discoveryCommand,
|
||||
toolCallCommand: settings.tools?.callCommand,
|
||||
toolDiscoveryCommand: settings.tools.discoveryCommand,
|
||||
toolCallCommand: settings.tools.callCommand,
|
||||
mcpServerCommand,
|
||||
mcpServers,
|
||||
mcpEnablementCallbacks,
|
||||
@@ -715,32 +785,32 @@ export async function loadCliConfig(
|
||||
agents: settings.agents,
|
||||
adminSkillsEnabled,
|
||||
allowedMcpServers: mcpEnabled
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp.allowed)
|
||||
: undefined,
|
||||
blockedMcpServers: mcpEnabled
|
||||
? argv.allowedMcpServerNames
|
||||
? undefined
|
||||
: settings.mcp?.excluded
|
||||
: settings.mcp.excluded
|
||||
: undefined,
|
||||
blockedEnvironmentVariables:
|
||||
settings.security?.environmentVariableRedaction?.blocked,
|
||||
settings.security.environmentVariableRedaction.blocked,
|
||||
enableEnvironmentVariableRedaction:
|
||||
settings.security?.environmentVariableRedaction?.enabled,
|
||||
settings.security.environmentVariableRedaction.enabled,
|
||||
userMemory: memoryContent,
|
||||
geminiMdFileCount: fileCount,
|
||||
geminiMdFilePaths: filePaths,
|
||||
approvalMode,
|
||||
disableYoloMode:
|
||||
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
||||
showMemoryUsage: settings.ui?.showMemoryUsage || false,
|
||||
settings.security.disableYoloMode || settings.admin.secureModeEnabled,
|
||||
showMemoryUsage: settings.ui.showMemoryUsage,
|
||||
accessibility: {
|
||||
...settings.ui?.accessibility,
|
||||
...settings.ui.accessibility,
|
||||
screenReader,
|
||||
},
|
||||
telemetry: telemetrySettings,
|
||||
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled,
|
||||
usageStatisticsEnabled: settings.privacy.usageStatisticsEnabled,
|
||||
fileFiltering,
|
||||
checkpointing: settings.general?.checkpointing?.enabled,
|
||||
checkpointing: settings.general.checkpointing.enabled,
|
||||
proxy:
|
||||
process.env['HTTPS_PROXY'] ||
|
||||
process.env['https_proxy'] ||
|
||||
|
||||
@@ -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/,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,16 +154,11 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
): Promise<GeminiCLIExtension> {
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
this.settings.security?.allowedExtensions.length > 0
|
||||
) {
|
||||
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||
if (this.settings.security.allowedExtensions.length > 0) {
|
||||
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 +207,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 +259,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}`);
|
||||
}
|
||||
@@ -312,7 +309,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
const destinationPath = new ExtensionStorage(
|
||||
newExtensionName,
|
||||
).getExtensionDir();
|
||||
let previousSettings: Record<string, string> | undefined;
|
||||
let previousSettings: Record<string, string | undefined> | undefined;
|
||||
if (isUpdate) {
|
||||
previousSettings = await getEnvContents(
|
||||
previousExtensionConfig,
|
||||
@@ -626,21 +623,16 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
const installMetadata = loadInstallMetadata(extensionDir);
|
||||
let effectiveExtensionPath = extensionDir;
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
this.settings.security?.allowedExtensions.length > 0
|
||||
) {
|
||||
if (this.settings.security.allowedExtensions.length > 0) {
|
||||
if (!installMetadata?.source) {
|
||||
throw new Error(
|
||||
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
|
||||
);
|
||||
}
|
||||
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||
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)}`,
|
||||
@@ -674,8 +666,8 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
const extensionId = getExtensionId(config, installMetadata);
|
||||
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
let userSettings: Record<string, string | undefined> = {};
|
||||
let workspaceSettings: Record<string, string | undefined> = {};
|
||||
|
||||
if (this.settings.experimental.extensionConfig) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
|
||||
@@ -166,7 +166,7 @@ export class ExtensionEnablementManager {
|
||||
const extensionConfig = config[extensionName];
|
||||
// Extensions are enabled by default.
|
||||
let enabled = true;
|
||||
const allOverrides = extensionConfig?.overrides ?? [];
|
||||
const allOverrides = extensionConfig.overrides ?? [];
|
||||
for (const rule of allOverrides) {
|
||||
const override = Override.fromFileRule(rule);
|
||||
if (override.matchesPath(ensureLeadingAndTrailingSlash(currentPath))) {
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function maybePromptForSettings(
|
||||
extensionId: string,
|
||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
previousSettings?: Record<string, string>,
|
||||
previousSettings?: Record<string, string | undefined>,
|
||||
): Promise<void> {
|
||||
const { name: extensionName, settings } = extensionConfig;
|
||||
if (
|
||||
@@ -92,7 +92,9 @@ export async function maybePromptForSettings(
|
||||
previousExtensionConfig?.settings ?? [],
|
||||
);
|
||||
|
||||
const allSettings: Record<string, string> = { ...previousSettings };
|
||||
const allSettings: Record<string, string | undefined> = {
|
||||
...previousSettings,
|
||||
};
|
||||
|
||||
for (const removedEnvSetting of settingsChanges.removeEnv) {
|
||||
delete allSettings[removedEnvSetting.envVar];
|
||||
@@ -174,13 +176,13 @@ export async function getScopedEnvContents(
|
||||
extensionId: string,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
): Promise<Record<string, string>> {
|
||||
): Promise<Record<string, string | undefined>> {
|
||||
const { name: extensionName } = extensionConfig;
|
||||
const keychain = new KeychainTokenStorage(
|
||||
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
|
||||
);
|
||||
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
|
||||
let customEnv: Record<string, string> = {};
|
||||
let customEnv: Record<string, string | undefined> = {};
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
const stat = fsSync.statSync(envFilePath);
|
||||
if (!stat.isDirectory()) {
|
||||
@@ -206,7 +208,7 @@ export async function getEnvContents(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
workspaceDir: string,
|
||||
): Promise<Record<string, string>> {
|
||||
): Promise<Record<string, string | undefined>> {
|
||||
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
@@ -109,16 +109,16 @@ export function tryParseGithubUrl(source: string): GithubRepoInfo | null {
|
||||
if (!parsedUrl) {
|
||||
throw new Error(`Invalid repo URL: ${source}`);
|
||||
}
|
||||
if (parsedUrl?.host !== 'github.com') {
|
||||
if (parsedUrl.host !== 'github.com') {
|
||||
return null;
|
||||
}
|
||||
// The pathname should be "/owner/repo".
|
||||
const parts = parsedUrl?.pathname
|
||||
const parts = parsedUrl.pathname
|
||||
.split('/')
|
||||
// Remove the empty segments, fixes trailing and leading slashes
|
||||
.filter((part) => part !== '');
|
||||
|
||||
if (parts?.length !== 2) {
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function updateExtension(
|
||||
`Extension ${extension.name} cannot be updated, type is unknown.`,
|
||||
);
|
||||
}
|
||||
if (installMetadata?.type === 'link') {
|
||||
if (installMetadata.type === 'link') {
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.UP_TO_DATE },
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -226,7 +226,7 @@ export class McpServerEnablementManager {
|
||||
async isFileEnabled(serverName: string): Promise<boolean> {
|
||||
const config = await this.readConfig();
|
||||
const state = config[normalizeServerId(serverName)];
|
||||
return state?.enabled ?? true;
|
||||
return state.enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -27,7 +27,6 @@ const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
|
||||
'docker',
|
||||
'podman',
|
||||
'sandbox-exec',
|
||||
'lxc',
|
||||
];
|
||||
|
||||
function isSandboxCommand(value: string): value is SandboxConfig['command'] {
|
||||
@@ -46,7 +45,7 @@ function getSandboxCommand(
|
||||
const environmentConfiguredSandbox =
|
||||
process.env['GEMINI_SANDBOX']?.toLowerCase().trim() ?? '';
|
||||
sandbox =
|
||||
environmentConfiguredSandbox?.length > 0
|
||||
environmentConfiguredSandbox.length > 0
|
||||
? environmentConfiguredSandbox
|
||||
: sandbox;
|
||||
if (sandbox === '1' || sandbox === 'true') sandbox = true;
|
||||
@@ -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(
|
||||
|
||||
@@ -460,7 +460,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.folderTrust?.enabled).toBe(false); // Workspace setting should be used
|
||||
expect(settings.merged.security.folderTrust?.enabled).toBe(false); // Workspace setting should be used
|
||||
});
|
||||
|
||||
it('should use system folderTrust over user setting', () => {
|
||||
@@ -500,7 +500,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
|
||||
expect(settings.merged.security.folderTrust?.enabled).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it('should not allow user or workspace to override system disableYoloMode', () => {
|
||||
@@ -534,7 +534,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
|
||||
expect(settings.merged.security.disableYoloMode).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -630,7 +630,7 @@ describe('Settings Loading and Merging', () => {
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'NODE_ENV',
|
||||
@@ -658,7 +658,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.context?.fileName).toBeUndefined();
|
||||
expect(settings.merged.context.fileName).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -991,7 +991,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.model?.compressionThreshold).toEqual(expected);
|
||||
expect(settings.merged.model.compressionThreshold).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1018,7 +1018,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.model?.compressionThreshold).toEqual(0.5);
|
||||
expect(settings.merged.model.compressionThreshold).toEqual(0.5);
|
||||
});
|
||||
|
||||
it('should merge includeDirectories from all scopes', () => {
|
||||
@@ -1052,7 +1052,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.context?.includeDirectories).toEqual([
|
||||
expect(settings.merged.context.includeDirectories).toEqual([
|
||||
'/system/defaults/dir',
|
||||
'/user/dir1',
|
||||
'/user/dir2',
|
||||
@@ -1247,7 +1247,7 @@ describe('Settings Loading and Merging', () => {
|
||||
expect((settings.merged as TestSettings)['workspaceOnly']).toBe(
|
||||
'workspace_value',
|
||||
);
|
||||
expect(settings.merged.ui?.theme).toBe('light'); // workspace overrides user
|
||||
expect(settings.merged.ui.theme).toBe('light'); // workspace overrides user
|
||||
|
||||
delete process.env['SYSTEM_VAR'];
|
||||
delete process.env['USER_VAR'];
|
||||
@@ -1275,7 +1275,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
|
||||
expect(settings.merged.advanced.dnsResolutionOrder).toBe('verbatim');
|
||||
});
|
||||
|
||||
it('should use user dnsResolutionOrder if workspace is not defined', () => {
|
||||
@@ -1294,7 +1294,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
|
||||
expect(settings.merged.advanced.dnsResolutionOrder).toBe('verbatim');
|
||||
});
|
||||
|
||||
it('should leave unresolved environment variables as is', () => {
|
||||
@@ -1599,7 +1599,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify the settings were loaded correctly
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
]);
|
||||
@@ -1637,7 +1637,7 @@ describe('Settings Loading and Merging', () => {
|
||||
'NODE_ENV',
|
||||
'DEBUG',
|
||||
]);
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'NODE_ENV',
|
||||
@@ -1677,7 +1677,7 @@ describe('Settings Loading and Merging', () => {
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'NODE_ENV',
|
||||
@@ -1711,9 +1711,9 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.tools?.sandbox).toBe(true);
|
||||
expect(settings.merged.context?.fileName).toBe('WORKSPACE.md');
|
||||
expect(settings.merged.ui?.theme).toBe('dark');
|
||||
expect(settings.merged.tools.sandbox).toBe(true);
|
||||
expect(settings.merged.context.fileName).toBe('WORKSPACE.md');
|
||||
expect(settings.merged.ui.theme).toBe('dark');
|
||||
});
|
||||
|
||||
it('should NOT merge workspace settings when workspace is not trusted', () => {
|
||||
@@ -1744,9 +1744,9 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
|
||||
expect(settings.merged.ui?.theme).toBe('dark'); // User setting
|
||||
expect(settings.merged.tools.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context.fileName).toBe('USER.md'); // User setting
|
||||
expect(settings.merged.ui.theme).toBe('dark'); // User setting
|
||||
});
|
||||
|
||||
it('should NOT merge workspace settings when workspace trust is undefined', () => {
|
||||
@@ -1777,8 +1777,8 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
|
||||
expect(settings.merged.tools.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context.fileName).toBe('USER.md'); // User setting
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -2244,7 +2229,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify it was migrated in the merged settings
|
||||
expect(settings.merged.general?.enableAutoUpdate).toBe(false);
|
||||
expect(settings.merged.general.enableAutoUpdate).toBe(false);
|
||||
|
||||
// Verify it was saved back to disk (via setValue calling updateSettingsFilePreservingFormat)
|
||||
expect(updateSettingsFilePreservingFormat).toHaveBeenCalledWith(
|
||||
@@ -2304,7 +2289,7 @@ describe('Settings Loading and Merging', () => {
|
||||
).toBe(true);
|
||||
|
||||
// Merged should also reflect it (system overrides defaults, but both are migrated)
|
||||
expect(settings.merged.general?.enableAutoUpdateNotification).toBe(false);
|
||||
expect(settings.merged.general.enableAutoUpdateNotification).toBe(false);
|
||||
|
||||
// Verify it was NOT saved back to disk
|
||||
expect(updateSettingsFilePreservingFormat).not.toHaveBeenCalledWith(
|
||||
@@ -2502,10 +2487,10 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
// 1. Verify that on initial load, file-based admin settings are ignored
|
||||
// and schema defaults are used instead.
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false); // default: false
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('system-theme'); // non-admin setting should be loaded
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false); // default: false
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.ui.theme).toBe('system-theme'); // non-admin setting should be loaded
|
||||
|
||||
// 2. Now, set remote admin settings.
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
@@ -2518,11 +2503,11 @@ describe('Settings Loading and Merging', () => {
|
||||
});
|
||||
|
||||
// 3. Verify that remote admin settings take precedence.
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(false);
|
||||
// non-admin setting should remain unchanged
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('system-theme');
|
||||
expect(loadedSettings.merged.ui.theme).toBe('system-theme');
|
||||
});
|
||||
|
||||
it('should set remote admin settings and recompute merged settings', () => {
|
||||
@@ -2547,10 +2532,10 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
// Ensure initial state from defaults (as file-based admin settings are ignored)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.ui.theme).toBe('initial-theme');
|
||||
|
||||
const newRemoteSettings = {
|
||||
strictModeDisabled: false,
|
||||
@@ -2564,11 +2549,11 @@ describe('Settings Loading and Merging', () => {
|
||||
loadedSettings.setRemoteAdminSettings(newRemoteSettings);
|
||||
|
||||
// Verify that remote admin settings are applied
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(false);
|
||||
// Non-admin settings should remain untouched
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
||||
expect(loadedSettings.merged.ui.theme).toBe('initial-theme');
|
||||
});
|
||||
|
||||
it('should correctly handle undefined remote admin settings', () => {
|
||||
@@ -2588,16 +2573,16 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
// Should have default admin settings
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({}); // Set empty remote settings
|
||||
|
||||
// Admin settings should revert to defaults because there are no remote overrides
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should un-nest MCP configuration from remote settings', () => {
|
||||
@@ -2619,7 +2604,7 @@ describe('Settings Loading and Merging', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers);
|
||||
expect(loadedSettings.merged.admin.mcp?.config).toEqual(mcpServers);
|
||||
});
|
||||
|
||||
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||
@@ -2645,9 +2630,9 @@ describe('Settings Loading and Merging', () => {
|
||||
loadedSettings.setRemoteAdminSettings({});
|
||||
|
||||
// Should default to schema defaults (standard defaults)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -54,24 +54,20 @@ import {
|
||||
export function getMergeStrategyForPath(
|
||||
path: string[],
|
||||
): MergeStrategy | undefined {
|
||||
let current: SettingDefinition | undefined = undefined;
|
||||
let currentSchema: SettingsSchema | undefined = getSettingsSchema();
|
||||
let parent: SettingDefinition | undefined = undefined;
|
||||
|
||||
for (const key of path) {
|
||||
if (!currentSchema || !currentSchema[key]) {
|
||||
const current: SettingDefinition | undefined = currentSchema?.[key];
|
||||
if (!current) {
|
||||
// Key not found in schema - check if parent has additionalProperties
|
||||
if (parent?.additionalProperties?.mergeStrategy) {
|
||||
return parent.additionalProperties.mergeStrategy;
|
||||
}
|
||||
return undefined;
|
||||
return parent?.additionalProperties?.mergeStrategy;
|
||||
}
|
||||
parent = current;
|
||||
current = currentSchema[key];
|
||||
currentSchema = current.properties;
|
||||
}
|
||||
|
||||
return current?.mergeStrategy;
|
||||
return parent?.mergeStrategy;
|
||||
}
|
||||
|
||||
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
|
||||
@@ -372,19 +368,17 @@ export class LoadedSettings {
|
||||
// Remote admin settings always take precedence and file-based admin settings
|
||||
// are ignored.
|
||||
const adminSettingSchema = getSettingsSchema().admin;
|
||||
if (adminSettingSchema?.properties) {
|
||||
const adminSchema = adminSettingSchema.properties;
|
||||
const adminDefaults = getDefaultsFromSchema(adminSchema);
|
||||
const adminSchema = adminSettingSchema.properties;
|
||||
const adminDefaults = getDefaultsFromSchema(adminSchema);
|
||||
|
||||
// The final admin settings are the defaults overridden by remote settings.
|
||||
// Any admin settings from files are ignored.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
merged.admin = customDeepMerge(
|
||||
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
|
||||
adminDefaults,
|
||||
this._remoteAdminSettings?.admin ?? {},
|
||||
) as MergedSettings['admin'];
|
||||
}
|
||||
// The final admin settings are the defaults overridden by remote settings.
|
||||
// Any admin settings from files are ignored.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
merged.admin = customDeepMerge(
|
||||
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
|
||||
adminDefaults,
|
||||
this._remoteAdminSettings?.admin ?? {},
|
||||
) as MergedSettings['admin'];
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -493,7 +487,7 @@ export class LoadedSettings {
|
||||
|
||||
function findEnvFile(startDir: string): string | null {
|
||||
let currentDir = path.resolve(startDir);
|
||||
while (true) {
|
||||
for (;;) {
|
||||
// prefer gemini-specific .env under GEMINI_DIR
|
||||
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
|
||||
if (fs.existsSync(geminiEnvPath)) {
|
||||
@@ -583,7 +577,7 @@ export function loadEnvironment(
|
||||
const parsedEnv = dotenv.parse(envFileContent);
|
||||
|
||||
const excludedVars =
|
||||
settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
|
||||
settings.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
|
||||
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
|
||||
|
||||
for (const key in parsedEnv) {
|
||||
@@ -796,13 +790,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();
|
||||
@@ -1084,7 +1079,7 @@ function migrateExperimentalSettings(
|
||||
};
|
||||
const agentsOverrides = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
...((agentsSettings['overrides'] as Record<string, unknown>) || {}),
|
||||
...(agentsSettings['overrides'] as Record<string, unknown>),
|
||||
};
|
||||
let modified = false;
|
||||
|
||||
|
||||
@@ -565,34 +565,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: {
|
||||
@@ -1256,8 +1236,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,
|
||||
},
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('Settings Repro', () => {
|
||||
// If it doesn't throw, check if it merged correctly.
|
||||
// The model.compressionThreshold should be present.
|
||||
// And model.name should probably be undefined or default, but certainly NOT { compressionThreshold: 0.8 }
|
||||
expect(settings.merged.model?.compressionThreshold).toBe(0.8);
|
||||
expect(typeof settings.merged.model?.name).not.toBe('object');
|
||||
expect(settings.merged.model.compressionThreshold).toBe(0.8);
|
||||
expect(typeof settings.merged.model.name).not.toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function runDeferredCommand(settings: MergedSettings) {
|
||||
const adminSettings = settings.admin;
|
||||
const commandName = deferredCommand.commandName;
|
||||
|
||||
if (commandName === 'mcp' && adminSettings?.mcp?.enabled === false) {
|
||||
if (commandName === 'mcp' && adminSettings.mcp.enabled === false) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
getAdminErrorMessage('MCP', undefined /* config */),
|
||||
@@ -44,7 +44,7 @@ export async function runDeferredCommand(settings: MergedSettings) {
|
||||
|
||||
if (
|
||||
commandName === 'extensions' &&
|
||||
adminSettings?.extensions?.enabled === false
|
||||
adminSettings.extensions.enabled === false
|
||||
) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
@@ -54,7 +54,7 @@ export async function runDeferredCommand(settings: MergedSettings) {
|
||||
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
}
|
||||
|
||||
if (commandName === 'skills' && adminSettings?.skills?.enabled === false) {
|
||||
if (commandName === 'skills' && adminSettings.skills.enabled === false) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
getAdminErrorMessage('Agent skills', undefined /* config */),
|
||||
|
||||
@@ -378,7 +378,7 @@ export async function main() {
|
||||
|
||||
if (
|
||||
(argv.allowedTools && argv.allowedTools.length > 0) ||
|
||||
(settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0)
|
||||
(settings.merged.tools.allowed && settings.merged.tools.allowed.length > 0)
|
||||
) {
|
||||
coreEvents.emitFeedback(
|
||||
'warning',
|
||||
@@ -387,7 +387,7 @@ export async function main() {
|
||||
}
|
||||
|
||||
if (
|
||||
settings.merged.tools?.exclude &&
|
||||
settings.merged.tools.exclude &&
|
||||
settings.merged.tools.exclude.length > 0
|
||||
) {
|
||||
coreEvents.emitFeedback(
|
||||
@@ -748,7 +748,7 @@ export async function main() {
|
||||
? SessionStartSource.Resume
|
||||
: SessionStartSource.Startup;
|
||||
|
||||
const hookSystem = config?.getHookSystem();
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (hookSystem) {
|
||||
const result = await hookSystem.fireSessionStartEvent(sessionStartSource);
|
||||
|
||||
@@ -784,7 +784,7 @@ export async function main() {
|
||||
new UserPromptEvent(
|
||||
input.length,
|
||||
prompt_id,
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
config.getContentGeneratorConfig().authType,
|
||||
input,
|
||||
),
|
||||
);
|
||||
@@ -881,7 +881,7 @@ function setupAdminControlsListener() {
|
||||
type?: string;
|
||||
settings?: AdminControlsSettings;
|
||||
};
|
||||
if (message?.type === 'admin-settings' && message.settings) {
|
||||
if (message.type === 'admin-settings' && message.settings) {
|
||||
if (config) {
|
||||
config.setRemoteAdminSettings(message.settings);
|
||||
} else {
|
||||
|
||||
@@ -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/,
|
||||
|
||||
@@ -135,7 +135,7 @@ export async function runNonInteractive({
|
||||
key: { name?: string; ctrl?: boolean },
|
||||
) => {
|
||||
// Detect Ctrl+C: either ctrl+c key combo or raw character code 3
|
||||
if ((key && key.ctrl && key.name === 'c') || str === '\u0003') {
|
||||
if ((key.ctrl && key.name === 'c') || str === '\u0003') {
|
||||
// Only handle once
|
||||
if (isAborting) {
|
||||
return;
|
||||
@@ -289,7 +289,7 @@ export async function runNonInteractive({
|
||||
let currentMessages: Content[] = [{ role: 'user', parts: query }];
|
||||
|
||||
let turnCount = 0;
|
||||
while (true) {
|
||||
for (;;) {
|
||||
turnCount++;
|
||||
if (
|
||||
config.getMaxSessionTurns() >= 0 &&
|
||||
@@ -461,8 +461,8 @@ export async function runNonInteractive({
|
||||
(tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION,
|
||||
);
|
||||
|
||||
if (stopExecutionTool && stopExecutionTool.response.error) {
|
||||
const stopMessage = `Agent execution stopped: ${stopExecutionTool.response.error.message}`;
|
||||
if (stopExecutionTool) {
|
||||
const stopMessage = `Agent execution stopped: ${stopExecutionTool.response.error?.message}`;
|
||||
|
||||
if (config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
process.stderr.write(`${stopMessage}\n`);
|
||||
|
||||
@@ -56,14 +56,14 @@ export const handleSlashCommand = async (
|
||||
if (commandToExecute.action) {
|
||||
// Not used by custom commands but may be in the future.
|
||||
const sessionStats: SessionStatsState = {
|
||||
sessionId: config?.getSessionId(),
|
||||
sessionId: config.getSessionId(),
|
||||
sessionStartTime: new Date(),
|
||||
metrics: uiTelemetryService.getMetrics(),
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 1,
|
||||
};
|
||||
|
||||
const logger = new Logger(config?.getSessionId() || '', config?.storage);
|
||||
const logger = new Logger(config.getSessionId() || '', config.storage);
|
||||
|
||||
const context: CommandContext = {
|
||||
services: {
|
||||
|
||||
@@ -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,
|
||||
@@ -160,7 +158,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
themeCommand,
|
||||
toolsCommand,
|
||||
...(this.config?.isSkillsSupportEnabled()
|
||||
? this.config?.getSkillManager()?.isAdminEnabled() === false
|
||||
? this.config.getSkillManager().isAdminEnabled() === false
|
||||
? [
|
||||
{
|
||||
name: 'skills',
|
||||
|
||||
@@ -126,7 +126,7 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
if (
|
||||
!signal.aborted &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(error as { code?: string })?.code !== 'ENOENT'
|
||||
(error as { code?: string }).code !== 'ENOENT'
|
||||
) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
|
||||
@@ -62,7 +62,7 @@ export class McpPromptLoader implements ICommandLoader {
|
||||
let helpMessage = `Arguments for "${prompt.name}":\n\n`;
|
||||
if (prompt.arguments && prompt.arguments.length > 0) {
|
||||
helpMessage += `You can provide arguments by name (e.g., --argName="value") or by position.\n\n`;
|
||||
helpMessage += `e.g., ${prompt.name} ${prompt.arguments?.map((_) => `"foo"`)} is equivalent to ${prompt.name} ${prompt.arguments?.map((arg) => `--${arg.name}="foo"`)}\n\n`;
|
||||
helpMessage += `e.g., ${prompt.name} ${prompt.arguments.map((_) => `"foo"`)} is equivalent to ${prompt.name} ${prompt.arguments.map((arg) => `--${arg.name}="foo"`)}\n\n`;
|
||||
}
|
||||
for (const arg of prompt.arguments) {
|
||||
helpMessage += ` --${arg.name}\n`;
|
||||
@@ -123,7 +123,7 @@ export class McpPromptLoader implements ICommandLoader {
|
||||
};
|
||||
}
|
||||
|
||||
const maybeContent = result.messages?.[0]?.content;
|
||||
const maybeContent = result.messages[0]?.content;
|
||||
if (maybeContent.type !== 'text') {
|
||||
return {
|
||||
type: 'message',
|
||||
|
||||
@@ -119,7 +119,7 @@ export class ShellProcessor implements IPromptProcessor {
|
||||
|
||||
if (!command) continue;
|
||||
|
||||
if (context.session.sessionShellAllowlist?.has(command)) {
|
||||
if (context.session.sessionShellAllowlist.has(command)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ export class AppRig {
|
||||
const details = call.confirmationDetails;
|
||||
const title = 'title' in details ? details.title : '';
|
||||
const toolDisplayName =
|
||||
call.tool?.displayName || title.replace(/^Confirm:\s*/, '');
|
||||
call.tool.displayName || title.replace(/^Confirm:\s*/, '');
|
||||
if (!this.pendingConfirmations.has(call.correlationId)) {
|
||||
this.pendingConfirmations.set(call.correlationId, {
|
||||
toolName: call.request.name,
|
||||
@@ -469,7 +469,7 @@ export class AppRig {
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
if (await predicate()) return;
|
||||
|
||||
if (Date.now() - start > timeout) {
|
||||
@@ -622,7 +622,7 @@ export class AppRig {
|
||||
onConfirmation?: (confirmation: PendingConfirmation) => void | boolean,
|
||||
timeout = 60000,
|
||||
) {
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const event = await this.waitForNextEvent(timeout);
|
||||
if (event.type === 'idle') {
|
||||
break;
|
||||
@@ -710,7 +710,7 @@ export class AppRig {
|
||||
if (this.config) {
|
||||
const recordingService = this.config
|
||||
.getGeminiClient()
|
||||
?.getChatRecordingService();
|
||||
.getChatRecordingService();
|
||||
if (recordingService) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
|
||||
(recordingService as any).conversationFile = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function waitFor(
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
try {
|
||||
await assertion();
|
||||
return;
|
||||
|
||||
@@ -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"}]}]}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
||||
const b = v[c % 6];
|
||||
const g = v[Math.floor(c / 6) % 6];
|
||||
const r = v[Math.floor(c / 36) % 6];
|
||||
return `#${[r, g, b].map((x) => x?.toString(16).padStart(2, '0')).join('')}`;
|
||||
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
|
||||
} else if (colorCode >= 232 && colorCode <= 255) {
|
||||
const gray = 8 + (colorCode - 232) * 10;
|
||||
const hex = gray.toString(16).padStart(2, '0');
|
||||
|
||||
@@ -80,8 +80,8 @@ import {
|
||||
type ConsentRequestPayload,
|
||||
type AgentsDiscoveredPayload,
|
||||
ChangeAuthRequestedError,
|
||||
ProjectIdRequiredError,
|
||||
CoreToolCallStatus,
|
||||
generateSteeringAckMessage,
|
||||
buildUserSteeringHintPrompt,
|
||||
logBillingEvent,
|
||||
ApiKeyUpdatedEvent,
|
||||
@@ -771,12 +771,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)}`,
|
||||
);
|
||||
@@ -2108,6 +2102,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,
|
||||
|
||||
@@ -73,12 +73,8 @@ export function IdeIntegrationNudge({
|
||||
];
|
||||
|
||||
const installText = isExtensionPreInstalled
|
||||
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${
|
||||
ideName ?? 'your editor'
|
||||
}.`
|
||||
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
|
||||
ideName ?? 'your editor'
|
||||
}.`;
|
||||
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${ideName}.`
|
||||
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${ideName}.`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -92,7 +88,7 @@ export function IdeIntegrationNudge({
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
<Text>
|
||||
<Text color={theme.status.warning}>{'> '}</Text>
|
||||
{`Do you want to connect ${ideName ?? 'your editor'} to Gemini CLI?`}
|
||||
{`Do you want to connect ${ideName} to Gemini CLI?`}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>{installText}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function ApiAuthDialog({
|
||||
|
||||
const buffer = useTextBuffer({
|
||||
initialText: initialApiKey || '',
|
||||
initialCursorOffset: initialApiKey?.length || 0,
|
||||
initialCursorOffset: initialApiKey.length || 0,
|
||||
viewport: {
|
||||
width: viewportWidth,
|
||||
height: 4,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -68,5 +68,5 @@ async function getIdeClientName(context: CommandContext) {
|
||||
return '';
|
||||
}
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
return ideClient?.getDetectedIdeDisplayName() ?? '';
|
||||
return ideClient.getDetectedIdeDisplayName() ?? '';
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ async function enableAction(
|
||||
const allAgents = agentRegistry.getAllAgentNames();
|
||||
const overrides = settings.merged.agents.overrides;
|
||||
const disabledAgents = Object.keys(overrides).filter(
|
||||
(name) => overrides[name]?.enabled === false,
|
||||
(name) => overrides[name].enabled === false,
|
||||
);
|
||||
|
||||
if (allAgents.includes(agentName) && !disabledAgents.includes(agentName)) {
|
||||
@@ -167,7 +167,7 @@ async function disableAction(
|
||||
const allAgents = agentRegistry.getAllAgentNames();
|
||||
const overrides = settings.merged.agents.overrides;
|
||||
const disabledAgents = Object.keys(overrides).filter(
|
||||
(name) => overrides[name]?.enabled === false,
|
||||
(name) => overrides[name].enabled === false,
|
||||
);
|
||||
|
||||
if (disabledAgents.includes(agentName)) {
|
||||
@@ -271,7 +271,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
|
||||
|
||||
const overrides = settings.merged.agents.overrides;
|
||||
const disabledAgents = Object.entries(overrides)
|
||||
.filter(([_, override]) => override?.enabled === false)
|
||||
.filter(([_, override]) => override.enabled === false)
|
||||
.map(([name]) => name);
|
||||
|
||||
return disabledAgents.filter((name) => name.startsWith(partialArg));
|
||||
@@ -291,7 +291,7 @@ function completeAllAgents(context: CommandContext, partialArg: string) {
|
||||
if (!config) return [];
|
||||
|
||||
const agentRegistry = config.getAgentRegistry();
|
||||
const allAgents = agentRegistry?.getAllDiscoveredAgentNames() ?? [];
|
||||
const allAgents = agentRegistry.getAllDiscoveredAgentNames() ?? [];
|
||||
return allAgents.filter((name: string) => name.startsWith(partialArg));
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const authLogoutCommand: SlashCommand = {
|
||||
undefined,
|
||||
);
|
||||
// Strip thoughts from history instead of clearing completely
|
||||
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
|
||||
context.services.config?.getGeminiClient().stripThoughtsFromHistory();
|
||||
// Return logout action to signal explicit state change
|
||||
return {
|
||||
type: 'logout',
|
||||
|
||||
@@ -54,7 +54,7 @@ export const bugCommand: SlashCommand = {
|
||||
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
|
||||
? 'Supported'
|
||||
: 'Unsupported';
|
||||
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
|
||||
const authType = config?.getContentGeneratorConfig().authType || 'Unknown';
|
||||
|
||||
let info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
@@ -73,13 +73,13 @@ export const bugCommand: SlashCommand = {
|
||||
info += `* **IDE Client:** ${ideClient}\n`;
|
||||
}
|
||||
|
||||
const chat = config?.getGeminiClient()?.getChat();
|
||||
const chat = config?.getGeminiClient().getChat();
|
||||
const history = chat?.getHistory() || [];
|
||||
let historyFileMessage = '';
|
||||
let problemValue = bugDescription;
|
||||
|
||||
if (history.length > INITIAL_HISTORY_LENGTH) {
|
||||
const tempDir = config?.storage?.getProjectTempDir();
|
||||
const tempDir = config?.storage.getProjectTempDir();
|
||||
if (tempDir) {
|
||||
const historyFileName = `bug-report-history-${Date.now()}.json`;
|
||||
const historyFilePath = path.join(tempDir, historyFileName);
|
||||
|
||||
@@ -34,7 +34,7 @@ const getSavedChatTags = async (
|
||||
mtSortDesc: boolean,
|
||||
): Promise<ChatDetail[]> => {
|
||||
const cfg = context.services.config;
|
||||
const geminiDir = cfg?.storage?.getProjectTempDir();
|
||||
const geminiDir = cfg?.storage.getProjectTempDir();
|
||||
if (!geminiDir) {
|
||||
return [];
|
||||
}
|
||||
@@ -123,7 +123,7 @@ const saveCommand: SlashCommand = {
|
||||
}
|
||||
}
|
||||
|
||||
const chat = config?.getGeminiClient()?.getChat();
|
||||
const chat = config?.getGeminiClient().getChat();
|
||||
if (!chat) {
|
||||
return {
|
||||
type: 'message',
|
||||
@@ -134,7 +134,7 @@ const saveCommand: SlashCommand = {
|
||||
|
||||
const history = chat.getHistory();
|
||||
if (history.length > INITIAL_HISTORY_LENGTH) {
|
||||
const authType = config?.getContentGeneratorConfig()?.authType;
|
||||
const authType = config?.getContentGeneratorConfig().authType;
|
||||
await logger.saveCheckpoint({ history, authType }, tag);
|
||||
return {
|
||||
type: 'message',
|
||||
@@ -183,7 +183,7 @@ const resumeCommand: SlashCommand = {
|
||||
};
|
||||
}
|
||||
|
||||
const currentAuthType = config?.getContentGeneratorConfig()?.authType;
|
||||
const currentAuthType = config?.getContentGeneratorConfig().authType;
|
||||
if (
|
||||
checkpoint.authType &&
|
||||
currentAuthType &&
|
||||
@@ -296,7 +296,7 @@ const shareCommand: SlashCommand = {
|
||||
};
|
||||
}
|
||||
|
||||
const chat = context.services.config?.getGeminiClient()?.getChat();
|
||||
const chat = context.services.config?.getGeminiClient().getChat();
|
||||
if (!chat) {
|
||||
return {
|
||||
type: 'message',
|
||||
|
||||
@@ -25,7 +25,7 @@ export const clearCommand: SlashCommand = {
|
||||
const config = context.services.config;
|
||||
const chatRecordingService = context.services.config
|
||||
?.getGeminiClient()
|
||||
?.getChat()
|
||||
.getChat()
|
||||
.getChatRecordingService();
|
||||
|
||||
// Fire SessionEnd hook before clearing
|
||||
|
||||
@@ -43,7 +43,7 @@ export const compressCommand: SlashCommand = {
|
||||
const promptId = `compress-${Date.now()}`;
|
||||
const compressed = await context.services.config
|
||||
?.getGeminiClient()
|
||||
?.tryCompressChat(promptId, true);
|
||||
.tryCompressChat(promptId, true);
|
||||
if (compressed) {
|
||||
ui.addItem(
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ export const copyCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
|
||||
const chat = context.services.config?.getGeminiClient()?.getChat();
|
||||
const chat = context.services.config?.getGeminiClient().getChat();
|
||||
const history = chat?.getHistory();
|
||||
|
||||
// Get the last message from the AI (model role)
|
||||
|
||||
@@ -236,7 +236,7 @@ async function restartAction(
|
||||
if (failures.length < extensionsToRestart.length) {
|
||||
try {
|
||||
await context.services.config?.reloadSkills();
|
||||
await context.services.config?.getAgentRegistry()?.reload();
|
||||
await context.services.config?.getAgentRegistry().reload();
|
||||
} catch (error) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
@@ -271,7 +271,7 @@ async function exploreAction(
|
||||
context: CommandContext,
|
||||
): Promise<SlashCommandActionReturn | void> {
|
||||
const settings = context.services.settings.merged;
|
||||
const useRegistryUI = settings.experimental?.extensionRegistry;
|
||||
const useRegistryUI = settings.experimental.extensionRegistry;
|
||||
|
||||
if (useRegistryUI) {
|
||||
const extensionManager = context.services.config?.getExtensionLoader();
|
||||
|
||||
@@ -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} />,
|
||||
}),
|
||||
};
|
||||
@@ -45,7 +45,7 @@ function getIdeStatusMessage(ideClient: IdeClient): {
|
||||
};
|
||||
default: {
|
||||
let content = `🔴 Disconnected`;
|
||||
if (connection?.details) {
|
||||
if (connection.details) {
|
||||
content += `: ${connection.details}`;
|
||||
}
|
||||
return {
|
||||
@@ -107,7 +107,7 @@ async function getIdeStatusMessageWithFiles(ideClient: IdeClient): Promise<{
|
||||
};
|
||||
default: {
|
||||
let content = `🔴 Disconnected`;
|
||||
if (connection?.details) {
|
||||
if (connection.details) {
|
||||
content += `: ${connection.details}`;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -139,7 +139,7 @@ const authCommand: SlashCommand = {
|
||||
}
|
||||
// Update the client with the new tools
|
||||
const geminiClient = config.getGeminiClient();
|
||||
if (geminiClient?.isInitialized()) {
|
||||
if (geminiClient.isInitialized()) {
|
||||
await geminiClient.setTools();
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ const refreshCommand: SlashCommand = {
|
||||
|
||||
// Update the client with the new tools
|
||||
const geminiClient = config.getGeminiClient();
|
||||
if (geminiClient?.isInitialized()) {
|
||||
if (geminiClient.isInitialized()) {
|
||||
await geminiClient.setTools();
|
||||
}
|
||||
|
||||
@@ -419,9 +419,9 @@ async function handleEnableDisable(
|
||||
if (enable) {
|
||||
const settings = loadSettings();
|
||||
const result = await canLoadServer(name, {
|
||||
adminMcpEnabled: settings.merged.admin?.mcp?.enabled ?? true,
|
||||
allowedList: settings.merged.mcp?.allowed,
|
||||
excludedList: settings.merged.mcp?.excluded,
|
||||
adminMcpEnabled: settings.merged.admin.mcp.enabled ?? true,
|
||||
allowedList: settings.merged.mcp.allowed,
|
||||
excludedList: settings.merged.mcp.excluded,
|
||||
});
|
||||
if (
|
||||
!result.allowed &&
|
||||
@@ -465,7 +465,7 @@ async function handleEnableDisable(
|
||||
);
|
||||
await mcpClientManager.restart();
|
||||
}
|
||||
if (config.getGeminiClient()?.isInitialized())
|
||||
if (config.getGeminiClient().isInitialized())
|
||||
await config.getGeminiClient().setTools();
|
||||
context.ui.reloadCommands();
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ async function restoreAction(
|
||||
} else if (action.type === 'load_history' && loadHistory) {
|
||||
loadHistory(action.history);
|
||||
if (action.clientHistory) {
|
||||
config?.getGeminiClient()?.setHistory(action.clientHistory);
|
||||
config?.getGeminiClient().setHistory(action.clientHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export const setupGithubCommand: SlashCommand = {
|
||||
}
|
||||
|
||||
// Get the latest release tag from GitHub
|
||||
const proxy = context?.services?.config?.getProxy();
|
||||
const proxy = context.services.config?.getProxy();
|
||||
const releaseTag = await getLatestGitHubRelease(proxy);
|
||||
const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||
{!['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
|
||||
@@ -161,25 +161,25 @@ function getFieldDefaultFromDefinition(
|
||||
return !definition.experimental; // Experimental agents default to disabled
|
||||
}
|
||||
if (field.key === 'model') {
|
||||
return definition.modelConfig?.model ?? 'inherit';
|
||||
return definition.modelConfig.model ?? 'inherit';
|
||||
}
|
||||
if (field.key === 'temperature') {
|
||||
return definition.modelConfig?.generateContentConfig?.temperature;
|
||||
return definition.modelConfig.generateContentConfig?.temperature;
|
||||
}
|
||||
if (field.key === 'topP') {
|
||||
return definition.modelConfig?.generateContentConfig?.topP;
|
||||
return definition.modelConfig.generateContentConfig?.topP;
|
||||
}
|
||||
if (field.key === 'topK') {
|
||||
return definition.modelConfig?.generateContentConfig?.topK;
|
||||
return definition.modelConfig.generateContentConfig?.topK;
|
||||
}
|
||||
if (field.key === 'maxOutputTokens') {
|
||||
return definition.modelConfig?.generateContentConfig?.maxOutputTokens;
|
||||
return definition.modelConfig.generateContentConfig?.maxOutputTokens;
|
||||
}
|
||||
if (field.key === 'maxTimeMinutes') {
|
||||
return definition.runConfig?.maxTimeMinutes;
|
||||
return definition.runConfig.maxTimeMinutes;
|
||||
}
|
||||
if (field.key === 'maxTurns') {
|
||||
return definition.runConfig?.maxTurns;
|
||||
return definition.runConfig.maxTurns;
|
||||
}
|
||||
|
||||
return field.defaultValue;
|
||||
|
||||
@@ -1083,7 +1083,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
||||
const currentQuestion = questions[currentQuestionIndex];
|
||||
|
||||
const effectiveQuestion = useMemo(() => {
|
||||
if (currentQuestion?.type === 'yesno') {
|
||||
if (currentQuestion.type === 'yesno') {
|
||||
return {
|
||||
...currentQuestion,
|
||||
options: [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user