diff --git a/.gemini/skills/docs-writer/SKILL.md b/.gemini/skills/docs-writer/SKILL.md index 01cb380f7c..d7cf7b81be 100644 --- a/.gemini/skills/docs-writer/SKILL.md +++ b/.gemini/skills/docs-writer/SKILL.md @@ -118,6 +118,8 @@ documentation. reflects existing code. - **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when adding new sections to existing pages. +- **Headers**: If you change a header, you must check for links that lead to + that header and update them. - **Tone:** Ensure the tone is active and engaging. Use "you" and contractions. - **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase sentences to make them easier for users to understand. @@ -133,7 +135,8 @@ and that all links are functional. technical behavior. 2. **Self-review:** Re-read changes for formatting, correctness, and flow. 3. **Link check:** Verify all new and existing links leading to or from modified - pages. + pages. If you changed a header, ensure that any links that lead to it are + updated. 4. **Format:** Once all changes are complete, ask to execute `npm run format` to ensure consistent formatting across the project. If the user confirms, execute the command. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 201d46a66d..0da8dd1a0b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,4 +14,9 @@ # 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 \ No newline at end of file +/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 diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml index 8f062205cb..70a413f13a 100644 --- a/.github/actions/publish-release/action.yml +++ b/.github/actions/publish-release/action.yml @@ -192,6 +192,13 @@ 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' diff --git a/.github/actions/push-sandbox/action.yml b/.github/actions/push-sandbox/action.yml index e2d1ac942c..bab85af453 100644 --- a/.github/actions/push-sandbox/action.yml +++ b/.github/actions/push-sandbox/action.yml @@ -44,6 +44,8 @@ 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' @@ -69,16 +71,19 @@ 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' @@ -92,10 +97,14 @@ runs: - name: 'publish' shell: 'bash' if: "${{ inputs.dry-run != 'true' }}" - run: |- - docker push "${STEPS_DOCKER_BUILD_OUTPUTS_URI}" env: - STEPS_DOCKER_BUILD_OUTPUTS_URI: '${{ steps.docker_build.outputs.uri }}' + 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}" - name: 'Create issue on failure' if: |- ${{ failure() }} diff --git a/.github/workflows/chained_e2e.yml b/.github/workflows/chained_e2e.yml index 3633c5027b..8d714b34b0 100644 --- a/.github/workflows/chained_e2e.yml +++ b/.github/workflows/chained_e2e.yml @@ -264,6 +264,27 @@ jobs: run: 'npm run build' shell: 'pwsh' + - name: 'Ensure Chrome is available' + shell: 'pwsh' + run: | + $chromePaths = @( + "${env:ProgramFiles}\Google\Chrome\Application\chrome.exe", + "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe" + ) + $chromeExists = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $chromeExists) { + Write-Host 'Chrome not found, installing via Chocolatey...' + choco install googlechrome -y --no-progress --ignore-checksums + } + $installed = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($installed) { + Write-Host "Chrome found at: $installed" + & $installed --version + } else { + Write-Error 'Chrome installation failed' + exit 1 + } + - name: 'Run E2E tests' env: GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' diff --git a/.github/workflows/gemini-automated-issue-triage.yml b/.github/workflows/gemini-automated-issue-triage.yml index fe4c52292a..1cab2abaa9 100644 --- a/.github/workflows/gemini-automated-issue-triage.yml +++ b/.github/workflows/gemini-automated-issue-triage.yml @@ -121,6 +121,7 @@ jobs: 'area/security', 'area/platform', 'area/extensions', + 'area/documentation', 'area/unknown' ]; const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name)); @@ -255,6 +256,14 @@ jobs: "Issues with a specific extension." "Feature request for the extension ecosystem." + area/documentation + - Description: Issues related to user-facing documentation and other content on the documentation website. + - Example Issues: + "A typo in a README file." + "DOCS: A command is not working as described in the documentation." + "A request for a new documentation page." + "Instructions missing for skills feature" + area/unknown - Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate. diff --git a/.github/workflows/gemini-scheduled-issue-triage.yml b/.github/workflows/gemini-scheduled-issue-triage.yml index 25b0cdf4ec..50dd56883e 100644 --- a/.github/workflows/gemini-scheduled-issue-triage.yml +++ b/.github/workflows/gemini-scheduled-issue-triage.yml @@ -63,7 +63,7 @@ jobs: echo '🔍 Finding issues missing area labels...' NO_AREA_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ - --search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/unknown' --limit 100 --json number,title,body)" + --search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 100 --json number,title,body)" echo '🔍 Finding issues missing kind labels...' NO_KIND_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ @@ -204,6 +204,7 @@ jobs: Categorization Guidelines (Area): area/agent: Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality area/core: User Interface, OS Support, Core Functionality + area/documentation: End-user and contributor-facing documentation, website-related area/enterprise: Telemetry, Policy, Quota / Licensing area/extensions: Gemini CLI extensions capability area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation diff --git a/.github/workflows/unassign-inactive-assignees.yml b/.github/workflows/unassign-inactive-assignees.yml new file mode 100644 index 0000000000..dd09f0feaf --- /dev/null +++ b/.github/workflows/unassign-inactive-assignees.yml @@ -0,0 +1,315 @@ +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}`); diff --git a/.vscode/settings.json b/.vscode/settings.json index 3661ecf9c2..3197edbbfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,9 @@ "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, diff --git a/README.md b/README.md index 02dd4988f0..46aa6604c2 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ gemini ### Tools & Extensions -- [**Built-in Tools Overview**](./docs/tools/index.md) +- [**Built-in Tools Overview**](./docs/reference/tools.md) - [File System Operations](./docs/tools/file-system.md) - [Shell Commands](./docs/tools/shell.md) - [Web Fetch & Search](./docs/tools/web-fetch.md) @@ -323,8 +323,7 @@ gemini - [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a corporate environment. - [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking. -- [**Tools API Development**](./docs/reference/tools-api.md) - Create custom - tools. +- [**Tools reference**](./docs/reference/tools.md) - Built-in tools overview. - [**Local development**](./docs/local-development.md) - Local development tooling. diff --git a/docs/changelogs/index.md b/docs/changelogs/index.md index 537e9d1aee..33c179072a 100644 --- a/docs/changelogs/index.md +++ b/docs/changelogs/index.md @@ -18,6 +18,30 @@ 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 diff --git a/docs/changelogs/latest.md b/docs/changelogs/latest.md index 760e070bd9..d5d13717c7 100644 --- a/docs/changelogs/latest.md +++ b/docs/changelogs/latest.md @@ -1,6 +1,6 @@ -# Latest stable release: v0.31.0 +# Latest stable release: v0.32.1 -Released: February 27, 2026 +Released: March 4, 2026 For most users, our latest stable release is the recommended release. Install the latest stable version with: @@ -11,405 +11,198 @@ npm install -g @google/gemini-cli ## Highlights -- **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. +- **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. ## What's Changed -- 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 +- 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 @SandyTao520 in - [#19369](https://github.com/google-gemini/gemini-cli/pull/19369) -- chore(deps): bump tar from 7.5.7 to 7.5.8 by @.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 + [#20328](https://github.com/google-gemini/gemini-cli/pull/20328) +- feat(plan): support opening and modifying plan in external editor by @Adib234 + in [#20348](https://github.com/google-gemini/gemini-cli/pull/20348) +- feat(cli): implement interactive shell autocompletion by @mrpmohiburrahman in + [#20082](https://github.com/google-gemini/gemini-cli/pull/20082) +- fix(core): allow /memory add to work in plan mode by @Jefftree in + [#20353](https://github.com/google-gemini/gemini-cli/pull/20353) +- feat(core): add HTTP 499 to retryable errors and map to RetryableQuotaError by + @bdmorgan in [#20432](https://github.com/google-gemini/gemini-cli/pull/20432) +- feat(core): Enable generalist agent by @joshualitt in + [#19665](https://github.com/google-gemini/gemini-cli/pull/19665) +- Updated tests in TableRenderer.test.tsx to use SVG snapshots by @devr0306 in + [#20450](https://github.com/google-gemini/gemini-cli/pull/20450) +- Refactor Github Action per b/485167538 by @google-admin in + [#19443](https://github.com/google-gemini/gemini-cli/pull/19443) +- fix(github): resolve actionlint and yamllint regressions from #19443 by @jerop + in [#20467](https://github.com/google-gemini/gemini-cli/pull/20467) +- fix: action var usage by @galz10 in + [#20492](https://github.com/google-gemini/gemini-cli/pull/20492) +- feat(core): improve A2A content extraction by @adamfweidman in + [#20487](https://github.com/google-gemini/gemini-cli/pull/20487) +- fix(cli): support quota error fallbacks for all authentication types by + @sehoon38 in [#20475](https://github.com/google-gemini/gemini-cli/pull/20475) +- fix(core): flush transcript for pure tool-call responses to ensure BeforeTool + hooks see complete state by @krishdef7 in + [#20419](https://github.com/google-gemini/gemini-cli/pull/20419) +- feat(plan): adapt planning workflow based on complexity of task by @jerop in + [#20465](https://github.com/google-gemini/gemini-cli/pull/20465) +- fix: prevent orphaned processes from consuming 100% CPU when terminal closes + by @yuvrajangadsingh in + [#16965](https://github.com/google-gemini/gemini-cli/pull/16965) +- feat(core): increase fetch timeout and fix [object Object] error + stringification by @bdmorgan in + [#20441](https://github.com/google-gemini/gemini-cli/pull/20441) +- [Gemma x Gemini CLI] Add an Experimental Gemma Router that uses a LiteRT-LM + shim into the Composite Model Classifier Strategy by @sidwan02 in + [#17231](https://github.com/google-gemini/gemini-cli/pull/17231) +- docs(plan): update documentation regarding supporting editing of plan files + during plan approval by @Adib234 in + [#20452](https://github.com/google-gemini/gemini-cli/pull/20452) +- test(cli): fix flaky ToolResultDisplay overflow test by @jwhelangoog in + [#20518](https://github.com/google-gemini/gemini-cli/pull/20518) +- ui(cli): reduce length of Ctrl+O hint by @jwhelangoog in + [#20490](https://github.com/google-gemini/gemini-cli/pull/20490) +- fix(ui): correct styled table width calculations by @devr0306 in + [#20042](https://github.com/google-gemini/gemini-cli/pull/20042) +- Avoid overaggressive unescaping by @scidomino in + [#20520](https://github.com/google-gemini/gemini-cli/pull/20520) +- feat(telemetry) Instrument traces with more attributes and make them available + to OTEL users by @heaventourist in + [#20237](https://github.com/google-gemini/gemini-cli/pull/20237) +- Add support for policy engine in extensions by @chrstnb in + [#20049](https://github.com/google-gemini/gemini-cli/pull/20049) +- Docs: Update to Terms of Service & FAQ by @jkcinouye in + [#20488](https://github.com/google-gemini/gemini-cli/pull/20488) +- Fix bottom border rendering for search and add a regression test. by @jacob314 + in [#20517](https://github.com/google-gemini/gemini-cli/pull/20517) +- fix(core): apply retry logic to CodeAssistServer for all users by @bdmorgan in + [#20507](https://github.com/google-gemini/gemini-cli/pull/20507) +- Fix extension MCP server env var loading by @chrstnb in + [#20374](https://github.com/google-gemini/gemini-cli/pull/20374) +- feat(ui): add 'ctrl+o' hint to truncated content message by @jerop in + [#20529](https://github.com/google-gemini/gemini-cli/pull/20529) +- Fix flicker showing message to press ctrl-O again to collapse. by @jacob314 in + [#20414](https://github.com/google-gemini/gemini-cli/pull/20414) +- fix(cli): hide shortcuts hint while model is thinking or the user has typed a + prompt + add debounce to avoid flicker by @jacob314 in + [#19389](https://github.com/google-gemini/gemini-cli/pull/19389) +- feat(plan): update planning workflow to encourage multi-select with + descriptions of options by @Adib234 in + [#20491](https://github.com/google-gemini/gemini-cli/pull/20491) +- refactor(core,cli): useAlternateBuffer read from config by @psinha40898 in + [#20346](https://github.com/google-gemini/gemini-cli/pull/20346) +- fix(cli): ensure dialogs stay scrolled to bottom in alternate buffer mode by + @jacob314 in [#20527](https://github.com/google-gemini/gemini-cli/pull/20527) +- fix(core): revert auto-save of policies to user space by @Abhijit-2592 in + [#20531](https://github.com/google-gemini/gemini-cli/pull/20531) +- Demote unreliable test. by @gundermanc in + [#20571](https://github.com/google-gemini/gemini-cli/pull/20571) +- fix(core): handle optional response fields from code assist API by @sehoon38 + in [#20345](https://github.com/google-gemini/gemini-cli/pull/20345) +- fix(cli): keep thought summary when loading phrases are off by @LyalinDotCom + in [#20497](https://github.com/google-gemini/gemini-cli/pull/20497) +- feat(cli): add temporary flag to disable workspace policies by @Abhijit-2592 + in [#20523](https://github.com/google-gemini/gemini-cli/pull/20523) +- Disable expensive and scheduled workflows on personal forks by @dewitt in + [#20449](https://github.com/google-gemini/gemini-cli/pull/20449) +- Moved markdown parsing logic to a separate util file by @devr0306 in + [#20526](https://github.com/google-gemini/gemini-cli/pull/20526) +- fix(plan): prevent agent from using ask_user for shell command confirmation by + @Adib234 in [#20504](https://github.com/google-gemini/gemini-cli/pull/20504) +- fix(core): disable retries for code assist streaming requests by @sehoon38 in + [#20561](https://github.com/google-gemini/gemini-cli/pull/20561) +- feat(billing): implement G1 AI credits overage flow with billing telemetry by + @gsquared94 in + [#18590](https://github.com/google-gemini/gemini-cli/pull/18590) +- feat: better error messages by @gsquared94 in + [#20577](https://github.com/google-gemini/gemini-cli/pull/20577) +- fix(ui): persist expansion in AskUser dialog when navigating options by @jerop + in [#20559](https://github.com/google-gemini/gemini-cli/pull/20559) +- fix(cli): prevent sub-agent tool calls from leaking into UI by @abhipatel12 in + [#20580](https://github.com/google-gemini/gemini-cli/pull/20580) +- fix(cli): Shell autocomplete polish by @jacob314 in + [#20411](https://github.com/google-gemini/gemini-cli/pull/20411) +- Changelog for v0.31.0-preview.1 by @gemini-cli-robot in + [#20590](https://github.com/google-gemini/gemini-cli/pull/20590) +- Add slash command for promoting behavioral evals to CI blocking by @gundermanc + in [#20575](https://github.com/google-gemini/gemini-cli/pull/20575) +- Changelog for v0.30.1 by @gemini-cli-robot in + [#20589](https://github.com/google-gemini/gemini-cli/pull/20589) +- Add low/full CLI error verbosity mode for cleaner UI by @LyalinDotCom in + [#20399](https://github.com/google-gemini/gemini-cli/pull/20399) +- Disable Gemini PR reviews on draft PRs. by @gundermanc in + [#20362](https://github.com/google-gemini/gemini-cli/pull/20362) +- Docs: FAQ update by @jkcinouye in + [#20585](https://github.com/google-gemini/gemini-cli/pull/20585) +- fix(core): reduce intrusive MCP errors and deduplicate diagnostics by @spencer426 in - [#19478](https://github.com/google-gemini/gemini-cli/pull/19478) -- Remove unused files and update index and sidebar. by @g-samroberts in - [#19479](https://github.com/google-gemini/gemini-cli/pull/19479) -- Migrate core render util to use xterm.js as part of the rendering loop. by - @jacob314 in [#19044](https://github.com/google-gemini/gemini-cli/pull/19044) -- Changelog for v0.30.0-preview.1 by @gemini-cli-robot in - [#19496](https://github.com/google-gemini/gemini-cli/pull/19496) -- build: replace deprecated built-in punycode with userland package by @jacob314 - in [#19502](https://github.com/google-gemini/gemini-cli/pull/19502) -- Speculative fixes to try to fix react error. by @jacob314 in - [#19508](https://github.com/google-gemini/gemini-cli/pull/19508) -- fix spacing by @jacob314 in - [#19494](https://github.com/google-gemini/gemini-cli/pull/19494) -- fix(core): ensure user rejections update tool outcome for telemetry by - @abhiasap in [#18982](https://github.com/google-gemini/gemini-cli/pull/18982) -- fix(acp): Initialize config (#18897) by @Mervap in - [#18898](https://github.com/google-gemini/gemini-cli/pull/18898) -- fix(core): add error logging for IDE fetch failures by @yuvrajangadsingh in - [#17981](https://github.com/google-gemini/gemini-cli/pull/17981) -- feat(acp): support set_mode interface (#18890) by @Mervap in - [#18891](https://github.com/google-gemini/gemini-cli/pull/18891) -- fix(core): robust workspace-based IDE connection discovery by @ehedlund in - [#18443](https://github.com/google-gemini/gemini-cli/pull/18443) -- Deflake windows tests. by @jacob314 in - [#19511](https://github.com/google-gemini/gemini-cli/pull/19511) -- Fix: Avoid tool confirmation timeout when no UI listeners are present by - @pdHaku0 in [#17955](https://github.com/google-gemini/gemini-cli/pull/17955) -- format md file by @scidomino in - [#19474](https://github.com/google-gemini/gemini-cli/pull/19474) -- feat(cli): add experimental.useOSC52Copy setting by @scidomino in - [#19488](https://github.com/google-gemini/gemini-cli/pull/19488) -- feat(cli): replace loading phrases boolean with enum setting by @LyalinDotCom - in [#19347](https://github.com/google-gemini/gemini-cli/pull/19347) -- Update skill to adjust for generated results. by @g-samroberts in - [#19500](https://github.com/google-gemini/gemini-cli/pull/19500) -- Fix message too large issue. by @gundermanc in - [#19499](https://github.com/google-gemini/gemini-cli/pull/19499) -- fix(core): prevent duplicate tool approval entries in auto-saved.toml by - @Abhijit-2592 in - [#19487](https://github.com/google-gemini/gemini-cli/pull/19487) -- fix(core): resolve crash in ClearcutLogger when os.cpus() is empty by @Adib234 - in [#19555](https://github.com/google-gemini/gemini-cli/pull/19555) -- chore(core): improve encapsulation and remove unused exports by @adamfweidman - in [#19556](https://github.com/google-gemini/gemini-cli/pull/19556) -- Revert "Add generic searchable list to back settings and extensions (… by - @chrstnb in [#19434](https://github.com/google-gemini/gemini-cli/pull/19434) -- fix(core): improve error type extraction for telemetry by @yunaseoul in - [#19565](https://github.com/google-gemini/gemini-cli/pull/19565) -- fix: remove extra padding in Composer by @jackwotherspoon in - [#19529](https://github.com/google-gemini/gemini-cli/pull/19529) -- feat(plan): support configuring custom plans storage directory by @jerop in - [#19577](https://github.com/google-gemini/gemini-cli/pull/19577) -- Migrate files to resource or references folder. by @g-samroberts in - [#19503](https://github.com/google-gemini/gemini-cli/pull/19503) -- feat(policy): implement project-level policy support by @Abhijit-2592 in - [#18682](https://github.com/google-gemini/gemini-cli/pull/18682) -- feat(core): Implement parallel FC for read only tools. by @joshualitt in - [#18791](https://github.com/google-gemini/gemini-cli/pull/18791) -- chore(skills): adds pr-address-comments skill to work on PR feedback by - @mbleigh in [#19576](https://github.com/google-gemini/gemini-cli/pull/19576) -- refactor(sdk): introduce session-based architecture by @mbleigh in - [#19180](https://github.com/google-gemini/gemini-cli/pull/19180) -- fix(ci): add fallback JSON extraction to issue triage workflow by @bdmorgan in - [#19593](https://github.com/google-gemini/gemini-cli/pull/19593) -- feat(core): refine Edit and WriteFile tool schemas for Gemini 3 by - @SandyTao520 in - [#19476](https://github.com/google-gemini/gemini-cli/pull/19476) -- Changelog for v0.30.0-preview.3 by @gemini-cli-robot in - [#19585](https://github.com/google-gemini/gemini-cli/pull/19585) -- fix(plan): exclude EnterPlanMode tool from YOLO mode by @Adib234 in - [#19570](https://github.com/google-gemini/gemini-cli/pull/19570) -- chore: resolve build warnings and update dependencies by @mattKorwel in - [#18880](https://github.com/google-gemini/gemini-cli/pull/18880) -- feat(ui): add source indicators to slash commands by @ehedlund in - [#18839](https://github.com/google-gemini/gemini-cli/pull/18839) -- docs: refine Plan Mode documentation structure and workflow by @jerop in - [#19644](https://github.com/google-gemini/gemini-cli/pull/19644) -- Docs: Update release information regarding Gemini 3.1 by @jkcinouye in - [#19568](https://github.com/google-gemini/gemini-cli/pull/19568) -- fix(security): rate limit web_fetch tool to mitigate DDoS via prompt injection - by @mattKorwel in - [#19567](https://github.com/google-gemini/gemini-cli/pull/19567) -- Add initial implementation of /extensions explore command by @chrstnb in - [#19029](https://github.com/google-gemini/gemini-cli/pull/19029) -- fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) by - @maximus12793 in - [#19038](https://github.com/google-gemini/gemini-cli/pull/19038) -- Search updates by @alisa-alisa in - [#19482](https://github.com/google-gemini/gemini-cli/pull/19482) -- feat(cli): add support for numpad SS3 sequences by @scidomino in - [#19659](https://github.com/google-gemini/gemini-cli/pull/19659) -- feat(cli): enhance folder trust with configuration discovery and security - warnings by @galz10 in - [#19492](https://github.com/google-gemini/gemini-cli/pull/19492) -- feat(ui): improve startup warnings UX with dismissal and show-count limits by - @spencer426 in - [#19584](https://github.com/google-gemini/gemini-cli/pull/19584) -- feat(a2a): Add API key authentication provider by @adamfweidman in - [#19548](https://github.com/google-gemini/gemini-cli/pull/19548) -- Send accepted/removed lines with ACCEPT_FILE telemetry. by @gundermanc in - [#19670](https://github.com/google-gemini/gemini-cli/pull/19670) -- feat(models): support Gemini 3.1 Pro Preview and fixes by @sehoon38 in - [#19676](https://github.com/google-gemini/gemini-cli/pull/19676) -- feat(plan): enforce read-only constraints in Plan Mode by @mattKorwel in - [#19433](https://github.com/google-gemini/gemini-cli/pull/19433) -- fix(cli): allow perfect match @scripts/test-windows-paths.js completions to - submit on Enter by @spencer426 in - [#19562](https://github.com/google-gemini/gemini-cli/pull/19562) -- fix(core): treat 503 Service Unavailable as retryable quota error by @sehoon38 - in [#19642](https://github.com/google-gemini/gemini-cli/pull/19642) -- Update sidebar.json for to allow top nav tabs. by @g-samroberts in - [#19595](https://github.com/google-gemini/gemini-cli/pull/19595) -- security: strip deceptive Unicode characters from terminal output by @ehedlund - in [#19026](https://github.com/google-gemini/gemini-cli/pull/19026) -- Fixes 'input.on' is not a function error in Gemini CLI by @gundermanc in - [#19691](https://github.com/google-gemini/gemini-cli/pull/19691) -- Revert "feat(ui): add source indicators to slash commands" by @ehedlund in - [#19695](https://github.com/google-gemini/gemini-cli/pull/19695) -- security: implement deceptive URL detection and disclosure in tool - confirmations by @ehedlund in - [#19288](https://github.com/google-gemini/gemini-cli/pull/19288) -- fix(core): restore auth consent in headless mode and add unit tests by - @ehedlund in [#19689](https://github.com/google-gemini/gemini-cli/pull/19689) -- Fix unsafe assertions in code_assist folder. by @gundermanc in - [#19706](https://github.com/google-gemini/gemini-cli/pull/19706) -- feat(cli): make JetBrains warning more specific by @jacob314 in - [#19687](https://github.com/google-gemini/gemini-cli/pull/19687) -- fix(cli): extensions dialog UX polish by @jacob314 in - [#19685](https://github.com/google-gemini/gemini-cli/pull/19685) -- fix(cli): use getDisplayString for manual model selection in dialog by - @sehoon38 in [#19726](https://github.com/google-gemini/gemini-cli/pull/19726) -- feat(policy): repurpose "Always Allow" persistence to workspace level by - @Abhijit-2592 in - [#19707](https://github.com/google-gemini/gemini-cli/pull/19707) -- fix(cli): re-enable CLI banner by @sehoon38 in - [#19741](https://github.com/google-gemini/gemini-cli/pull/19741) -- Disallow and suppress unsafe assignment by @gundermanc in - [#19736](https://github.com/google-gemini/gemini-cli/pull/19736) -- feat(core): migrate read_file to 1-based start_line/end_line parameters by - @adamfweidman in - [#19526](https://github.com/google-gemini/gemini-cli/pull/19526) -- feat(cli): improve CTRL+O experience for both standard and alternate screen - buffer (ASB) modes by @jwhelangoog in - [#19010](https://github.com/google-gemini/gemini-cli/pull/19010) -- Utilize pipelining of grep_search -> read_file to eliminate turns by - @gundermanc in - [#19574](https://github.com/google-gemini/gemini-cli/pull/19574) -- refactor(core): remove unsafe type assertions in error utils (Phase 1.1) by - @mattKorwel in - [#19750](https://github.com/google-gemini/gemini-cli/pull/19750) -- Disallow unsafe returns. by @gundermanc in - [#19767](https://github.com/google-gemini/gemini-cli/pull/19767) -- fix(cli): filter subagent sessions from resume history by @abhipatel12 in - [#19698](https://github.com/google-gemini/gemini-cli/pull/19698) -- chore(lint): fix lint errors seen when running npm run lint by @abhipatel12 in - [#19844](https://github.com/google-gemini/gemini-cli/pull/19844) -- feat(core): remove unnecessary login verbiage from Code Assist auth by - @NTaylorMullen in - [#19861](https://github.com/google-gemini/gemini-cli/pull/19861) -- fix(plan): time share by approval mode dashboard reporting negative time - shares by @Adib234 in - [#19847](https://github.com/google-gemini/gemini-cli/pull/19847) -- fix(core): allow any preview model in quota access check by @bdmorgan in - [#19867](https://github.com/google-gemini/gemini-cli/pull/19867) -- fix(core): prevent omission placeholder deletions in replace/write_file by - @nsalerni in [#19870](https://github.com/google-gemini/gemini-cli/pull/19870) -- fix(core): add uniqueness guard to edit tool by @Shivangisharma4 in - [#19890](https://github.com/google-gemini/gemini-cli/pull/19890) -- refactor(config): remove enablePromptCompletion from settings by @sehoon38 in - [#19974](https://github.com/google-gemini/gemini-cli/pull/19974) -- refactor(core): move session conversion logic to core by @abhipatel12 in - [#19972](https://github.com/google-gemini/gemini-cli/pull/19972) -- Fix: Persist manual model selection on restart #19864 by @Nixxx19 in - [#19891](https://github.com/google-gemini/gemini-cli/pull/19891) -- fix(core): increase default retry attempts and add quota error backoff by - @sehoon38 in [#19949](https://github.com/google-gemini/gemini-cli/pull/19949) -- feat(core): add policy chain support for Gemini 3.1 by @sehoon38 in - [#19991](https://github.com/google-gemini/gemini-cli/pull/19991) -- Updates command reference and /stats command. by @g-samroberts in - [#19794](https://github.com/google-gemini/gemini-cli/pull/19794) -- Fix for silent failures in non-interactive mode by @owenofbrien in - [#19905](https://github.com/google-gemini/gemini-cli/pull/19905) -- fix(plan): allow plan mode writes on Windows and fix prompt paths by @Adib234 - in [#19658](https://github.com/google-gemini/gemini-cli/pull/19658) -- fix(core): prevent OAuth server crash on unexpected requests by @reyyanxahmed - in [#19668](https://github.com/google-gemini/gemini-cli/pull/19668) -- feat: Map tool kinds to explicit ACP.ToolKind values and update test … by - @sripasg in [#19547](https://github.com/google-gemini/gemini-cli/pull/19547) -- chore: restrict gemini-automted-issue-triage to only allow echo by @galz10 in - [#20047](https://github.com/google-gemini/gemini-cli/pull/20047) -- Allow ask headers longer than 16 chars by @scidomino in - [#20041](https://github.com/google-gemini/gemini-cli/pull/20041) -- fix(core): prevent state corruption in McpClientManager during collis by @h30s - in [#19782](https://github.com/google-gemini/gemini-cli/pull/19782) -- fix(bundling): copy devtools package to bundle for runtime resolution by - @SandyTao520 in - [#19766](https://github.com/google-gemini/gemini-cli/pull/19766) -- feat(policy): Support MCP Server Wildcards in Policy Engine by @jerop in - [#20024](https://github.com/google-gemini/gemini-cli/pull/20024) -- docs(CONTRIBUTING): update React DevTools version to 6 by @mmgok in - [#20014](https://github.com/google-gemini/gemini-cli/pull/20014) -- feat(core): optimize tool descriptions and schemas for Gemini 3 by - @aishaneeshah in - [#19643](https://github.com/google-gemini/gemini-cli/pull/19643) -- feat(core): implement experimental direct web fetch by @mbleigh in - [#19557](https://github.com/google-gemini/gemini-cli/pull/19557) -- feat(core): replace expected_replacements with allow_multiple in replace tool - by @SandyTao520 in - [#20033](https://github.com/google-gemini/gemini-cli/pull/20033) -- fix(sandbox): harden image packaging integrity checks by @aviralgarg05 in - [#19552](https://github.com/google-gemini/gemini-cli/pull/19552) -- fix(core): allow environment variable expansion and explicit overrides for MCP - servers by @galz10 in - [#18837](https://github.com/google-gemini/gemini-cli/pull/18837) -- feat(policy): Implement Tool Annotation Matching in Policy Engine by @jerop in - [#20029](https://github.com/google-gemini/gemini-cli/pull/20029) -- fix(core): prevent utility calls from changing session active model by - @adamfweidman in - [#20035](https://github.com/google-gemini/gemini-cli/pull/20035) -- fix(cli): skip workspace policy loading when in home directory by - @Abhijit-2592 in - [#20054](https://github.com/google-gemini/gemini-cli/pull/20054) -- fix(scripts): Add Windows (win32/x64) support to lint.js by @ZafeerMahmood in - [#16193](https://github.com/google-gemini/gemini-cli/pull/16193) -- fix(a2a-server): Remove unsafe type assertions in agent by @Nixxx19 in - [#19723](https://github.com/google-gemini/gemini-cli/pull/19723) -- Fix: Handle corrupted token file gracefully when switching auth types (#19845) - by @Nixxx19 in - [#19850](https://github.com/google-gemini/gemini-cli/pull/19850) -- fix critical dep vulnerability by @scidomino in - [#20087](https://github.com/google-gemini/gemini-cli/pull/20087) -- Add new setting to configure maxRetries by @kevinjwang1 in - [#20064](https://github.com/google-gemini/gemini-cli/pull/20064) -- Stabilize tests. by @gundermanc in - [#20095](https://github.com/google-gemini/gemini-cli/pull/20095) -- make windows tests mandatory by @scidomino in - [#20096](https://github.com/google-gemini/gemini-cli/pull/20096) -- Add 3.1 pro preview to behavioral evals. by @gundermanc in - [#20088](https://github.com/google-gemini/gemini-cli/pull/20088) -- feat:PR-rate-limit by @JagjeevanAK in - [#19804](https://github.com/google-gemini/gemini-cli/pull/19804) -- feat(cli): allow expanding full details of MCP tool on approval by @y-okt in - [#19916](https://github.com/google-gemini/gemini-cli/pull/19916) -- feat(security): Introduce Conseca framework by @shrishabh in - [#13193](https://github.com/google-gemini/gemini-cli/pull/13193) -- fix(cli): Remove unsafe type assertions in activityLogger #19713 by @Nixxx19 - in [#19745](https://github.com/google-gemini/gemini-cli/pull/19745) -- feat: implement AfterTool tail tool calls by @googlestrobe in - [#18486](https://github.com/google-gemini/gemini-cli/pull/18486) -- ci(actions): fix PR rate limiter excluding maintainers by @scidomino in - [#20117](https://github.com/google-gemini/gemini-cli/pull/20117) -- Shortcuts: Move SectionHeader title below top line and refine styling by - @keithguerin in - [#18721](https://github.com/google-gemini/gemini-cli/pull/18721) -- refactor(ui): Update and simplify use of gray colors in themes by @keithguerin - in [#20141](https://github.com/google-gemini/gemini-cli/pull/20141) -- fix punycode2 by @jacob314 in - [#20154](https://github.com/google-gemini/gemini-cli/pull/20154) -- feat(ide): add GEMINI_CLI_IDE_PID env var to override IDE process detection by - @kiryltech in [#15842](https://github.com/google-gemini/gemini-cli/pull/15842) -- feat(policy): Propagate Tool Annotations for MCP Servers by @jerop in - [#20083](https://github.com/google-gemini/gemini-cli/pull/20083) -- fix(a2a-server): pass allowedTools settings to core Config by @reyyanxahmed in - [#19680](https://github.com/google-gemini/gemini-cli/pull/19680) -- feat(mcp): add progress bar, throttling, and input validation for MCP tool - progress by @jasmeetsb in - [#19772](https://github.com/google-gemini/gemini-cli/pull/19772) -- feat(policy): centralize plan mode tool visibility in policy engine by @jerop - in [#20178](https://github.com/google-gemini/gemini-cli/pull/20178) -- feat(browser): implement experimental browser agent by @gsquared94 in - [#19284](https://github.com/google-gemini/gemini-cli/pull/19284) -- feat(plan): summarize work after executing a plan by @jerop in - [#19432](https://github.com/google-gemini/gemini-cli/pull/19432) -- fix(core): create new McpClient on restart to apply updated config by @h30s in - [#20126](https://github.com/google-gemini/gemini-cli/pull/20126) -- Changelog for v0.30.0-preview.5 by @gemini-cli-robot in - [#20107](https://github.com/google-gemini/gemini-cli/pull/20107) -- Update packages. by @jacob314 in - [#20152](https://github.com/google-gemini/gemini-cli/pull/20152) -- Fix extension env dir loading issue by @chrstnb in - [#20198](https://github.com/google-gemini/gemini-cli/pull/20198) -- restrict /assign to help-wanted issues by @scidomino in - [#20207](https://github.com/google-gemini/gemini-cli/pull/20207) -- feat(plan): inject message when user manually exits Plan mode by @jerop in - [#20203](https://github.com/google-gemini/gemini-cli/pull/20203) -- feat(extensions): enforce folder trust for local extension install by @galz10 - in [#19703](https://github.com/google-gemini/gemini-cli/pull/19703) -- feat(hooks): adds support for RuntimeHook functions. by @mbleigh in - [#19598](https://github.com/google-gemini/gemini-cli/pull/19598) -- Docs: Update UI links. by @jkcinouye in - [#20224](https://github.com/google-gemini/gemini-cli/pull/20224) -- feat: prompt users to run /terminal-setup with yes/no by @ishaanxgupta in - [#16235](https://github.com/google-gemini/gemini-cli/pull/16235) -- fix: additional high vulnerabilities (minimatch, cross-spawn) by @adamfweidman - in [#20221](https://github.com/google-gemini/gemini-cli/pull/20221) -- feat(telemetry): Add context breakdown to API response event by @SandyTao520 - in [#19699](https://github.com/google-gemini/gemini-cli/pull/19699) -- Docs: Add nested sub-folders for related topics by @g-samroberts in - [#20235](https://github.com/google-gemini/gemini-cli/pull/20235) -- feat(plan): support automatic model switching for Plan Mode by @jerop in - [#20240](https://github.com/google-gemini/gemini-cli/pull/20240) -- 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) + [#20232](https://github.com/google-gemini/gemini-cli/pull/20232) +- docs: fix spelling typos in installation guide by @campox747 in + [#20579](https://github.com/google-gemini/gemini-cli/pull/20579) +- Promote stable tests to CI blocking. by @gundermanc in + [#20581](https://github.com/google-gemini/gemini-cli/pull/20581) +- feat(core): enable contiguous parallel admission for Kind.Agent tools by + @abhipatel12 in + [#20583](https://github.com/google-gemini/gemini-cli/pull/20583) +- Enforce import/no-duplicates as error by @Nixxx19 in + [#19797](https://github.com/google-gemini/gemini-cli/pull/19797) +- fix: merge duplicate imports in sdk and test-utils packages (1/4) by @Nixxx19 + in [#19777](https://github.com/google-gemini/gemini-cli/pull/19777) +- fix: merge duplicate imports in a2a-server package (2/4) by @Nixxx19 in + [#19781](https://github.com/google-gemini/gemini-cli/pull/19781) **Full Changelog**: -https://github.com/google-gemini/gemini-cli/compare/v0.30.1...v0.31.0 +https://github.com/google-gemini/gemini-cli/compare/v0.31.0...v0.32.1 diff --git a/docs/changelogs/preview.md b/docs/changelogs/preview.md index 853207db6f..3b4e10bae8 100644 --- a/docs/changelogs/preview.md +++ b/docs/changelogs/preview.md @@ -1,6 +1,6 @@ -# Preview release: v0.33.0-preview.0 +# Preview release: v0.33.0-preview.1 -Released: March 03, 2026 +Released: March 04, 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,154 +29,159 @@ npm install -g @google/gemini-cli@preview ## What's Changed -- Docs: Update model docs to remove Preview Features. by @jkcinouye in +- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch + version v0.33.0-preview.0 and create version 0.33.0-preview.1 by + @gemini-cli-robot in + [#21047](https://github.com/google-gemini/gemini-cli/pull/21047) + +* Docs: Update model docs to remove Preview Features. by @jkcinouye in [#20084](https://github.com/google-gemini/gemini-cli/pull/20084) -- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in +* 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.0 +https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.1 diff --git a/docs/cli/enterprise.md b/docs/cli/enterprise.md index 44d8ba9467..39c0f7c5c1 100644 --- a/docs/cli/enterprise.md +++ b/docs/cli/enterprise.md @@ -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 documentation](../tools/index.md). +see the [Tools reference](../reference/tools.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](../reference/tools-api.md), it is crucial -to understand how server configurations are managed to apply security policies +[Model-Context Protocol (MCP) servers](../tools/mcp-server.md), it is crucial to +understand how server configurations are managed to apply security policies effectively. ### How MCP server configurations are merged diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 91bfefc990..41f8ededcd 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -12,8 +12,7 @@ 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](https://github.com/google-gemini/gemini-cli/issues) on -> GitHub. +> - [Open an issue] on GitHub. > - Use the **/bug** command within Gemini CLI to file an issue. ## How to enable Plan Mode @@ -132,10 +131,10 @@ These are the only allowed tools: ### Custom planning with skills -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. +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. For example: @@ -252,10 +251,59 @@ 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 @@ -306,8 +354,8 @@ those files are not automatically deleted and must be managed manually. [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 +[`codebase_investigator`]: /docs/core/subagents.md#codebase-investigator +[`cli_help`]: /docs/core/subagents.md#cli-help-agent [subagents]: /docs/core/subagents.md [custom subagents]: /docs/core/subagents.md#creating-custom-subagents [policy engine]: /docs/reference/policy-engine.md @@ -317,7 +365,11 @@ those files are not automatically deleted and must be managed manually. [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]: /docs/reference/configuration.md#model-settings +[auto model]: /docs/reference/configuration.md#model [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 +[extensions]: /docs/extensions/ +[Conductor]: https://github.com/gemini-cli-extensions/conductor +[open an issue]: https://github.com/google-gemini/gemini-cli/issues +[Agent Skills]: /docs/cli/skills.md diff --git a/docs/cli/sandbox.md b/docs/cli/sandbox.md index 1d075989af..ec7e88f624 100644 --- a/docs/cli/sandbox.md +++ b/docs/cli/sandbox.md @@ -50,6 +50,74 @@ 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. gVisor / runsc (Linux only) + +Strongest isolation available: runs containers inside a user-space kernel via +[gVisor](https://github.com/google/gvisor). gVisor intercepts all container +system calls and handles them in a sandboxed kernel written in Go, providing a +strong security barrier between AI operations and the host OS. + +**Prerequisites:** + +- Linux (gVisor supports Linux only) +- Docker installed and running +- gVisor/runsc runtime configured + +When you set `sandbox: "runsc"`, Gemini CLI runs +`docker run --runtime=runsc ...` to execute containers with gVisor isolation. +runsc is not auto-detected; you must specify it explicitly (e.g. +`GEMINI_SANDBOX=runsc` or `sandbox: "runsc"`). + +To set up runsc: + +1. Install the runsc binary. +2. Configure the Docker daemon to use the runsc runtime. +3. Verify the installation. + +### 4. 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 @@ -88,7 +156,8 @@ 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` +2. **Environment variable**: + `GEMINI_SANDBOX=true|docker|podman|sandbox-exec|runsc|lxc` 3. **Settings file**: `"sandbox": true` in the `tools` object of your `settings.json` file (e.g., `{"tools": {"sandbox": true}}`). diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 37508fc04e..d2680d65ad 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -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 path in the footer. | `false` | +| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory 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` | diff --git a/docs/core/index.md b/docs/core/index.md index 53aa647dc2..adf186116f 100644 --- a/docs/core/index.md +++ b/docs/core/index.md @@ -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 API](../reference/tools-api.md):** Information on how tools are - defined, registered, and used by the core. +- **[Core tools reference](../reference/tools.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 diff --git a/docs/get-started/authentication.md b/docs/get-started/authentication.md index 61d4a5c040..8b8f592335 100644 --- a/docs/get-started/authentication.md +++ b/docs/get-started/authentication.md @@ -4,6 +4,10 @@ To use Gemini CLI, you'll need to authenticate with Google. This guide helps you quickly find the best way to sign in based on your account type and how you're using the CLI. +> **Note:** Looking for a high-level comparison of all available subscriptions? +> To compare features and find the right quota for your needs, see our +> [Plans page](/plans/). + For most users, we recommend starting Gemini CLI and logging in with your personal Google account. diff --git a/docs/get-started/gemini-3.md b/docs/get-started/gemini-3.md index a5eed9ab1d..bc83d990d5 100644 --- a/docs/get-started/gemini-3.md +++ b/docs/get-started/gemini-3.md @@ -39,6 +39,10 @@ When you encounter that limit, you’ll be given the option to switch to Gemini 2.5 Pro, upgrade for higher limits, or stop. You’ll also be told when your usage limit resets and Gemini 3 Pro can be used again. +> **Note:** Looking to upgrade for higher limits? To compare subscription +> options and find the right quota for your needs, see our +> [Plans page](/plans/). + Similarly, when you reach your daily usage limit for Gemini 2.5 Pro, you’ll see a message prompting fallback to Gemini 2.5 Flash. diff --git a/docs/hooks/reference.md b/docs/hooks/reference.md index 9b7226ac05..445035b1aa 100644 --- a/docs/hooks/reference.md +++ b/docs/hooks/reference.md @@ -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/tools) for a full list - of available tool names. + `run_shell_command`). See the [Tools Reference](/docs/reference/tools) for a + full list of available tool names. - **MCP Tools**: Tools from MCP servers follow the naming pattern `mcp____`. - **Regex Support**: Matchers support regular expressions (e.g., diff --git a/docs/index.md b/docs/index.md index 3ccaf3b797..af1915bb8f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 API](./reference/tools-api.md):** The API for defining and using - tools. +- **[Tools reference](./reference/tools.md):** Information on how tools are + defined, registered, and used. ## Resources diff --git a/docs/issue-and-pr-automation.md b/docs/issue-and-pr-automation.md index 27185de11c..6c023b651b 100644 --- a/docs/issue-and-pr-automation.md +++ b/docs/issue-and-pr-automation.md @@ -113,7 +113,45 @@ process. ensure every issue is eventually categorized, even if the initial triage fails. -### 5. Release automation +### 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 #` 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 This workflow handles the process of packaging and publishing new versions of the Gemini CLI. diff --git a/docs/redirects.json b/docs/redirects.json index 5183d0d476..598f42cccf 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -8,7 +8,8 @@ "/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-api", + "/docs/core/tools-api": "/docs/reference/tools", + "/docs/reference/tools-api": "/docs/reference/tools", "/docs/faq": "/docs/resources/faq", "/docs/get-started/configuration": "/docs/reference/configuration", "/docs/get-started/configuration-v1": "/docs/reference/configuration", diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 82ee987eb2..1f1299072b 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -250,8 +250,18 @@ 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 path in the footer. + - **Description:** Hide the current working directory in the footer. - **Default:** `false` - **`ui.footer.hideSandboxStatus`** (boolean): @@ -747,7 +757,8 @@ 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, or provide a string path to a sandbox profile. + or disable the sandbox, provide a string path to a sandbox profile, or + specify an explicit sandbox command (e.g., "docker", "podman", "lxc"). - **Default:** `undefined` - **Requires restart:** Yes diff --git a/docs/reference/tools-api.md b/docs/reference/tools-api.md deleted file mode 100644 index 91fae3f720..0000000000 --- a/docs/reference/tools-api.md +++ /dev/null @@ -1,131 +0,0 @@ -# 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. diff --git a/docs/reference/tools.md b/docs/reference/tools.md new file mode 100644 index 0000000000..e1a0958866 --- /dev/null +++ b/docs/reference/tools.md @@ -0,0 +1,106 @@ +# 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.

**Parameters:** `command`, `description`, `dir_path`, `is_background` | +| File System | [`glob`](../tools/file-system.md) | `Search` | Finds files matching specific glob patterns across the workspace.

**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`.

**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.

**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.

**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.

**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.

**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.

**Parameters:** `file_path`, `content` | +| Interaction | [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog.

**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.

**Parameters:** `todos` | +| Memory | [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise for specific tasks from the `.gemini/skills` directory.

**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.

**Parameters:** `path` | +| Memory | [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file to retain context.

**Parameters:** `fact` | +| Planning | [`enter_plan_mode`](../tools/planning.md) | `Plan` | Switches the CLI to a safe, read-only "Plan Mode" for researching complex changes.

**Parameters:** `reason` | +| Planning | [`exit_plan_mode`](../tools/planning.md) | `Plan` | Finalizes a plan, presents it for review, and requests approval to start implementation.

**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.

**Parameters:** `result` | +| Web | [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information.

**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.

**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. diff --git a/docs/resources/quota-and-pricing.md b/docs/resources/quota-and-pricing.md index d4ed22a1cb..62258bae09 100644 --- a/docs/resources/quota-and-pricing.md +++ b/docs/resources/quota-and-pricing.md @@ -1,14 +1,13 @@ # Gemini CLI: Quotas and pricing Gemini CLI offers a generous free tier that covers many individual developers' -use cases. For enterprise or professional usage, or if you need higher limits, +use cases. For enterprise or professional usage, or if you need increased quota, several options are available depending on your authentication account type. -See [privacy and terms](./tos-privacy.md) for details on the Privacy Policy and -Terms of Service. +For a high-level comparison of available subscriptions and to select the right +quota for your needs, see the [Plans page](/plans/). -> **Note:** Published prices are list price; additional negotiated commercial -> discounting may apply. +## Overview This article outlines the specific quotas and pricing applicable to Gemini CLI when using different authentication methods. @@ -23,10 +22,11 @@ Generally, there are three categories to choose from: ## Free usage -Your journey begins with a generous free tier, perfect for experimentation and -light use. +Access to Gemini CLI begins with a generous free tier, perfect for +experimentation and light use. -Your free usage limits depend on your authorization type. +Your free usage is governed by the following limits, which depend on your +authorization type. ### Log in with Google (Gemini Code Assist for individuals) @@ -78,14 +78,12 @@ Gemini CLI by upgrading to one of the following subscriptions: Learn more at [Gemini Code Assist Quotas and Limits](https://developers.google.com/gemini-code-assist/resources/quotas) -- [Purchase a Gemini Code Assist Subscription through Google Cloud ](https://cloud.google.com/gemini/docs/codeassist/overview) - by signing up in the Google Cloud console. Learn more at - [Set up Gemini Code Assist](https://cloud.google.com/gemini/docs/discover/set-up-gemini). +- [Purchase a Gemini Code Assist Subscription through Google Cloud](https://cloud.google.com/gemini/docs/codeassist/overview). Quotas and pricing are based on a fixed price subscription with assigned license seats. For predictable costs, you can sign in with Google. - This includes: + This includes the following request limits: - Gemini Code Assist Standard edition: - 1500 model requests / user / day - 120 model requests / user / minute @@ -106,18 +104,27 @@ recommended path for uninterrupted access. To do this, log in using a Gemini API key or Vertex AI. -- Vertex AI (Regular Mode): - - Quota: Governed by a dynamic shared quota system or pre-purchased - provisioned throughput. - - Cost: Based on model and token usage. +### Vertex AI (regular mode) + +An enterprise-grade platform for building, deploying, and managing AI models, +including Gemini. It offers enhanced security, data governance, and integration +with other Google Cloud services. + +- Quota: Governed by a dynamic shared quota system or pre-purchased provisioned + throughput. +- Cost: Based on model and token usage. Learn more at [Vertex AI Dynamic Shared Quota](https://cloud.google.com/vertex-ai/generative-ai/docs/resources/dynamic-shared-quota) and [Vertex AI Pricing](https://cloud.google.com/vertex-ai/pricing). -- Gemini API key: - - Quota: Varies by pricing tier. - - Cost: Varies by pricing tier and model/token usage. +### Gemini API key + +Ideal for developers who want to quickly build applications with the Gemini +models. This is the most direct way to use the models. + +- Quota: Varies by pricing tier. +- Cost: Varies by pricing tier and model/token usage. Learn more at [Gemini API Rate Limits](https://ai.google.dev/gemini-api/docs/rate-limits), @@ -125,7 +132,8 @@ Learn more at It’s important to highlight that when using an API key, you pay per token/call. This can be more expensive for many small calls with few tokens, but it's the -only way to ensure your workflow isn't interrupted by quota limits. +only way to ensure your workflow isn't interrupted by reaching a limit on your +quota. ## Gemini for workspace plans @@ -135,12 +143,12 @@ Flow video editor). These plans do not apply to the API usage which powers the Gemini CLI. Supporting these plans is under active consideration for future support. -## Check usage and quota +## Check usage and limits -You can check your current token usage and quota information using the +You can check your current token usage and applicable limits using the `/stats model` command. This command provides a snapshot of your current -session's token usage, as well as your overall quota and usage for the supported -models. +session's token usage, as well as information about the limits associated with +your current quota. For more information on the `/stats` command and its subcommands, see the [Command Reference](../../reference/commands.md#stats). @@ -149,17 +157,16 @@ A summary of model usage is also presented on exit at the end of a session. ## Tips to avoid high costs -When using a Pay as you Go API key, be mindful of your usage to avoid unexpected +When using a pay-as-you-go plan, be mindful of your usage to avoid unexpected costs. -- Don't blindly accept every suggestion, especially for computationally - intensive tasks like refactoring large codebases. -- Be intentional with your prompts and commands. You are paying per call, so - think about the most efficient way to get the job done. - -## Gemini API vs. Vertex - -- Gemini API (gemini developer api): This is the fastest way to use the Gemini - models directly. -- Vertex AI: This is the enterprise-grade platform for building, deploying, and - managing Gemini models with specific security and control requirements. +- **Be selective with suggestions**: Before accepting a suggestion, especially + for a computationally intensive task like refactoring a large codebase, + consider if it's the most cost-effective approach. +- **Use precise prompts**: You are paying per call, so think about the most + efficient way to get your desired result. A well-crafted prompt can often get + you the answer you need in a single call, rather than multiple back-and-forth + interactions. +- **Monitor your usage**: Use the `/stats model` command to track your token + usage during a session. This can help you stay aware of your spending in real + time. diff --git a/docs/sidebar.json b/docs/sidebar.json index 4d6d9df10a..000f571077 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -188,7 +188,7 @@ "slug": "docs/reference/memport" }, { "label": "Policy engine", "slug": "docs/reference/policy-engine" }, - { "label": "Tools API", "slug": "docs/reference/tools-api" } + { "label": "Tools reference", "slug": "docs/reference/tools" } ] } ] diff --git a/docs/tools/index.md b/docs/tools/index.md deleted file mode 100644 index 6bdf298fea..0000000000 --- a/docs/tools/index.md +++ /dev/null @@ -1,105 +0,0 @@ -# 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. diff --git a/integration-tests/acp-env-auth.test.ts b/integration-tests/acp-env-auth.test.ts index c83dbafce5..65f8adbf22 100644 --- a/integration-tests/acp-env-auth.test.ts +++ b/integration-tests/acp-env-auth.test.ts @@ -55,7 +55,7 @@ describe.skip('ACP Environment and Auth', () => { const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js'); - child = spawn('node', [bundlePath, '--experimental-acp'], { + child = spawn('node', [bundlePath, '--acp'], { cwd: rig.homeDir!, stdio: ['pipe', 'pipe', 'inherit'], env: { @@ -120,7 +120,7 @@ describe.skip('ACP Environment and Auth', () => { const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js'); - child = spawn('node', [bundlePath, '--experimental-acp'], { + child = spawn('node', [bundlePath, '--acp'], { cwd: rig.homeDir!, stdio: ['pipe', 'pipe', 'inherit'], env: { diff --git a/integration-tests/acp-telemetry.test.ts b/integration-tests/acp-telemetry.test.ts index 393156df3e..f883b977bf 100644 --- a/integration-tests/acp-telemetry.test.ts +++ b/integration-tests/acp-telemetry.test.ts @@ -58,7 +58,7 @@ describe('ACP telemetry', () => { 'node', [ bundlePath, - '--experimental-acp', + '--acp', '--fake-responses', join(rig.testDir!, 'fake-responses.json'), ], diff --git a/integration-tests/api-resilience.responses b/integration-tests/api-resilience.responses new file mode 100644 index 0000000000..d30d29906e --- /dev/null +++ b/integration-tests/api-resilience.responses @@ -0,0 +1 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Part 1. "}],"role":"model"},"index":0}]},{"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":10,"totalTokenCount":110}},{"candidates":[{"content":{"parts":[{"text":"Part 2."}],"role":"model"},"index":0}],"finishReason":"STOP"}]} diff --git a/integration-tests/api-resilience.test.ts b/integration-tests/api-resilience.test.ts new file mode 100644 index 0000000000..870adf701a --- /dev/null +++ b/integration-tests/api-resilience.test.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { TestRig } from './test-helper.js'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +describe('API Resilience E2E', () => { + let rig: TestRig; + + beforeEach(() => { + rig = new TestRig(); + }); + + afterEach(async () => { + await rig.cleanup(); + }); + + it('should not crash when receiving metadata-only chunks in a stream', async () => { + await rig.setup('api-resilience-metadata-only', { + fakeResponsesPath: join( + dirname(fileURLToPath(import.meta.url)), + 'api-resilience.responses', + ), + settings: { + planSettings: { modelRouting: false }, + }, + }); + + // Run the CLI with a simple prompt. + // The fake responses will provide a stream with a metadata-only chunk in the middle. + // We use gemini-3-pro-preview to minimize internal service calls. + const result = await rig.run({ + args: ['hi', '--model', 'gemini-3-pro-preview'], + }); + + // Verify the output contains text from the normal chunks. + // If the CLI crashed on the metadata chunk, rig.run would throw. + expect(result).toContain('Part 1.'); + expect(result).toContain('Part 2.'); + + // Verify telemetry event for the prompt was still generated + const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt'); + expect(hasUserPromptEvent).toBe(true); + }); +}); diff --git a/integration-tests/browser-agent.cleanup.responses b/integration-tests/browser-agent.cleanup.responses new file mode 100644 index 0000000000..988f2fa456 --- /dev/null +++ b/integration-tests/browser-agent.cleanup.responses @@ -0,0 +1,2 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll open https://example.com and check the page title for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and get the page title"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The page title of https://example.com is \"Example Domain\". The browser session has been completed and cleaned up successfully."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]} diff --git a/integration-tests/browser-agent.interaction.responses b/integration-tests/browser-agent.interaction.responses new file mode 100644 index 0000000000..98474d6b59 --- /dev/null +++ b/integration-tests/browser-agent.interaction.responses @@ -0,0 +1,2 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and analyze the links on the page."},{"functionCall":{"name":"browser_agent","args":{"task":"Go to https://example.com and find all links on the page, then describe them"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"After analyzing https://example.com, I found the following links:\n\n1. **\"More information...\"** - This is the main link on the page that points to the IANA (Internet Assigned Numbers Authority) website for more details about reserved domains.\n\nThe page is quite minimal with just this single informational link, which is typical for example domains used in documentation."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":70,"totalTokenCount":270}}]} diff --git a/integration-tests/browser-agent.navigate-snapshot.responses b/integration-tests/browser-agent.navigate-snapshot.responses new file mode 100644 index 0000000000..481520234d --- /dev/null +++ b/integration-tests/browser-agent.navigate-snapshot.responses @@ -0,0 +1,2 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you open https://example.com and analyze the page. Let me use the browser agent to navigate and capture the page information."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and capture the accessibility tree to get the page title and main content"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Based on the browser analysis of https://example.com:\n\n**Page Title**: Example Domain\n\n**Main Content**: The page contains a simple heading \"Example Domain\" and explanatory text stating \"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\"\n\nThe page has a clean, minimal layout typical of placeholder domains used for documentation and examples."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]} diff --git a/integration-tests/browser-agent.screenshot.responses b/integration-tests/browser-agent.screenshot.responses new file mode 100644 index 0000000000..c9abaa4268 --- /dev/null +++ b/integration-tests/browser-agent.screenshot.responses @@ -0,0 +1,2 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and take a screenshot for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully navigated to https://example.com and captured a screenshot. The page shows the familiar \"Example Domain\" header with explanatory text below it. The screenshot captures the clean, minimal layout of this demonstration website."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]} diff --git a/integration-tests/browser-agent.sequential.responses b/integration-tests/browser-agent.sequential.responses new file mode 100644 index 0000000000..74732998aa --- /dev/null +++ b/integration-tests/browser-agent.sequential.responses @@ -0,0 +1,2 @@ +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you navigate to https://example.com and perform both operations - capturing the accessibility tree and taking a screenshot."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":45,"totalTokenCount":145}}]} +{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully completed both operations on https://example.com:\n\n**Accessibility Tree Snapshot**: The page has a clear structure with the main heading \"Example Domain\" and descriptive text about the domain's purpose for documentation examples.\n\n**Screenshot**: Captured a visual representation of the page showing the clean, minimal layout with the heading and explanatory text.\n\nBoth the accessibility data and visual screenshot confirm this is the standard example domain page used for documentation purposes."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]} diff --git a/integration-tests/browser-agent.test.ts b/integration-tests/browser-agent.test.ts new file mode 100644 index 0000000000..0fdb3e717b --- /dev/null +++ b/integration-tests/browser-agent.test.ts @@ -0,0 +1,206 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Integration tests for the browser agent. + * + * These tests verify the complete end-to-end flow from CLI prompt through + * browser_agent delegation to MCP/Chrome DevTools and back. Unlike the unit + * tests in packages/core/src/agents/browser/ which mock all MCP components, + * these tests launch real Chrome instances in headless mode. + * + * Tests are skipped on systems without Chrome/Chromium installed. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { TestRig, assertModelHasOutput } from './test-helper.js'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const chromeAvailable = (() => { + try { + if (process.platform === 'darwin') { + execSync( + 'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"', + { + stdio: 'ignore', + }, + ); + } else if (process.platform === 'linux') { + execSync( + 'which google-chrome || which chromium-browser || which chromium', + { stdio: 'ignore' }, + ); + } else if (process.platform === 'win32') { + // Check standard Windows installation paths using Node.js fs + const chromePaths = [ + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', + `${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`, + ]; + const found = chromePaths.some((p) => existsSync(p)); + if (!found) { + // Fall back to PATH check + execSync('where chrome || where chromium', { stdio: 'ignore' }); + } + } else { + return false; + } + return true; + } catch { + return false; + } +})(); + +describe.skipIf(!chromeAvailable)('browser-agent', () => { + let rig: TestRig; + + beforeEach(() => { + rig = new TestRig(); + }); + + afterEach(async () => await rig.cleanup()); + + it('should navigate to a page and capture accessibility tree', async () => { + rig.setup('browser-navigate-and-snapshot', { + fakeResponsesPath: join( + __dirname, + 'browser-agent.navigate-snapshot.responses', + ), + settings: { + agents: { + browser_agent: { + headless: true, + sessionMode: 'isolated', + }, + }, + }, + }); + + const result = await rig.run({ + args: 'Open https://example.com in the browser and tell me the page title and main content.', + }); + + assertModelHasOutput(result); + + const toolLogs = rig.readToolLogs(); + const browserAgentCall = toolLogs.find( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect( + browserAgentCall, + 'Expected browser_agent to be called', + ).toBeDefined(); + }); + + it('should take screenshots of web pages', async () => { + rig.setup('browser-screenshot', { + fakeResponsesPath: join(__dirname, 'browser-agent.screenshot.responses'), + settings: { + agents: { + browser_agent: { + headless: true, + sessionMode: 'isolated', + }, + }, + }, + }); + + const result = await rig.run({ + args: 'Navigate to https://example.com and take a screenshot.', + }); + + const toolLogs = rig.readToolLogs(); + const browserCalls = toolLogs.filter( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect(browserCalls.length).toBeGreaterThan(0); + + assertModelHasOutput(result); + }); + + it('should interact with page elements', async () => { + rig.setup('browser-interaction', { + fakeResponsesPath: join(__dirname, 'browser-agent.interaction.responses'), + settings: { + agents: { + browser_agent: { + headless: true, + sessionMode: 'isolated', + }, + }, + }, + }); + + const result = await rig.run({ + args: 'Go to https://example.com, find any links on the page, and describe them.', + }); + + const toolLogs = rig.readToolLogs(); + const browserAgentCall = toolLogs.find( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect( + browserAgentCall, + 'Expected browser_agent to be called', + ).toBeDefined(); + + assertModelHasOutput(result); + }); + + it('should clean up browser processes after completion', async () => { + rig.setup('browser-cleanup', { + fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'), + settings: { + agents: { + browser_agent: { + headless: true, + sessionMode: 'isolated', + }, + }, + }, + }); + + await rig.run({ + args: 'Open https://example.com in the browser and check the page title.', + }); + + // Test passes if we reach here, relying on Vitest's timeout mechanism + // to detect hanging browser processes. + }); + + it('should handle multiple browser operations in sequence', async () => { + rig.setup('browser-sequential', { + fakeResponsesPath: join(__dirname, 'browser-agent.sequential.responses'), + settings: { + agents: { + browser_agent: { + headless: true, + sessionMode: 'isolated', + }, + }, + }, + }); + + const result = await rig.run({ + args: 'Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot.', + }); + + const toolLogs = rig.readToolLogs(); + const browserCalls = toolLogs.filter( + (t) => t.toolRequest.name === 'browser_agent', + ); + expect(browserCalls.length).toBeGreaterThan(0); + + // Should successfully complete all operations + assertModelHasOutput(result); + }); +}); diff --git a/integration-tests/policy-headless-readonly.responses b/integration-tests/policy-headless-readonly.responses new file mode 100644 index 0000000000..35ba546bae --- /dev/null +++ b/integration-tests/policy-headless-readonly.responses @@ -0,0 +1,2 @@ +{"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}}]} diff --git a/integration-tests/policy-headless-shell-allowed.responses b/integration-tests/policy-headless-shell-allowed.responses new file mode 100644 index 0000000000..7c98e60db0 --- /dev/null +++ b/integration-tests/policy-headless-shell-allowed.responses @@ -0,0 +1,2 @@ +{"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}]}}]} diff --git a/integration-tests/policy-headless-shell-denied.responses b/integration-tests/policy-headless-shell-denied.responses new file mode 100644 index 0000000000..4278543b7e --- /dev/null +++ b/integration-tests/policy-headless-shell-denied.responses @@ -0,0 +1,2 @@ +{"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}}]} diff --git a/integration-tests/policy-headless.test.ts b/integration-tests/policy-headless.test.ts new file mode 100644 index 0000000000..1e3286e1ae --- /dev/null +++ b/integration-tests/policy-headless.test.ts @@ -0,0 +1,192 @@ +/** + * @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, + ); +}); diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index c969e601c3..fe15aed37b 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -28,6 +28,9 @@ import { type Config, type UserTierId, type ToolLiveOutput, + type AnsiLine, + type AnsiOutput, + type AnsiToken, isSubagentProgress, EDIT_TOOL_NAMES, processRestorableToolCalls, @@ -344,10 +347,15 @@ export class Task { outputAsText = outputChunk; } else if (isSubagentProgress(outputChunk)) { outputAsText = JSON.stringify(outputChunk); - } else { - outputAsText = outputChunk - .map((line) => line.map((token) => token.text).join('')) + } 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); } logger.info( diff --git a/packages/a2a-server/src/config/config.test.ts b/packages/a2a-server/src/config/config.test.ts index c676e46289..ee63df36f7 100644 --- a/packages/a2a-server/src/config/config.test.ts +++ b/packages/a2a-server/src/config/config.test.ts @@ -16,6 +16,9 @@ import { ExperimentFlags, fetchAdminControlsOnce, type FetchAdminControlsResponse, + AuthType, + isHeadlessMode, + FatalAuthenticationError, } from '@google/gemini-cli-core'; // Mock dependencies @@ -50,6 +53,7 @@ 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(), @@ -62,6 +66,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('../utils/logger.js', () => ({ logger: { info: vi.fn(), + warn: vi.fn(), error: vi.fn(), }, })); @@ -73,12 +78,11 @@ describe('loadConfig', () => { beforeEach(() => { vi.clearAllMocks(); - process.env['GEMINI_API_KEY'] = 'test-key'; + vi.stubEnv('GEMINI_API_KEY', 'test-key'); }); afterEach(() => { - delete process.env['CUSTOM_IGNORE_FILE_PATHS']; - delete process.env['GEMINI_API_KEY']; + vi.unstubAllEnvs(); }); describe('admin settings overrides', () => { @@ -199,7 +203,7 @@ describe('loadConfig', () => { it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => { const testPath = '/tmp/ignore'; - process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath; + vi.stubEnv('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([ @@ -224,7 +228,7 @@ describe('loadConfig', () => { it('should merge customIgnoreFilePaths from settings and env var', async () => { const envPath = '/env/ignore'; const settingsPath = '/settings/ignore'; - process.env['CUSTOM_IGNORE_FILE_PATHS'] = envPath; + vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', envPath); const settings: Settings = { fileFiltering: { customIgnoreFilePaths: [settingsPath], @@ -240,7 +244,7 @@ describe('loadConfig', () => { it('should split CUSTOM_IGNORE_FILE_PATHS using system delimiter', async () => { const paths = ['/path/one', '/path/two']; - process.env['CUSTOM_IGNORE_FILE_PATHS'] = paths.join(path.delimiter); + vi.stubEnv('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); @@ -254,7 +258,7 @@ describe('loadConfig', () => { it('should initialize FileDiscoveryService with correct options', async () => { const testPath = '/tmp/ignore'; - process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath; + vi.stubEnv('CUSTOM_IGNORE_FILE_PATHS', testPath); const settings: Settings = { fileFiltering: { respectGitIgnore: false, @@ -311,5 +315,219 @@ 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', + ); + }); + }); }); }); diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index f3100bce4d..1b236f9ac7 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -23,6 +23,9 @@ import { fetchAdminControlsOnce, getCodeAssistServer, ExperimentFlags, + isHeadlessMode, + FatalAuthenticationError, + isCloudShell, type TelemetryTarget, type ConfigParameters, type ExtensionLoader, @@ -103,8 +106,8 @@ export async function loadConfig( trustedFolder: true, extensionLoader, checkpointing, - interactive: true, - enableInteractiveShell: true, + interactive: !isHeadlessMode(), + enableInteractiveShell: !isHeadlessMode(), ptyInfo: 'auto', }; @@ -255,7 +258,61 @@ async function refreshAuthentication( `[${logPrefix}] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`, ); } - await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE); + + 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; + } + } + } logger.info( `[${logPrefix}] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`, ); diff --git a/packages/cli/src/zed-integration/zedIntegration.test.ts b/packages/cli/src/acp/acpClient.test.ts similarity index 99% rename from packages/cli/src/zed-integration/zedIntegration.test.ts rename to packages/cli/src/acp/acpClient.test.ts index 810cb9a1de..399b365c46 100644 --- a/packages/cli/src/zed-integration/zedIntegration.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -14,7 +14,7 @@ import { type Mock, type Mocked, } from 'vitest'; -import { GeminiAgent, Session } from './zedIntegration.js'; +import { GeminiAgent, Session } from './acpClient.js'; import type { CommandHandler } from './commandHandler.js'; import * as acp from '@agentclientprotocol/sdk'; import { diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/acp/acpClient.ts similarity index 99% rename from packages/cli/src/zed-integration/zedIntegration.ts rename to packages/cli/src/acp/acpClient.ts index dc07502f7f..a4afb1ade2 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -70,7 +70,7 @@ import { runExitCleanup } from '../utils/cleanup.js'; import { SessionSelector } from '../utils/sessionUtils.js'; import { CommandHandler } from './commandHandler.js'; -export async function runZedIntegration( +export async function runAcpClient( config: Config, settings: LoadedSettings, argv: CliArgs, diff --git a/packages/cli/src/zed-integration/acpErrors.test.ts b/packages/cli/src/acp/acpErrors.test.ts similarity index 100% rename from packages/cli/src/zed-integration/acpErrors.test.ts rename to packages/cli/src/acp/acpErrors.test.ts diff --git a/packages/cli/src/zed-integration/acpErrors.ts b/packages/cli/src/acp/acpErrors.ts similarity index 100% rename from packages/cli/src/zed-integration/acpErrors.ts rename to packages/cli/src/acp/acpErrors.ts diff --git a/packages/cli/src/zed-integration/acpResume.test.ts b/packages/cli/src/acp/acpResume.test.ts similarity index 99% rename from packages/cli/src/zed-integration/acpResume.test.ts rename to packages/cli/src/acp/acpResume.test.ts index cda47c17b4..37354af5c9 100644 --- a/packages/cli/src/zed-integration/acpResume.test.ts +++ b/packages/cli/src/acp/acpResume.test.ts @@ -13,7 +13,7 @@ import { type Mocked, type Mock, } from 'vitest'; -import { GeminiAgent } from './zedIntegration.js'; +import { GeminiAgent } from './acpClient.js'; import * as acp from '@agentclientprotocol/sdk'; import { ApprovalMode, diff --git a/packages/cli/src/zed-integration/commandHandler.test.ts b/packages/cli/src/acp/commandHandler.test.ts similarity index 100% rename from packages/cli/src/zed-integration/commandHandler.test.ts rename to packages/cli/src/acp/commandHandler.test.ts diff --git a/packages/cli/src/zed-integration/commandHandler.ts b/packages/cli/src/acp/commandHandler.ts similarity index 100% rename from packages/cli/src/zed-integration/commandHandler.ts rename to packages/cli/src/acp/commandHandler.ts diff --git a/packages/cli/src/zed-integration/commands/commandRegistry.ts b/packages/cli/src/acp/commands/commandRegistry.ts similarity index 100% rename from packages/cli/src/zed-integration/commands/commandRegistry.ts rename to packages/cli/src/acp/commands/commandRegistry.ts diff --git a/packages/cli/src/zed-integration/commands/extensions.ts b/packages/cli/src/acp/commands/extensions.ts similarity index 100% rename from packages/cli/src/zed-integration/commands/extensions.ts rename to packages/cli/src/acp/commands/extensions.ts diff --git a/packages/cli/src/zed-integration/commands/init.ts b/packages/cli/src/acp/commands/init.ts similarity index 100% rename from packages/cli/src/zed-integration/commands/init.ts rename to packages/cli/src/acp/commands/init.ts diff --git a/packages/cli/src/zed-integration/commands/memory.ts b/packages/cli/src/acp/commands/memory.ts similarity index 100% rename from packages/cli/src/zed-integration/commands/memory.ts rename to packages/cli/src/acp/commands/memory.ts diff --git a/packages/cli/src/zed-integration/commands/restore.ts b/packages/cli/src/acp/commands/restore.ts similarity index 100% rename from packages/cli/src/zed-integration/commands/restore.ts rename to packages/cli/src/acp/commands/restore.ts diff --git a/packages/cli/src/zed-integration/commands/types.ts b/packages/cli/src/acp/commands/types.ts similarity index 100% rename from packages/cli/src/zed-integration/commands/types.ts rename to packages/cli/src/acp/commands/types.ts diff --git a/packages/cli/src/zed-integration/fileSystemService.test.ts b/packages/cli/src/acp/fileSystemService.test.ts similarity index 100% rename from packages/cli/src/zed-integration/fileSystemService.test.ts rename to packages/cli/src/acp/fileSystemService.test.ts diff --git a/packages/cli/src/zed-integration/fileSystemService.ts b/packages/cli/src/acp/fileSystemService.ts similarity index 100% rename from packages/cli/src/zed-integration/fileSystemService.ts rename to packages/cli/src/acp/fileSystemService.ts diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index 5255dfeb83..1886444b88 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -5,6 +5,7 @@ */ import type { CommandModule } from 'yargs'; +import * as path from 'node:path'; import chalk from 'chalk'; import { debugLogger, @@ -51,12 +52,13 @@ export async function handleInstall(args: InstallArgs) { const settings = loadSettings(workspaceDir).merged; if (installMetadata.type === 'local' || installMetadata.type === 'link') { - const resolvedPath = getRealPath(source); - installMetadata.source = resolvedPath; - const trustResult = isWorkspaceTrusted(settings, resolvedPath); + const absolutePath = path.resolve(source); + const realPath = getRealPath(absolutePath); + installMetadata.source = absolutePath; + const trustResult = isWorkspaceTrusted(settings, absolutePath); if (trustResult.isTrusted !== true) { const discoveryResults = - await FolderTrustDiscoveryService.discover(resolvedPath); + await FolderTrustDiscoveryService.discover(realPath); const hasDiscovery = discoveryResults.commands.length > 0 || @@ -69,7 +71,7 @@ export async function handleInstall(args: InstallArgs) { '', chalk.bold('Do you trust the files in this folder?'), '', - `The extension source at "${resolvedPath}" is not trusted.`, + `The extension source at "${absolutePath}" is not trusted.`, '', 'Trusting a folder allows Gemini CLI to load its local configurations,', 'including custom commands, hooks, MCP servers, agent skills, and', @@ -127,10 +129,10 @@ export async function handleInstall(args: InstallArgs) { ); if (confirmed) { const trustedFolders = loadTrustedFolders(); - await trustedFolders.setValue(resolvedPath, TrustLevel.TRUST_FOLDER); + await trustedFolders.setValue(realPath, TrustLevel.TRUST_FOLDER); } else { throw new Error( - `Installation aborted: Folder "${resolvedPath}" is not trusted.`, + `Installation aborted: Folder "${absolutePath}" is not trusted.`, ); } } diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index b22b7412cc..f8c857cee8 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -953,12 +953,6 @@ 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(() => { @@ -1080,9 +1074,7 @@ 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([...defaultExcludes, ASK_USER_TOOL_NAME]), - ); + expect(config.getExcludeTools()).toEqual(new Set([ASK_USER_TOOL_NAME])); }); it('should handle settings with excludeTools but no extensions', async () => { @@ -1163,9 +1155,9 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, 'test-session', argv); const excludedTools = config.getExcludeTools(); - expect(excludedTools).toContain(SHELL_TOOL_NAME); - expect(excludedTools).toContain(EDIT_TOOL_NAME); - expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME); + 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(ASK_USER_TOOL_NAME); }); @@ -1184,9 +1176,9 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, 'test-session', argv); const excludedTools = config.getExcludeTools(); - expect(excludedTools).toContain(SHELL_TOOL_NAME); - expect(excludedTools).toContain(EDIT_TOOL_NAME); - expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME); + 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(ASK_USER_TOOL_NAME); }); @@ -1205,7 +1197,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, 'test-session', argv); const excludedTools = config.getExcludeTools(); - expect(excludedTools).toContain(SHELL_TOOL_NAME); + 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(ASK_USER_TOOL_NAME); @@ -1251,9 +1243,9 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, 'test-session', argv); const excludedTools = config.getExcludeTools(); - expect(excludedTools).toContain(SHELL_TOOL_NAME); - expect(excludedTools).toContain(EDIT_TOOL_NAME); - expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME); + 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(ASK_USER_TOOL_NAME); }); @@ -1315,9 +1307,10 @@ describe('Approval mode tool exclusion logic', () => { const excludedTools = config.getExcludeTools(); expect(excludedTools).toContain('custom_tool'); // From settings - expect(excludedTools).toContain(SHELL_TOOL_NAME); // From approval mode + expect(excludedTools).not.toContain(SHELL_TOOL_NAME); // No longer 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 () => { @@ -2164,9 +2157,9 @@ describe('loadCliConfig tool exclusions', () => { 'test-session', argv, ); - expect(config.getExcludeTools()).toContain('run_shell_command'); - expect(config.getExcludeTools()).toContain('replace'); - expect(config.getExcludeTools()).toContain('write_file'); + 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('ask_user'); }); @@ -2204,7 +2197,7 @@ describe('loadCliConfig tool exclusions', () => { expect(config.getExcludeTools()).not.toContain(SHELL_TOOL_NAME); }); - it('should exclude web-fetch in non-interactive mode when not allowed', async () => { + it('should not exclude web-fetch in non-interactive mode at config level', async () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments(createTestMergedSettings()); @@ -2213,7 +2206,7 @@ describe('loadCliConfig tool exclusions', () => { 'test-session', argv, ); - expect(config.getExcludeTools()).toContain(WEB_FETCH_TOOL_NAME); + expect(config.getExcludeTools()).not.toContain(WEB_FETCH_TOOL_NAME); }); it('should not exclude web-fetch in non-interactive mode when allowed', async () => { @@ -3326,11 +3319,11 @@ describe('Policy Engine Integration in loadCliConfig', () => { await loadCliConfig(settings, 'test-session', argv); - // In non-interactive mode, ShellTool, etc. are excluded + // In non-interactive mode, only ask_user is excluded by default expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith( expect.objectContaining({ tools: expect.objectContaining({ - exclude: expect.arrayContaining([SHELL_TOOL_NAME]), + exclude: expect.arrayContaining([ASK_USER_TOOL_NAME]), }), }), expect.anything(), diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 4f48c696b4..a1ce5b7d1c 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -19,16 +19,11 @@ 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, @@ -81,7 +76,8 @@ export interface CliArgs { policy: string[] | undefined; allowedMcpServerNames: string[] | undefined; allowedTools: string[] | undefined; - experimentalAcp: boolean | undefined; + acp?: boolean; + experimentalAcp?: boolean; extensions: string[] | undefined; listExtensions: boolean | undefined; resume: string | typeof RESUME_LATEST | undefined; @@ -177,10 +173,15 @@ export async function parseArguments( .filter(Boolean), ), }) - .option('experimental-acp', { + .option('acp', { type: 'boolean', description: 'Starts the agent in ACP mode', }) + .option('experimental-acp', { + type: 'boolean', + description: + 'Starts the agent in ACP mode (deprecated, use --acp instead)', + }) .option('allowed-mcp-server-names', { type: 'array', string: true, @@ -395,36 +396,6 @@ 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, -) { - 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 || @@ -632,54 +603,20 @@ export async function loadCliConfig( // -i/--prompt-interactive forces interactive mode with an initial prompt const interactive = !!argv.promptInteractive || + !!argv.acp || !!argv.experimentalAcp || (!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) && !argv.isCommand); const allowedTools = argv.allowedTools || settings.tools?.allowed || []; - const allowedToolsSet = new Set(allowedTools); // In non-interactive mode, exclude tools that require a prompt. const extraExcludes: string[] = []; if (!interactive) { - // ask_user requires user interaction and must be excluded in all - // non-interactive modes, regardless of the approval mode. + // 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. 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); @@ -758,6 +695,7 @@ export async function loadCliConfig( } return new Config({ + acpMode: !!argv.acp || !!argv.experimentalAcp, sessionId, clientVersion: await getVersion(), embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, @@ -821,7 +759,7 @@ export async function loadCliConfig( bugCommand: settings.advanced?.bugCommand, model: resolvedModel, maxSessionTurns: settings.model?.maxSessionTurns, - experimentalZedIntegration: argv.experimentalAcp || false, + listExtensions: argv.listExtensions || false, listSessions: argv.listSessions || false, deleteSession: argv.deleteSession, diff --git a/packages/cli/src/config/extension-manager.test.ts b/packages/cli/src/config/extension-manager.test.ts index 4ab52e24b5..a5fb822cdb 100644 --- a/packages/cli/src/config/extension-manager.test.ts +++ b/packages/cli/src/config/extension-manager.test.ts @@ -12,6 +12,13 @@ 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')); @@ -185,4 +192,157 @@ 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/, + ); + } + }); + }); }); diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index a9fce44635..678350ba49 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -161,7 +161,9 @@ export class ExtensionManager extends ExtensionLoader { const extensionAllowed = this.settings.security?.allowedExtensions.some( (pattern) => { try { - return new RegExp(pattern).test(installMetadata.source); + return new RegExp(pattern).test( + getRealPath(installMetadata.source), + ); } catch (e) { throw new Error( `Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`, @@ -210,11 +212,9 @@ export class ExtensionManager extends ExtensionLoader { await fs.promises.mkdir(extensionsDir, { recursive: true }); if (installMetadata.type === 'local' || installMetadata.type === 'link') { - installMetadata.source = getRealPath( - path.isAbsolute(installMetadata.source) - ? installMetadata.source - : path.resolve(this.workspaceDir, installMetadata.source), - ); + installMetadata.source = path.isAbsolute(installMetadata.source) + ? installMetadata.source + : path.resolve(this.workspaceDir, installMetadata.source); } let tempDir: string | undefined; @@ -262,7 +262,7 @@ Would you like to attempt to install via "git clone" instead?`, installMetadata.type === 'local' || installMetadata.type === 'link' ) { - localSourcePath = installMetadata.source; + localSourcePath = getRealPath(installMetadata.source); } else { throw new Error(`Unsupported install type: ${installMetadata.type}`); } @@ -638,7 +638,9 @@ Would you like to attempt to install via "git clone" instead?`, const extensionAllowed = this.settings.security?.allowedExtensions.some( (pattern) => { try { - return new RegExp(pattern).test(installMetadata?.source); + return new RegExp(pattern).test( + getRealPath(installMetadata?.source ?? ''), + ); } catch (e) { throw new Error( `Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`, diff --git a/packages/cli/src/config/footerItems.test.ts b/packages/cli/src/config/footerItems.test.ts new file mode 100644 index 0000000000..420246811b --- /dev/null +++ b/packages/cli/src/config/footerItems.test.ts @@ -0,0 +1,91 @@ +/** + * @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', + ]); + }); +}); diff --git a/packages/cli/src/config/footerItems.ts b/packages/cli/src/config/footerItems.ts new file mode 100644 index 0000000000..8410d0b5ec --- /dev/null +++ b/packages/cli/src/config/footerItems.ts @@ -0,0 +1,132 @@ +/** + * @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 = 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; +} { + 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), + }; +} diff --git a/packages/cli/src/config/policy-engine.integration.test.ts b/packages/cli/src/config/policy-engine.integration.test.ts index 02515815d0..0c90ad2b0f 100644 --- a/packages/cli/src/config/policy-engine.integration.test.ts +++ b/packages/cli/src/config/policy-engine.integration.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ApprovalMode, PolicyDecision, @@ -29,6 +29,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }); describe('Policy Engine Integration Tests', () => { + beforeEach(() => vi.stubEnv('GEMINI_SYSTEM_MD', '')); + + afterEach(() => vi.unstubAllEnvs()); + describe('Policy configuration produces valid PolicyEngine config', () => { it('should create a working PolicyEngine from basic settings', async () => { const settings: Settings = { diff --git a/packages/cli/src/config/sandboxConfig.test.ts b/packages/cli/src/config/sandboxConfig.test.ts index 14080dc30b..51c4f7d83c 100644 --- a/packages/cli/src/config/sandboxConfig.test.ts +++ b/packages/cli/src/config/sandboxConfig.test.ts @@ -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", + "Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec, runsc, lxc", ); }); @@ -108,6 +108,22 @@ 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', () => { @@ -178,7 +194,7 @@ describe('loadSandboxConfig', () => { await expect( loadSandboxConfig({}, { sandbox: 'invalid-command' }), ).rejects.toThrow( - "Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec", + "Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec, runsc, lxc", ); }); }); @@ -231,4 +247,92 @@ describe('loadSandboxConfig', () => { }, ); }); + + describe('with sandbox: runsc (gVisor)', () => { + beforeEach(() => { + mockedOsPlatform.mockReturnValue('linux'); + mockedCommandExistsSync.mockReturnValue(true); + }); + + it('should use runsc via CLI argument on Linux', async () => { + const config = await loadSandboxConfig({}, { sandbox: 'runsc' }); + + expect(config).toEqual({ command: 'runsc', image: 'default/image' }); + expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc'); + expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker'); + }); + + it('should use runsc via GEMINI_SANDBOX environment variable', async () => { + process.env['GEMINI_SANDBOX'] = 'runsc'; + const config = await loadSandboxConfig({}, {}); + + expect(config).toEqual({ command: 'runsc', image: 'default/image' }); + expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc'); + expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker'); + }); + + it('should use runsc via settings file', async () => { + const config = await loadSandboxConfig( + { tools: { sandbox: 'runsc' } }, + {}, + ); + + expect(config).toEqual({ command: 'runsc', image: 'default/image' }); + expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc'); + expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker'); + }); + + it('should prioritize GEMINI_SANDBOX over CLI and settings', async () => { + process.env['GEMINI_SANDBOX'] = 'runsc'; + const config = await loadSandboxConfig( + { tools: { sandbox: 'docker' } }, + { sandbox: 'podman' }, + ); + + expect(config).toEqual({ command: 'runsc', image: 'default/image' }); + }); + + it('should reject runsc on macOS (Linux-only)', async () => { + mockedOsPlatform.mockReturnValue('darwin'); + + await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow( + 'gVisor (runsc) sandboxing is only supported on Linux', + ); + }); + + it('should reject runsc on Windows (Linux-only)', async () => { + mockedOsPlatform.mockReturnValue('win32'); + + await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow( + 'gVisor (runsc) sandboxing is only supported on Linux', + ); + }); + + it('should throw if runsc binary not found', async () => { + mockedCommandExistsSync.mockReturnValue(false); + + await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow( + "Missing sandbox command 'runsc' (from GEMINI_SANDBOX)", + ); + }); + + it('should throw if Docker not available (runsc requires Docker)', async () => { + mockedCommandExistsSync.mockImplementation((cmd) => cmd === 'runsc'); + + await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow( + "runsc (gVisor) requires Docker. Install Docker, or use sandbox: 'docker'.", + ); + }); + + it('should NOT auto-detect runsc when both runsc and docker available', async () => { + mockedCommandExistsSync.mockImplementation( + (cmd) => cmd === 'runsc' || cmd === 'docker', + ); + + const config = await loadSandboxConfig({}, { sandbox: true }); + + expect(config?.command).toBe('docker'); + expect(config?.command).not.toBe('runsc'); + }); + }); }); diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index 57430becae..968d3e427a 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -27,6 +27,8 @@ const VALID_SANDBOX_COMMANDS: ReadonlyArray = [ 'docker', 'podman', 'sandbox-exec', + 'runsc', + 'lxc', ]; function isSandboxCommand(value: string): value is SandboxConfig['command'] { @@ -63,17 +65,30 @@ function getSandboxCommand( )}`, ); } - // confirm that specified command exists - if (commandExists.sync(sandbox)) { - return sandbox; + // runsc (gVisor) is only supported on Linux + if (sandbox === 'runsc' && os.platform() !== 'linux') { + throw new FatalSandboxError( + 'gVisor (runsc) sandboxing is only supported on Linux', + ); } - throw new FatalSandboxError( - `Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`, - ); + // confirm that specified command exists + if (!commandExists.sync(sandbox)) { + throw new FatalSandboxError( + `Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`, + ); + } + // runsc uses Docker with --runtime=runsc; both must be available (prioritize runsc when explicitly chosen) + if (sandbox === 'runsc' && !commandExists.sync('docker')) { + throw new FatalSandboxError( + "runsc (gVisor) requires Docker. Install Docker, or use sandbox: 'docker'.", + ); + } + return sandbox; } // look for seatbelt, docker, or podman, in that order // for container-based sandboxing, require sandbox to be enabled explicitly + // note: runsc is NOT auto-detected, it must be explicitly specified if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) { return 'sandbox-exec'; } else if (commandExists.sync('docker') && sandbox === true) { @@ -91,6 +106,9 @@ 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( diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 8fd0bd81b0..5589ef11ba 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2162,7 +2162,7 @@ describe('Settings Loading and Merging', () => { } }); - it('should prioritize new settings over deprecated ones and respect removeDeprecated flag', () => { + it('should remove deprecated settings by default and prioritize new ones', () => { const userSettingsContent = { general: { disableAutoUpdate: true, @@ -2177,27 +2177,11 @@ describe('Settings Loading and Merging', () => { }; const loadedSettings = createMockSettings(userSettingsContent); - const setValueSpy = vi.spyOn(loadedSettings, 'setValue'); - // 1. removeDeprecated = false (default) + // Default is now removeDeprecated = true migrateDeprecatedSettings(loadedSettings); - // Should still have old settings - expect( - loadedSettings.forScope(SettingScope.User).settings.general, - ).toHaveProperty('disableAutoUpdate'); - expect( - ( - loadedSettings.forScope(SettingScope.User).settings.context as { - fileFiltering: { disableFuzzySearch: boolean }; - } - ).fileFiltering, - ).toHaveProperty('disableFuzzySearch'); - - // 2. removeDeprecated = true - migrateDeprecatedSettings(loadedSettings, true); - // Should remove disableAutoUpdate and trust enableAutoUpdate: true expect(setValueSpy).toHaveBeenCalledWith(SettingScope.User, 'general', { enableAutoUpdate: true, @@ -2209,6 +2193,37 @@ describe('Settings Loading and Merging', () => { }); }); + 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 + expect( + loadedSettings.forScope(SettingScope.User).settings.general, + ).toHaveProperty('disableAutoUpdate'); + expect( + ( + loadedSettings.forScope(SettingScope.User).settings.context as { + fileFiltering: { disableFuzzySearch: boolean }; + } + ).fileFiltering, + ).toHaveProperty('disableFuzzySearch'); + }); + it('should trigger migration automatically during loadSettings', () => { mockFsExistsSync.mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 4e9faf5767..422dda6115 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -20,8 +20,8 @@ import { type AdminControlsSettings, } from '@google/gemini-cli-core'; import stripJsonComments from 'strip-json-comments'; -import { DefaultLight } from '../ui/themes/default-light.js'; -import { DefaultDark } from '../ui/themes/default.js'; +import { DefaultLight } from '../ui/themes/builtin/light/default-light.js'; +import { DefaultDark } from '../ui/themes/builtin/dark/default-dark.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { type Settings, @@ -796,14 +796,13 @@ export function loadSettings( /** * Migrates deprecated settings to their new counterparts. * - * TODO: After a couple of weeks (around early Feb 2026), we should start removing - * the deprecated settings from the settings files by default. + * Deprecated settings are removed from settings files by default. * * @returns true if any changes were made and need to be saved. */ export function migrateDeprecatedSettings( loadedSettings: LoadedSettings, - removeDeprecated = false, + removeDeprecated = true, ): boolean { let anyModified = false; const systemWarnings: Map = new Map(); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 04d1bda8e1..e378b6bc0d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -575,14 +575,34 @@ 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 path in the footer.', + description: 'Hide the current working directory in the footer.', showInDialog: true, }, hideSandboxStatus: { @@ -1246,7 +1266,8 @@ const SETTINGS_SCHEMA = { ref: 'BooleanOrString', description: oneLine` Sandbox execution environment. - Set to a boolean to enable or disable the sandbox, or provide a string path to a sandbox profile. + 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"). `, showInDialog: false, }, diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index 714d703241..cfe0447078 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -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); + fs.symlinkSync(realDir, symlinkDir, 'dir'); // Rule uses realpath const config = { [realDir]: TrustLevel.TRUST_FOLDER }; diff --git a/packages/cli/src/core/auth.test.ts b/packages/cli/src/core/auth.test.ts index f28e826f49..5db9cd5449 100644 --- a/packages/cli/src/core/auth.test.ts +++ b/packages/cli/src/core/auth.test.ts @@ -9,6 +9,7 @@ import { performInitialAuth } from './auth.js'; import { type Config, ValidationRequiredError, + ProjectIdRequiredError, AuthType, } from '@google/gemini-cli-core'; @@ -116,4 +117,22 @@ 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, + ); + }); }); diff --git a/packages/cli/src/core/auth.ts b/packages/cli/src/core/auth.ts index f49fdecf76..f0b8015013 100644 --- a/packages/cli/src/core/auth.ts +++ b/packages/cli/src/core/auth.ts @@ -10,6 +10,7 @@ import { getErrorMessage, ValidationRequiredError, isAccountSuspendedError, + ProjectIdRequiredError, } from '@google/gemini-cli-core'; import type { AccountSuspensionInfo } from '../ui/contexts/UIStateContext.js'; @@ -54,6 +55,14 @@ 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, diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 88f9f404cd..6071488542 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -79,7 +79,7 @@ import { type InitializationResult, } from './core/initializer.js'; import { validateAuthMethod } from './config/auth.js'; -import { runZedIntegration } from './zed-integration/zedIntegration.js'; +import { runAcpClient } from './acp/acpClient.js'; import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; import { checkForUpdates } from './ui/utils/updateCheck.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; @@ -672,8 +672,8 @@ export async function main() { await getOauthClient(settings.merged.security.auth.selectedType, config); } - if (config.getExperimentalZedIntegration()) { - return runZedIntegration(config, settings, argv); + if (config.getAcpMode()) { + return runAcpClient(config, settings, argv); } let input = config.getQuestion(); diff --git a/packages/cli/src/gemini_cleanup.test.tsx b/packages/cli/src/gemini_cleanup.test.tsx index fb37bb94ec..536da027d4 100644 --- a/packages/cli/src/gemini_cleanup.test.tsx +++ b/packages/cli/src/gemini_cleanup.test.tsx @@ -179,7 +179,7 @@ describe('gemini.tsx main function cleanup', () => { vi.restoreAllMocks(); }); - it('should log error when cleanupExpiredSessions fails', async () => { + it.skip('should log error when cleanupExpiredSessions fails', async () => { const { loadCliConfig, parseArguments } = await import( './config/config.js' ); @@ -216,7 +216,7 @@ describe('gemini.tsx main function cleanup', () => { getMcpServers: () => ({}), getMcpClientManager: vi.fn(), getIdeMode: vi.fn(() => false), - getExperimentalZedIntegration: vi.fn(() => true), + getAcpMode: vi.fn(() => true), getScreenReader: vi.fn(() => false), getGeminiMdFileCount: vi.fn(() => 0), getProjectRoot: vi.fn(() => '/'), diff --git a/packages/cli/src/integration-tests/modelSteering.test.tsx b/packages/cli/src/integration-tests/modelSteering.test.tsx index ca1970cebc..27bcde0dc2 100644 --- a/packages/cli/src/integration-tests/modelSteering.test.tsx +++ b/packages/cli/src/integration-tests/modelSteering.test.tsx @@ -65,10 +65,6 @@ 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/, diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 31673e921a..f867f84c80 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -31,6 +31,7 @@ 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'; @@ -119,6 +120,7 @@ export class BuiltinCommandLoader implements ICommandLoader { ] : [extensionsCommand(this.config?.getEnableExtensionReloading())]), helpCommand, + footerCommand, shortcutsCommand, ...(this.config?.getEnableHooksUI() ? [hooksCommand] : []), rewindCommand, diff --git a/packages/cli/src/test-utils/fixtures/steering.responses b/packages/cli/src/test-utils/fixtures/steering.responses index 66407f819e..6d843010f1 100644 --- a/packages/cli/src/test-utils/fixtures/steering.responses +++ b/packages/cli/src/test-utils/fixtures/steering.responses @@ -1,4 +1,3 @@ {"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"}]}]} diff --git a/packages/cli/src/test-utils/mockConfig.ts b/packages/cli/src/test-utils/mockConfig.ts index 8b7c7c520d..c8ab45a35d 100644 --- a/packages/cli/src/test-utils/mockConfig.ts +++ b/packages/cli/src/test-utils/mockConfig.ts @@ -42,7 +42,7 @@ export const createMockConfig = (overrides: Partial = {}): Config => setSessionId: vi.fn(), getSessionId: vi.fn().mockReturnValue('mock-session-id'), getContentGeneratorConfig: vi.fn(() => ({ authType: 'google' })), - getExperimentalZedIntegration: vi.fn(() => false), + getAcpMode: vi.fn(() => false), isBrowserLaunchSuppressed: vi.fn(() => false), setRemoteAdminSettings: vi.fn(), isYoloModeDisabled: vi.fn(() => false), diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 468e7b6bfc..7d5a0e62bb 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -17,6 +17,7 @@ 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'; @@ -49,7 +50,7 @@ import { AppContext, type AppState } from '../ui/contexts/AppContext.js'; import { createMockSettings } from './settings.js'; import { SessionStatsProvider } from '../ui/contexts/SessionContext.js'; import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js'; -import { DefaultLight } from '../ui/themes/default-light.js'; +import { DefaultLight } from '../ui/themes/builtin/light/default-light.js'; import { pickDefaultThemeName } from '../ui/themes/theme.js'; import { generateSvgForTerminal } from './svg.js'; @@ -502,7 +503,22 @@ const configProxy = new Proxy({} as Config, { get(_target, prop) { if (prop === 'getTargetDir') { return () => - '/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long'; + path.join( + path.parse(process.cwd()).root, + 'Users', + 'test', + 'project', + 'foo', + 'bar', + 'and', + 'some', + 'more', + 'directories', + 'to', + 'make', + 'it', + 'long', + ); } if (prop === 'getUseBackgroundColor') { return () => true; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 5243a1984d..9c7e51eda1 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -80,8 +80,8 @@ import { type ConsentRequestPayload, type AgentsDiscoveredPayload, ChangeAuthRequestedError, + ProjectIdRequiredError, CoreToolCallStatus, - generateSteeringAckMessage, buildUserSteeringHintPrompt, logBillingEvent, ApiKeyUpdatedEvent, @@ -796,6 +796,12 @@ 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)}`, ); @@ -2139,15 +2145,6 @@ 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, diff --git a/packages/cli/src/ui/auth/useAuth.test.tsx b/packages/cli/src/ui/auth/useAuth.test.tsx index 36d9aeec4f..20a02ffb21 100644 --- a/packages/cli/src/ui/auth/useAuth.test.tsx +++ b/packages/cli/src/ui/auth/useAuth.test.tsx @@ -15,7 +15,11 @@ import { } from 'vitest'; import { renderHook } from '../../test-utils/render.js'; import { useAuthCommand, validateAuthMethodWithSettings } from './useAuth.js'; -import { AuthType, type Config } from '@google/gemini-cli-core'; +import { + AuthType, + type Config, + ProjectIdRequiredError, +} from '@google/gemini-cli-core'; import { AuthState } from '../types.js'; import type { LoadedSettings } from '../../config/settings.js'; import { waitFor } from '../../test-utils/async.js'; @@ -288,5 +292,21 @@ 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); + }); + }); }); }); diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index 3faec2d5a8..afd438bb00 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -12,6 +12,7 @@ import { loadApiKey, debugLogger, isAccountSuspendedError, + ProjectIdRequiredError, } from '@google/gemini-cli-core'; import { getErrorMessage } from '@google/gemini-cli-core'; import { AuthState } from '../types.js'; @@ -143,6 +144,10 @@ 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)}`); } diff --git a/packages/cli/src/ui/commands/footerCommand.tsx b/packages/cli/src/ui/commands/footerCommand.tsx new file mode 100644 index 0000000000..4a6760e229 --- /dev/null +++ b/packages/cli/src/ui/commands/footerCommand.tsx @@ -0,0 +1,25 @@ +/** + * @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: , + }), +}; diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx index bcd5fd62b5..dcb2a3eae7 100644 --- a/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx +++ b/packages/cli/src/ui/components/ContextUsageDisplay.test.tsx @@ -28,7 +28,7 @@ describe('ContextUsageDisplay', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain('50% context used'); + expect(output).toContain('50% used'); unmount(); }); @@ -42,7 +42,7 @@ describe('ContextUsageDisplay', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain('0% context used'); + expect(output).toContain('0% used'); unmount(); }); @@ -72,7 +72,7 @@ describe('ContextUsageDisplay', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain('80% context used'); + expect(output).toContain('80% used'); unmount(); }); @@ -86,7 +86,7 @@ describe('ContextUsageDisplay', () => { ); await waitUntilReady(); const output = lastFrame(); - expect(output).toContain('100% context used'); + expect(output).toContain('100% used'); unmount(); }); }); diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.tsx index 66cb8ed234..3e82145dca 100644 --- a/packages/cli/src/ui/components/ContextUsageDisplay.tsx +++ b/packages/cli/src/ui/components/ContextUsageDisplay.tsx @@ -38,7 +38,7 @@ export const ContextUsageDisplay = ({ } const label = - terminalWidth < MIN_TERMINAL_WIDTH_FOR_FULL_LABEL ? '%' : '% context used'; + terminalWidth < MIN_TERMINAL_WIDTH_FOR_FULL_LABEL ? '%' : '% used'; return ( diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 7187240249..b79b005d85 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -4,16 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { renderWithProviders } from '../../test-utils/render.js'; -import { createMockSettings } from '../../test-utils/settings.js'; import { Footer } from './Footer.js'; -import { - makeFakeConfig, - tildeifyPath, - ToolCallDecision, -} from '@google/gemini-cli-core'; -import type { SessionStatsState } from '../contexts/SessionContext.js'; +import { createMockSettings } from '../../test-utils/settings.js'; +import path from 'node:path'; + +// Normalize paths to POSIX slashes for stable cross-platform snapshots. +const normalizeFrame = (frame: string | undefined) => { + if (!frame) return frame; + return frame.replace(/\\/g, '/'); +}; let mockIsDevelopment = false; @@ -49,14 +50,18 @@ const defaultProps = { branchName: 'main', }; -const mockSessionStats: SessionStatsState = { - sessionId: 'test-session', +const mockSessionStats = { + sessionId: 'test-session-id', sessionStartTime: new Date(), - lastPromptTokenCount: 0, promptCount: 0, + lastPromptTokenCount: 150000, metrics: { - models: {}, + files: { + totalLinesAdded: 12, + totalLinesRemoved: 4, + }, tools: { + count: 0, totalCalls: 0, totalSuccess: 0, totalFail: 0, @@ -65,18 +70,39 @@ const mockSessionStats: SessionStatsState = { accept: 0, reject: 0, modify: 0, - [ToolCallDecision.AUTO_ACCEPT]: 0, + auto_accept: 0, }, byName: {}, + latency: { avg: 0, max: 0, min: 0 }, }, - files: { - totalLinesAdded: 0, - totalLinesRemoved: 0, + models: { + 'gemini-pro': { + api: { + totalRequests: 0, + totalErrors: 0, + totalLatencyMs: 0, + }, + tokens: { + input: 0, + prompt: 0, + candidates: 0, + total: 1500, + cached: 0, + thoughts: 0, + tool: 0, + }, + roles: {}, + }, }, }, }; describe('