diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml index c37444498a..5c74524ddb 100644 --- a/.github/actions/publish-release/action.yml +++ b/.github/actions/publish-release/action.yml @@ -20,6 +20,9 @@ inputs: github-token: description: 'The GitHub token for creating the release.' required: true + github-release-token: + description: 'The GitHub token used specifically for creating the GitHub release (to trigger other workflows).' + required: false dry-run: description: 'Whether to run in dry-run mode.' type: 'string' @@ -254,7 +257,7 @@ runs: working-directory: '${{ inputs.working-directory }}' if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}" env: - GITHUB_TOKEN: '${{ inputs.github-token }}' + GITHUB_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}' shell: 'bash' run: | gh release create "${{ inputs.release-tag }}" \ diff --git a/.github/workflows/gemini-scheduled-stale-pr-closer.yml b/.github/workflows/gemini-scheduled-stale-pr-closer.yml index 90d7417b05..bd7fd0ddc9 100644 --- a/.github/workflows/gemini-scheduled-stale-pr-closer.yml +++ b/.github/workflows/gemini-scheduled-stale-pr-closer.yml @@ -43,23 +43,56 @@ jobs: // 1. Fetch maintainers for verification let maintainerLogins = new Set(); - let teamFetchSucceeded = false; - try { - const members = await github.paginate(github.rest.teams.listMembersInOrg, { - org: context.repo.owner, - team_slug: 'gemini-cli-maintainers' - }); - maintainerLogins = new Set(members.map(m => m.login.toLowerCase())); - teamFetchSucceeded = true; - core.info(`Successfully fetched ${maintainerLogins.size} team members from gemini-cli-maintainers`); - } catch (e) { - core.warning(`Failed to fetch team members from gemini-cli-maintainers: ${e.message}. Falling back to author_association only.`); + 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: context.repo.owner, + team_slug: team_slug + }); + for (const m of members) maintainerLogins.add(m.login.toLowerCase()); + core.info(`Successfully fetched ${members.length} team members from ${team_slug}`); + } catch (e) { + core.warning(`Failed to fetch team members from ${team_slug}: ${e.message}`); + } } - const isMaintainer = (login, assoc) => { + const isGooglerCache = new Map(); + const isGoogler = async (login) => { + if (isGooglerCache.has(login)) return isGooglerCache.get(login); + + try { + // Check membership in 'googlers' or 'google' orgs + const orgs = ['googlers', 'google']; + for (const org of orgs) { + try { + await github.rest.orgs.checkMembershipForUser({ + org: org, + username: login + }); + core.info(`User ${login} is a member of ${org} organization.`); + isGooglerCache.set(login, true); + return true; + } catch (e) { + // 404 just means they aren't a member, which is fine + if (e.status !== 404) throw e; + } + } + } catch (e) { + core.warning(`Failed to check org membership for ${login}: ${e.message}`); + } + + isGooglerCache.set(login, false); + return false; + }; + + const isMaintainer = async (login, assoc) => { const isTeamMember = maintainerLogins.has(login.toLowerCase()); const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc); - return isTeamMember || isRepoMaintainer; + if (isTeamMember || isRepoMaintainer) return true; + + return await isGoogler(login); }; // 2. Determine which PRs to check @@ -81,7 +114,7 @@ jobs: } for (const pr of prs) { - const maintainerPr = isMaintainer(pr.user.login, pr.author_association); + const maintainerPr = await isMaintainer(pr.user.login, pr.author_association); const isBot = pr.user.type === 'Bot' || pr.user.login.endsWith('[bot]'); // Detection Logic for Linked Issues @@ -175,7 +208,7 @@ jobs: pull_number: pr.number }); for (const r of reviews) { - if (isMaintainer(r.user.login, r.author_association)) { + if (await isMaintainer(r.user.login, r.author_association)) { const d = new Date(r.submitted_at || r.updated_at); if (d > lastActivity) lastActivity = d; } @@ -186,7 +219,7 @@ jobs: issue_number: pr.number }); for (const c of comments) { - if (isMaintainer(c.user.login, c.author_association)) { + if (await isMaintainer(c.user.login, c.author_association)) { const d = new Date(c.updated_at); if (d > lastActivity) lastActivity = d; } diff --git a/.github/workflows/pr-contribution-guidelines-notifier.yml b/.github/workflows/pr-contribution-guidelines-notifier.yml index fdabd20f3d..2658520371 100644 --- a/.github/workflows/pr-contribution-guidelines-notifier.yml +++ b/.github/workflows/pr-contribution-guidelines-notifier.yml @@ -35,9 +35,31 @@ jobs: const pr_number = context.payload.pull_request.number; // 1. Check if the PR author is a maintainer + const isGoogler = async (login) => { + try { + const orgs = ['googlers', 'google']; + for (const org of orgs) { + try { + await github.rest.orgs.checkMembershipForUser({ + org: org, + username: login + }); + return true; + } catch (e) { + if (e.status !== 404) throw e; + } + } + } catch (e) { + core.warning(`Failed to check org membership for ${login}: ${e.message}`); + } + return false; + }; + const authorAssociation = context.payload.pull_request.author_association; - if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation)) { - core.info(`${username} is a maintainer (Association: ${authorAssociation}). No notification needed.`); + const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation); + + if (isRepoMaintainer || await isGoogler(username)) { + core.info(`${username} is a maintainer or Googler. No notification needed.`); return; } diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml index b393d87ea9..c9d2290a1c 100644 --- a/.github/workflows/release-manual.yml +++ b/.github/workflows/release-manual.yml @@ -110,6 +110,7 @@ jobs: wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}' github-token: '${{ secrets.GITHUB_TOKEN }}' + github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' dry-run: '${{ github.event.inputs.dry_run }}' previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}' skip-github-release: '${{ github.event.inputs.skip_github_release }}' diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 5fe7bca115..0a04e93517 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -124,6 +124,7 @@ jobs: wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}' github-token: '${{ secrets.GITHUB_TOKEN }}' + github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' dry-run: '${{ steps.vars.outputs.is_dry_run }}' previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}' working-directory: './release' @@ -144,7 +145,7 @@ jobs: branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}' pr-title: 'chore/release: bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}' pr-body: 'Automated version bump for nightly release.' - github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' dry-run: '${{ steps.vars.outputs.is_dry_run }}' working-directory: './release' diff --git a/.github/workflows/release-patch-3-release.yml b/.github/workflows/release-patch-3-release.yml index 19241b7396..b0d459f256 100644 --- a/.github/workflows/release-patch-3-release.yml +++ b/.github/workflows/release-patch-3-release.yml @@ -184,6 +184,7 @@ jobs: wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}' github-token: '${{ secrets.GITHUB_TOKEN }}' + github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' dry-run: '${{ github.event.inputs.dry_run }}' previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}' gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' diff --git a/.github/workflows/release-promote.yml b/.github/workflows/release-promote.yml index 486b9a2558..ebe16b1a39 100644 --- a/.github/workflows/release-promote.yml +++ b/.github/workflows/release-promote.yml @@ -239,6 +239,7 @@ jobs: wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}' github-token: '${{ secrets.GITHUB_TOKEN }}' + github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' dry-run: '${{ github.event.inputs.dry_run }}' previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_PREVIEW_TAG }}' working-directory: './release' @@ -305,6 +306,7 @@ jobs: wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}' github-token: '${{ secrets.GITHUB_TOKEN }}' + github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' dry-run: '${{ github.event.inputs.dry_run }}' previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_STABLE_TAG }}' working-directory: './release' @@ -390,7 +392,7 @@ jobs: branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}' pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}' pr-body: 'Automated version bump to prepare for the next nightly release.' - github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' dry-run: '${{ github.event.inputs.dry_run }}' - name: 'Create Issue on Failure' diff --git a/.prettierignore b/.prettierignore index e8f035ad74..9009498d8d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,3 +21,4 @@ junit.xml Thumbs.db .pytest_cache **/SKILL.md +packages/sdk/test-data/*.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3ff7505c7..6d8252f86c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -408,12 +408,13 @@ On macOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open` profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that restricts writes to the project folder but otherwise allows all other operations and outbound network traffic ("open") by default. You can switch to a -`restrictive-closed` profile (see -`packages/cli/src/utils/sandbox-macos-restrictive-closed.sb`) that declines all -operations and outbound network traffic ("closed") by default by setting -`SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file. -Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}` -(see below for proxied networking). You can also switch to a custom profile +`strict-open` profile (see +`packages/cli/src/utils/sandbox-macos-strict-open.sb`) that restricts both reads +and writes to the working directory while allowing outbound network traffic by +setting `SEATBELT_PROFILE=strict-open` in your environment or `.env` file. +Available built-in profiles are `permissive-{open,proxied}`, +`restrictive-{open,proxied}`, and `strict-{open,proxied}` (see below for proxied +networking). You can also switch to a custom profile `SEATBELT_PROFILE=` if you also create a file `.gemini/sandbox-macos-.sb` under your project settings directory `.gemini`. diff --git a/README.md b/README.md index 22e258e289..6e9b1da146 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,9 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/). ## 📦 Installation -### Pre-requisites before installation - -- Node.js version 20 or higher -- macOS, Linux, or Windows +See +[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md) +for recommended system specifications and a detailed installation guide. ### Quick Install diff --git a/docs/admin/enterprise-controls.md b/docs/admin/enterprise-controls.md new file mode 100644 index 0000000000..8c9ba60a13 --- /dev/null +++ b/docs/admin/enterprise-controls.md @@ -0,0 +1,115 @@ +# Enterprise Admin Controls + +Gemini CLI empowers enterprise administrators to manage and enforce security +policies and configuration settings across their entire organization. Secure +defaults are enabled automatically for all enterprise users, but can be +customized via the [Management Console](https://goo.gle/manage-gemini-cli). + +**Enterprise Admin Controls are enforced globally and cannot be overridden by +users locally**, ensuring a consistent security posture. + +## Admin Controls vs. System Settings + +While [System-wide settings](../cli/settings.md) act as convenient configuration +overrides, they can still be modified by users with sufficient privileges. In +contrast, admin controls are immutable at the local level, making them the +preferred method for enforcing policy. + +## Available Controls + +### Strict Mode + +**Enabled/Disabled** | Default: enabled + +If enabled, users will not be able to enter yolo mode. + +### Extensions + +**Enabled/Disabled** | Default: disabled + +If disabled, users will not be able to use or install extensions. See +[Extensions](../extensions/index.md) for more details. + +### MCP + +#### Enabled/Disabled + +**Enabled/Disabled** | Default: disabled + +If disabled, users will not be able to use MCP servers. See +[MCP Server Integration](../tools/mcp-server.md) for more details. + +#### MCP Servers (preview) + +**Default**: empty + +Allows administrators to define an explicit allowlist of MCP servers. This +guarantees that users can only connect to trusted MCP servers defined by the +organization. + +**Allowlist Format:** + +```json +{ + "mcpServers": { + "external-provider": { + "url": "https://api.mcp-provider.com", + "type": "sse", + "trust": true, + "includeTools": ["toolA", "toolB"], + "excludeTools": [] + }, + "internal-corp-tool": { + "url": "https://mcp.internal-tool.corp", + "type": "http", + "includeTools": [], + "excludeTools": ["adminTool"] + } + } +} +``` + +**Supported Fields:** + +- `url`: (Required) The full URL of the MCP server endpoint. +- `type`: (Required) The connection type (e.g., `sse` or `http`). +- `trust`: (Optional) If set to `true`, the server is trusted and tool execution + will not require user approval. +- `includeTools`: (Optional) An explicit list of tool names to allow. If + specified, only these tools will be available. +- `excludeTools`: (Optional) A list of tool names to hide. These tools will be + blocked. + +**Client Enforcement Logic:** + +- **Empty Allowlist**: If the admin allowlist is empty, the client uses the + user’s local configuration as is (unless the MCP toggle above is disabled). +- **Active Allowlist**: If the allowlist contains one or more servers, **all + locally configured servers not present in the allowlist are ignored**. +- **Configuration Merging**: For a server to be active, it must exist in + **both** the admin allowlist and the user’s local configuration (matched by + name). The client merges these definitions as follows: + - **Override Fields**: The `url`, `type`, & `trust` are always taken from the + admin allowlist, overriding any local values. + - **Tools Filtering**: If `includeTools` or `excludeTools` are defined in the + allowlist, the admin’s rules are used exclusively. If both are undefined in + the admin allowlist, the client falls back to the user’s local tool + settings. + - **Cleared Fields**: To ensure security and consistency, the client + automatically clears local execution fields (`command`, `args`, `env`, + `cwd`, `httpUrl`, `tcp`). This prevents users from overriding the connection + method. + - **Other Fields**: All other MCP fields are pulled from the user’s local + configuration. +- **Missing Allowlisted Servers**: If a server appears in the admin allowlist + but is missing from the local configuration, it will not be initialized. This + ensures users maintain final control over which permitted servers are actually + active in their environment. + +### Unmanaged Capabilities + +**Enabled/Disabled** | Default: disabled + +If disabled, users will not be able to use certain features. Currently, this +control disables Agent Skills. See [Agent Skills](../cli/skills.md) for more +details. diff --git a/docs/changelogs/index.md b/docs/changelogs/index.md index 98e290c30d..013ee3281c 100644 --- a/docs/changelogs/index.md +++ b/docs/changelogs/index.md @@ -18,6 +18,26 @@ on GitHub. | [Preview](preview.md) | Experimental features ready for early feedback. | | [Stable](latest.md) | Stable, recommended for general use. | +## Announcements: v0.28.0 - 2026-02-03 + +- **Slash Command:** We've added a new `/prompt-suggest` slash command to help + you generate prompt suggestions + ([#17264](https://github.com/google-gemini/gemini-cli/pull/17264) by + @NTaylorMullen). +- **IDE Support:** Gemini CLI now supports the Positron IDE + ([#15047](https://github.com/google-gemini/gemini-cli/pull/15047) by + @kapsner). +- **Customization:** You can now use custom themes in extensions, and we've + implemented automatic theme switching based on your terminal's background + ([#17327](https://github.com/google-gemini/gemini-cli/pull/17327) by + @spencer426, [#17976](https://github.com/google-gemini/gemini-cli/pull/17976) + by @Abhijit-2592). +- **Authentication:** We've added interactive and non-interactive consent for + OAuth, and you can now include your auth method in bug reports + ([#17699](https://github.com/google-gemini/gemini-cli/pull/17699) by + @ehedlund, [#17569](https://github.com/google-gemini/gemini-cli/pull/17569) by + @erikus). + ## Announcements: v0.27.0 - 2026-02-03 - **Event-Driven Architecture:** The CLI now uses a new event-driven scheduler diff --git a/docs/changelogs/latest.md b/docs/changelogs/latest.md index ce0a0fdfff..6ba7b88e1c 100644 --- a/docs/changelogs/latest.md +++ b/docs/changelogs/latest.md @@ -1,6 +1,6 @@ -# Latest stable release: v0.27.0 +# Latest stable release: v0.28.0 -Released: February 3, 2026 +Released: February 10, 2026 For most users, our latest stable release is the recommended release. Install the latest stable version with: @@ -11,437 +11,305 @@ npm install -g @google/gemini-cli ## Highlights -- **Event-Driven Architecture:** The CLI now uses an event-driven scheduler for - tool execution, improving performance and responsiveness. This includes - migrating non-interactive flows and sub-agents to the new scheduler. -- **Enhanced User Experience:** This release introduces several UI/UX - improvements, including queued tool confirmations and the ability to expand - and collapse large pasted text blocks. The `Settings` dialog has been improved - to reduce jitter and preserve focus. -- **Agent and Skill Improvements:** Agent Skills have been promoted to a stable - feature. Sub-agents now use a JSON schema for input and are tracked by an - `AgentRegistry`. -- **New `/rewind` Command:** A new `/rewind` command has been implemented to - allow users to go back in their session history. -- **Improved Shell and File Handling:** The shell tool's output format has been - optimized, and the CLI now gracefully handles disk-full errors during chat - recording. A bug in detecting already added paths has been fixed. -- **Linux Clipboard Support:** Image pasting capabilities for Wayland and X11 on - Linux have been added. +- **Commands & UX Enhancements:** Introduced `/prompt-suggest` command, + alongside updated undo/redo keybindings and automatic theme switching. +- **Expanded IDE Support:** Now offering compatibility with Positron IDE, + expanding integration options for developers. +- **Enhanced Security & Authentication:** Implemented interactive and + non-interactive OAuth consent, improving both security and diagnostic + capabilities for bug reports. +- **Advanced Planning & Agent Tools:** Integrated a generic Checklist component + for structured task management and evolved subagent capabilities with dynamic + policy registration. +- **Improved Core Stability & Reliability:** Resolved critical environment + loading, authentication, and session management issues, ensuring a more robust + experience. +- **Background Shell Commands:** Enabled the execution of shell commands in the + background for increased workflow efficiency. ## What's Changed -- remove fireAgent and beforeAgent hook by @ishaanxgupta in - [#16919](https://github.com/google-gemini/gemini-cli/pull/16919) -- Remove unused modelHooks and toolHooks by @ved015 in - [#17115](https://github.com/google-gemini/gemini-cli/pull/17115) -- feat(cli): sanitize ANSI escape sequences in non-interactive output by - @sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172) -- Update Attempt text to Retry when showing the retry happening to the … by - @sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178) -- chore(skills): update pr-creator skill workflow by @sehoon38 in - [#17180](https://github.com/google-gemini/gemini-cli/pull/17180) -- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in - [#17078](https://github.com/google-gemini/gemini-cli/pull/17078) -- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by +- feat(commands): add /prompt-suggest slash command by @NTaylorMullen in + [#17264](https://github.com/google-gemini/gemini-cli/pull/17264) +- feat(cli): align hooks enable/disable with skills and improve completion by + @sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822) +- docs: add CLI reference documentation by @leochiu-a in + [#17504](https://github.com/google-gemini/gemini-cli/pull/17504) +- chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by @gemini-cli-robot in - [#17181](https://github.com/google-gemini/gemini-cli/pull/17181) -- Remove other rewind reference in docs by @chrstnb in - [#17149](https://github.com/google-gemini/gemini-cli/pull/17149) -- feat(skills): add code-reviewer skill by @sehoon38 in - [#17187](https://github.com/google-gemini/gemini-cli/pull/17187) -- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in - [#17177](https://github.com/google-gemini/gemini-cli/pull/17177) -- feat(plan): refactor TestRig and eval helper to support configurable approval - modes by @jerop in - [#17171](https://github.com/google-gemini/gemini-cli/pull/17171) -- feat(workflows): support recursive workstream labeling and new IDs by - @bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207) -- Run evals for all models. by @gundermanc in - [#17123](https://github.com/google-gemini/gemini-cli/pull/17123) -- fix(github): improve label-workstream-rollup efficiency with GraphQL by - @bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217) -- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by - @g-samroberts in - [#17215](https://github.com/google-gemini/gemini-cli/pull/17215) -- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in - [#17204](https://github.com/google-gemini/gemini-cli/pull/17204) -- fix(github): improve label-workstream-rollup efficiency and fix bugs by - @bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219) -- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in - [#17192](https://github.com/google-gemini/gemini-cli/pull/17192) -- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in - [#17218](https://github.com/google-gemini/gemini-cli/pull/17218) -- feat(core): support dynamic variable substitution in system prompt override by - @NTaylorMullen in - [#17042](https://github.com/google-gemini/gemini-cli/pull/17042) -- fix(core,cli): enable recursive directory access for by @galz10 in - [#17094](https://github.com/google-gemini/gemini-cli/pull/17094) -- Docs: Marking for experimental features by @jkcinouye in - [#16760](https://github.com/google-gemini/gemini-cli/pull/16760) -- Support command/ctrl/alt backspace correctly by @scidomino in - [#17175](https://github.com/google-gemini/gemini-cli/pull/17175) -- feat(plan): add approval mode instructions to system prompt by @jerop in - [#17151](https://github.com/google-gemini/gemini-cli/pull/17151) -- feat(core): enable disableLLMCorrection by default by @SandyTao520 in - [#17223](https://github.com/google-gemini/gemini-cli/pull/17223) -- Remove unused slug from sidebar by @chrstnb in - [#17229](https://github.com/google-gemini/gemini-cli/pull/17229) -- drain stdin on exit by @scidomino in - [#17241](https://github.com/google-gemini/gemini-cli/pull/17241) -- refactor(cli): decouple UI from live tool execution via ToolActionsContext by + [#17725](https://github.com/google-gemini/gemini-cli/pull/17725) +- feat(skills): final stable promotion cleanup by @abhipatel12 in + [#17726](https://github.com/google-gemini/gemini-cli/pull/17726) +- test(core): mock fetch in OAuth transport fallback tests by @jw409 in + [#17059](https://github.com/google-gemini/gemini-cli/pull/17059) +- feat(cli): include auth method in /bug by @erikus in + [#17569](https://github.com/google-gemini/gemini-cli/pull/17569) +- Add a email privacy note to bug_report template by @nemyung in + [#17474](https://github.com/google-gemini/gemini-cli/pull/17474) +- Rewind documentation by @Adib234 in + [#17446](https://github.com/google-gemini/gemini-cli/pull/17446) +- fix: verify audio/video MIME types with content check by @maru0804 in + [#16907](https://github.com/google-gemini/gemini-cli/pull/16907) +- feat(core): add support for positron ide + ([#15045](https://github.com/google-gemini/gemini-cli/pull/15045)) by @kapsner + in [#15047](https://github.com/google-gemini/gemini-cli/pull/15047) +- /oncall dedup - wrap texts to nextlines by @sehoon38 in + [#17782](https://github.com/google-gemini/gemini-cli/pull/17782) +- fix(admin): rename advanced features admin setting by @skeshive in + [#17786](https://github.com/google-gemini/gemini-cli/pull/17786) +- [extension config] Make breaking optional value non-optional by @chrstnb in + [#17785](https://github.com/google-gemini/gemini-cli/pull/17785) +- Fix docs-writer skill issues by @g-samroberts in + [#17734](https://github.com/google-gemini/gemini-cli/pull/17734) +- fix(core): suppress duplicate hook failure warnings during streaming by @abhipatel12 in - [#17183](https://github.com/google-gemini/gemini-cli/pull/17183) -- fix(core): update token count and telemetry on /chat resume history load by - @psinha40898 in - [#16279](https://github.com/google-gemini/gemini-cli/pull/16279) -- fix: /policy to display policies according to mode by @ishaanxgupta in - [#16772](https://github.com/google-gemini/gemini-cli/pull/16772) -- fix(core): simplify replace tool error message by @SandyTao520 in - [#17246](https://github.com/google-gemini/gemini-cli/pull/17246) -- feat(cli): consolidate shell inactivity and redirection monitoring by - @NTaylorMullen in - [#17086](https://github.com/google-gemini/gemini-cli/pull/17086) -- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by + [#17727](https://github.com/google-gemini/gemini-cli/pull/17727) +- test: add more tests for AskUser by @jackwotherspoon in + [#17720](https://github.com/google-gemini/gemini-cli/pull/17720) +- feat(cli): enable activity logging for non-interactive mode and evals by + @SandyTao520 in + [#17703](https://github.com/google-gemini/gemini-cli/pull/17703) +- feat(core): add support for custom deny messages in policy rules by + @allenhutchison in + [#17427](https://github.com/google-gemini/gemini-cli/pull/17427) +- Fix unintended credential exposure to MCP Servers by @Adib234 in + [#17311](https://github.com/google-gemini/gemini-cli/pull/17311) +- feat(extensions): add support for custom themes in extensions by @spencer426 + in [#17327](https://github.com/google-gemini/gemini-cli/pull/17327) +- fix: persist and restore workspace directories on session resume by + @korade-krushna in + [#17454](https://github.com/google-gemini/gemini-cli/pull/17454) +- Update release notes pages for 0.26.0 and 0.27.0-preview. by @g-samroberts in + [#17744](https://github.com/google-gemini/gemini-cli/pull/17744) +- feat(ux): update cell border color and created test file for table rendering + by @devr0306 in + [#17798](https://github.com/google-gemini/gemini-cli/pull/17798) +- Change height for the ToolConfirmationQueue. by @jacob314 in + [#17799](https://github.com/google-gemini/gemini-cli/pull/17799) +- feat(cli): add user identity info to stats command by @sehoon38 in + [#17612](https://github.com/google-gemini/gemini-cli/pull/17612) +- fix(ux): fixed off-by-some wrapping caused by fixed-width characters by + @devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816) +- feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and + Shift+Cmd+Z/Shift+Alt+Z by @scidomino in + [#17800](https://github.com/google-gemini/gemini-cli/pull/17800) +- fix(evals): use absolute path for activity log directory by @SandyTao520 in + [#17830](https://github.com/google-gemini/gemini-cli/pull/17830) +- test: add integration test to verify stdout/stderr routing by @ved015 in + [#17280](https://github.com/google-gemini/gemini-cli/pull/17280) +- fix(cli): list installed extensions when update target missing by @tt-a1i in + [#17082](https://github.com/google-gemini/gemini-cli/pull/17082) +- fix(cli): handle PAT tokens and credentials in git remote URL parsing by + @afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650) +- fix(core): use returnDisplay for error result display by @Nubebuster in + [#14994](https://github.com/google-gemini/gemini-cli/pull/14994) +- Fix detection of bun as package manager by @Randomblock1 in + [#17462](https://github.com/google-gemini/gemini-cli/pull/17462) +- feat(cli): show hooksConfig.enabled in settings dialog by @abhipatel12 in + [#17810](https://github.com/google-gemini/gemini-cli/pull/17810) +- feat(cli): Display user identity (auth, email, tier) on startup by @yunaseoul + in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591) +- fix: prevent ghost border for AskUserDialog by @jackwotherspoon in + [#17788](https://github.com/google-gemini/gemini-cli/pull/17788) +- docs: mark A2A subagents as experimental in subagents.md by @adamfweidman in + [#17863](https://github.com/google-gemini/gemini-cli/pull/17863) +- Resolve error thrown for sensitive values by @chrstnb in + [#17826](https://github.com/google-gemini/gemini-cli/pull/17826) +- fix(admin): Rename secureModeEnabled to strictModeDisabled by @skeshive in + [#17789](https://github.com/google-gemini/gemini-cli/pull/17789) +- feat(ux): update truncate dots to be shorter in tables by @devr0306 in + [#17825](https://github.com/google-gemini/gemini-cli/pull/17825) +- fix(core): resolve DEP0040 punycode deprecation via patch-package by + @ATHARVA262005 in + [#17692](https://github.com/google-gemini/gemini-cli/pull/17692) +- feat(plan): create generic Checklist component and refactor Todo by @Adib234 + in [#17741](https://github.com/google-gemini/gemini-cli/pull/17741) +- Cleanup post delegate_to_agent removal by @gundermanc in + [#17875](https://github.com/google-gemini/gemini-cli/pull/17875) +- fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration - + Fixes [#17877](https://github.com/google-gemini/gemini-cli/pull/17877) by + @cocosheng-g in + [#17803](https://github.com/google-gemini/gemini-cli/pull/17803) +- Disable mouse tracking e2e by @alisa-alisa in + [#17880](https://github.com/google-gemini/gemini-cli/pull/17880) +- fix(cli): use correct setting key for Cloud Shell auth by @sehoon38 in + [#17884](https://github.com/google-gemini/gemini-cli/pull/17884) +- chore: revert IDE specific ASCII logo by @jackwotherspoon in + [#17887](https://github.com/google-gemini/gemini-cli/pull/17887) +- Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by + @sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898) +- Refactoring of disabling of mouse tracking in e2e tests by @alisa-alisa in + [#17902](https://github.com/google-gemini/gemini-cli/pull/17902) +- feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by + @deyim in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177) +- feat(core): Isolate and cleanup truncated tool outputs by @SandyTao520 in + [#17594](https://github.com/google-gemini/gemini-cli/pull/17594) +- Create skills page, update commands, refine docs by @g-samroberts in + [#17842](https://github.com/google-gemini/gemini-cli/pull/17842) +- feat: preserve EOL in files by @Thomas-Shephard in + [#16087](https://github.com/google-gemini/gemini-cli/pull/16087) +- Fix HalfLinePaddedBox in screenreader mode. by @jacob314 in + [#17914](https://github.com/google-gemini/gemini-cli/pull/17914) +- bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X + in vim mode. by @jacob314 in + [#17938](https://github.com/google-gemini/gemini-cli/pull/17938) +- feat(core): implement interactive and non-interactive consent for OAuth by + @ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699) +- perf(core): optimize token calculation and add support for multimodal tool + responses by @abhipatel12 in + [#17835](https://github.com/google-gemini/gemini-cli/pull/17835) +- refactor(hooks): remove legacy tools.enableHooks setting by @abhipatel12 in + [#17867](https://github.com/google-gemini/gemini-cli/pull/17867) +- feat(ci): add npx smoke test to verify installability by @bdmorgan in + [#17927](https://github.com/google-gemini/gemini-cli/pull/17927) +- feat(core): implement dynamic policy registration for subagents by @abhipatel12 in - [#17227](https://github.com/google-gemini/gemini-cli/pull/17227) -- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in - [#17211](https://github.com/google-gemini/gemini-cli/pull/17211) -- feat(hooks): enable hooks system by default by @abhipatel12 in - [#17247](https://github.com/google-gemini/gemini-cli/pull/17247) -- feat(core): Enable AgentRegistry to track all discovered subagents by + [#17838](https://github.com/google-gemini/gemini-cli/pull/17838) +- feat: Implement background shell commands by @galz10 in + [#14849](https://github.com/google-gemini/gemini-cli/pull/14849) +- feat(admin): provide actionable error messages for disabled features by + @skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815) +- Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by + @jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940) +- Fix broken link in docs by @chrstnb in + [#17959](https://github.com/google-gemini/gemini-cli/pull/17959) +- feat(plan): reuse standard tool confirmation for AskUser tool by @jerop in + [#17864](https://github.com/google-gemini/gemini-cli/pull/17864) +- feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by + @lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942) +- run npx pointing to the specific commit SHA by @sehoon38 in + [#17970](https://github.com/google-gemini/gemini-cli/pull/17970) +- Add allowedExtensions setting by @kevinjwang1 in + [#17695](https://github.com/google-gemini/gemini-cli/pull/17695) +- feat(plan): refactor ToolConfirmationPayload to union type by @jerop in + [#17980](https://github.com/google-gemini/gemini-cli/pull/17980) +- lower the default max retries to reduce contention by @sehoon38 in + [#17975](https://github.com/google-gemini/gemini-cli/pull/17975) +- fix(core): ensure YOLO mode auto-approves complex shell commands when parsing + fails by @abhipatel12 in + [#17920](https://github.com/google-gemini/gemini-cli/pull/17920) +- Fix broken link. by @g-samroberts in + [#17972](https://github.com/google-gemini/gemini-cli/pull/17972) +- Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over + AppContainer for input handling. by @jacob314 in + [#17993](https://github.com/google-gemini/gemini-cli/pull/17993) +- Fix truncation for AskQuestion by @jacob314 in + [#18001](https://github.com/google-gemini/gemini-cli/pull/18001) +- fix(workflow): update maintainer check logic to be inclusive and + case-insensitive by @bdmorgan in + [#18009](https://github.com/google-gemini/gemini-cli/pull/18009) +- Fix Esc cancel during streaming by @LyalinDotCom in + [#18039](https://github.com/google-gemini/gemini-cli/pull/18039) +- feat(acp): add session resume support by @bdmorgan in + [#18043](https://github.com/google-gemini/gemini-cli/pull/18043) +- fix(ci): prevent stale PR closer from incorrectly closing new PRs by @bdmorgan + in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069) +- chore: delete autoAccept setting unused in production by @victorvianna in + [#17862](https://github.com/google-gemini/gemini-cli/pull/17862) +- feat(plan): use placeholder for choice question "Other" option by @jerop in + [#18101](https://github.com/google-gemini/gemini-cli/pull/18101) +- docs: update clearContext to hookSpecificOutput by @jackwotherspoon in + [#18024](https://github.com/google-gemini/gemini-cli/pull/18024) +- docs-writer skill: Update docs writer skill by @jkcinouye in + [#17928](https://github.com/google-gemini/gemini-cli/pull/17928) +- Sehoon/oncall filter by @sehoon38 in + [#18105](https://github.com/google-gemini/gemini-cli/pull/18105) +- feat(core): add setting to disable loop detection by @SandyTao520 in + [#18008](https://github.com/google-gemini/gemini-cli/pull/18008) +- Docs: Revise docs/index.md by @jkcinouye in + [#17879](https://github.com/google-gemini/gemini-cli/pull/17879) +- Fix up/down arrow regression and add test. by @jacob314 in + [#18108](https://github.com/google-gemini/gemini-cli/pull/18108) +- fix(ui): prevent content leak in MaxSizedBox bottom overflow by @jerop in + [#17991](https://github.com/google-gemini/gemini-cli/pull/17991) +- refactor: migrate checks.ts utility to core and deduplicate by @jerop in + [#18139](https://github.com/google-gemini/gemini-cli/pull/18139) +- feat(core): implement tool name aliasing for backward compatibility by @SandyTao520 in - [#17253](https://github.com/google-gemini/gemini-cli/pull/17253) -- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in - [#17152](https://github.com/google-gemini/gemini-cli/pull/17152) -- feat: replace large text pastes with [Pasted Text: X lines] placeholder by - @jackwotherspoon in - [#16422](https://github.com/google-gemini/gemini-cli/pull/16422) -- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul - in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237) -- Enable the ability to queue specific nightly eval tests by @gundermanc in - [#17262](https://github.com/google-gemini/gemini-cli/pull/17262) -- docs(hooks): comprehensive update of hook documentation and specs by - @abhipatel12 in - [#16816](https://github.com/google-gemini/gemini-cli/pull/16816) -- refactor: improve large text paste placeholder by @jacob314 in - [#17269](https://github.com/google-gemini/gemini-cli/pull/17269) -- feat: implement /rewind command by @Adib234 in - [#15720](https://github.com/google-gemini/gemini-cli/pull/15720) -- Feature/jetbrains ide detection by @SoLoHiC in - [#16243](https://github.com/google-gemini/gemini-cli/pull/16243) -- docs: update typo in mcp-server.md file by @schifferl in - [#17099](https://github.com/google-gemini/gemini-cli/pull/17099) -- Sanitize command names and descriptions by @ehedlund in - [#17228](https://github.com/google-gemini/gemini-cli/pull/17228) -- fix(auth): don't crash when initial auth fails by @skeshive in - [#17308](https://github.com/google-gemini/gemini-cli/pull/17308) -- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in - [#17144](https://github.com/google-gemini/gemini-cli/pull/17144) -- feat: add AskUser tool schema by @jackwotherspoon in - [#16988](https://github.com/google-gemini/gemini-cli/pull/16988) -- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in - [#16256](https://github.com/google-gemini/gemini-cli/pull/16256) -- fix: show whitespace changes in edit tool diffs by @Ujjiyara in - [#17213](https://github.com/google-gemini/gemini-cli/pull/17213) -- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool() - now handles the caching internally by @jacob314 in - [#17320](https://github.com/google-gemini/gemini-cli/pull/17320) -- ci: allow failure in evals-nightly run step by @gundermanc in - [#17319](https://github.com/google-gemini/gemini-cli/pull/17319) -- feat(cli): Add state management and plumbing for agent configuration dialog by - @SandyTao520 in - [#17259](https://github.com/google-gemini/gemini-cli/pull/17259) -- bug: fix ide-client connection to ide-companion when inside docker via - ssh/devcontainer by @kapsner in - [#15049](https://github.com/google-gemini/gemini-cli/pull/15049) -- Emit correct newline type return by @scidomino in - [#17331](https://github.com/google-gemini/gemini-cli/pull/17331) -- New skill: docs-writer by @g-samroberts in - [#17268](https://github.com/google-gemini/gemini-cli/pull/17268) -- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by - @spencer426 in - [#16735](https://github.com/google-gemini/gemini-cli/pull/16735) -- Disable tips after 10 runs by @Adib234 in - [#17101](https://github.com/google-gemini/gemini-cli/pull/17101) -- Fix so rewind starts at the bottom and loadHistory refreshes static content. - by @jacob314 in - [#17335](https://github.com/google-gemini/gemini-cli/pull/17335) -- feat(core): Remove legacy settings. by @joshualitt in - [#17244](https://github.com/google-gemini/gemini-cli/pull/17244) -- feat(plan): add 'communicate' tool kind by @jerop in - [#17341](https://github.com/google-gemini/gemini-cli/pull/17341) -- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by - @mattKorwel in - [#16041](https://github.com/google-gemini/gemini-cli/pull/16041) -- feat(plan): update UI Theme for Plan Mode by @Adib234 in - [#17243](https://github.com/google-gemini/gemini-cli/pull/17243) -- fix(ui): stabilize rendering during terminal resize in alternate buffer by - @lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783) -- feat(cli): add /agents config command and improve agent discovery by - @SandyTao520 in - [#17342](https://github.com/google-gemini/gemini-cli/pull/17342) -- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb - in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299) -- fix(cli)!: Default to interactive mode for positional arguments by - @ishaanxgupta in - [#16329](https://github.com/google-gemini/gemini-cli/pull/16329) -- Fix issue #17080 by @jacob314 in - [#17100](https://github.com/google-gemini/gemini-cli/pull/17100) -- feat(core): Refresh agents after loading an extension. by @joshualitt in - [#17355](https://github.com/google-gemini/gemini-cli/pull/17355) -- fix(cli): include source in policy rule display by @allenhutchison in - [#17358](https://github.com/google-gemini/gemini-cli/pull/17358) -- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by + [#17974](https://github.com/google-gemini/gemini-cli/pull/17974) +- docs: fix help-wanted label spelling by @pavan-sh in + [#18114](https://github.com/google-gemini/gemini-cli/pull/18114) +- feat(cli): implement automatic theme switching based on terminal background by + @Abhijit-2592 in + [#17976](https://github.com/google-gemini/gemini-cli/pull/17976) +- fix(ide): no-op refactoring that moves the connection logic to helper + functions by @skeshive in + [#18118](https://github.com/google-gemini/gemini-cli/pull/18118) +- feat: update review-frontend-and-fix slash command to review-and-fix by + @galz10 in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146) +- fix: improve Ctrl+R reverse search by @jackwotherspoon in + [#18075](https://github.com/google-gemini/gemini-cli/pull/18075) +- feat(plan): handle inconsistency in schedulers by @Adib234 in + [#17813](https://github.com/google-gemini/gemini-cli/pull/17813) +- feat(plan): add core logic and exit_plan_mode tool definition by @jerop in + [#18110](https://github.com/google-gemini/gemini-cli/pull/18110) +- feat(core): rename search_file_content tool to grep_search and add legacy + alias by @SandyTao520 in + [#18003](https://github.com/google-gemini/gemini-cli/pull/18003) +- fix(core): prioritize detailed error messages for code assist setup by @gsquared94 in - [#17236](https://github.com/google-gemini/gemini-cli/pull/17236) -- Refactor subagent delegation to be one tool per agent by @gundermanc in - [#17346](https://github.com/google-gemini/gemini-cli/pull/17346) -- fix(core): Include MCP server name in OAuth message by @jerop in - [#17351](https://github.com/google-gemini/gemini-cli/pull/17351) -- Fix pr-triage.sh script to update pull requests with tags "help wanted" and - "maintainer only" by @jacob314 in - [#17324](https://github.com/google-gemini/gemini-cli/pull/17324) -- feat(plan): implement simple workflow for planning in main agent by @jerop in - [#17326](https://github.com/google-gemini/gemini-cli/pull/17326) -- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in - [#16967](https://github.com/google-gemini/gemini-cli/pull/16967) -- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by - @medic-code in - [#12038](https://github.com/google-gemini/gemini-cli/pull/12038) -- fix(core): add alternative command names for Antigravity editor detec… by - @baeseokjae in - [#16829](https://github.com/google-gemini/gemini-cli/pull/16829) -- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in - [#15737](https://github.com/google-gemini/gemini-cli/pull/15737) -- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in - [#17390](https://github.com/google-gemini/gemini-cli/pull/17390) -- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in - [#16714](https://github.com/google-gemini/gemini-cli/pull/16714) -- fix(core): gracefully handle disk full errors in chat recording by - @godwiniheuwa in - [#17305](https://github.com/google-gemini/gemini-cli/pull/17305) -- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in - [#17388](https://github.com/google-gemini/gemini-cli/pull/17388) -- fix(core): use RFC 9728 compliant path-based OAuth protected resource - discovery by @vrv in - [#15756](https://github.com/google-gemini/gemini-cli/pull/15756) -- Update Code Wiki README badge by @PatoBeltran in - [#15229](https://github.com/google-gemini/gemini-cli/pull/15229) -- Add conda installation instructions for Gemini CLI by @ishaanxgupta in - [#16921](https://github.com/google-gemini/gemini-cli/pull/16921) -- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in - [#17369](https://github.com/google-gemini/gemini-cli/pull/17369) -- fix(cli): preserve input text when declining tool approval (#15624) by - @ManojINaik in - [#15659](https://github.com/google-gemini/gemini-cli/pull/15659) -- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in - [#17403](https://github.com/google-gemini/gemini-cli/pull/17403) -- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon - in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344) -- feat(ui): display user tier in about command by @sehoon38 in - [#17400](https://github.com/google-gemini/gemini-cli/pull/17400) -- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in - [#16574](https://github.com/google-gemini/gemini-cli/pull/16574) -- fix(cli): change image paste location to global temp directory (#17396) by - @devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396) -- Fix line endings issue with Notice file by @scidomino in - [#17417](https://github.com/google-gemini/gemini-cli/pull/17417) -- feat(plan): implement persistent approvalMode setting by @Adib234 in - [#17350](https://github.com/google-gemini/gemini-cli/pull/17350) -- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in - [#17404](https://github.com/google-gemini/gemini-cli/pull/17404) -- Allow prompt queueing during MCP initialization by @Adib234 in - [#17395](https://github.com/google-gemini/gemini-cli/pull/17395) -- feat: implement AgentConfigDialog for /agents config command by @SandyTao520 - in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370) -- fix(agents): default to all tools when tool list is omitted in subagents by - @gundermanc in - [#17422](https://github.com/google-gemini/gemini-cli/pull/17422) -- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in - [#17276](https://github.com/google-gemini/gemini-cli/pull/17276) -- fix(core): hide user tier name by @sehoon38 in - [#17418](https://github.com/google-gemini/gemini-cli/pull/17418) -- feat: Enforce unified folder trust for /directory add by @galz10 in - [#17359](https://github.com/google-gemini/gemini-cli/pull/17359) -- migrate fireToolNotificationHook to hookSystem by @ved015 in - [#17398](https://github.com/google-gemini/gemini-cli/pull/17398) -- Clean up dead code by @scidomino in - [#17443](https://github.com/google-gemini/gemini-cli/pull/17443) -- feat(workflow): add stale pull request closer with linked-issue enforcement by - @bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449) -- feat(workflow): expand stale-exempt labels to include help wanted and Public - Roadmap by @bdmorgan in - [#17459](https://github.com/google-gemini/gemini-cli/pull/17459) -- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in - [#17460](https://github.com/google-gemini/gemini-cli/pull/17460) -- Resolves the confusing error message `ripgrep exited with code null that - occurs when a search operation is cancelled or aborted by @maximmasiutin in - [#14267](https://github.com/google-gemini/gemini-cli/pull/14267) -- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in - [#15254](https://github.com/google-gemini/gemini-cli/pull/15254) -- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by - @breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412) -- docs(hooks): clarify mandatory 'type' field and update hook schema - documentation by @abhipatel12 in - [#17499](https://github.com/google-gemini/gemini-cli/pull/17499) -- Improve error messages on failed onboarding by @gsquared94 in - [#17357](https://github.com/google-gemini/gemini-cli/pull/17357) -- Follow up to "enableInteractiveShell for external tooling relying on a2a - server" by @DavidAPierce in - [#17130](https://github.com/google-gemini/gemini-cli/pull/17130) -- Fix/issue 17070 by @alih552 in - [#17242](https://github.com/google-gemini/gemini-cli/pull/17242) -- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin - in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476) -- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in - [#17470](https://github.com/google-gemini/gemini-cli/pull/17470) -- feat(core): optimize shell tool llmContent output format by @SandyTao520 in - [#17538](https://github.com/google-gemini/gemini-cli/pull/17538) -- Fix bug in detecting already added paths. by @jacob314 in - [#17430](https://github.com/google-gemini/gemini-cli/pull/17430) -- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs - by @abhipatel12 in - [#17429](https://github.com/google-gemini/gemini-cli/pull/17429) -- feat(agents): implement first-run experience for project-level sub-agents by - @gundermanc in - [#17266](https://github.com/google-gemini/gemini-cli/pull/17266) -- Update extensions docs by @chrstnb in - [#16093](https://github.com/google-gemini/gemini-cli/pull/16093) -- Docs: Refactor left nav on the website by @jkcinouye in - [#17558](https://github.com/google-gemini/gemini-cli/pull/17558) -- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in - [#17146](https://github.com/google-gemini/gemini-cli/pull/17146) -- feat(plan): add persistent plan file storage by @jerop in - [#17563](https://github.com/google-gemini/gemini-cli/pull/17563) -- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in - [#17567](https://github.com/google-gemini/gemini-cli/pull/17567) -- Fix extensions config error by @chrstnb in - [#17580](https://github.com/google-gemini/gemini-cli/pull/17580) -- fix(plan): remove subagent invocation from plan mode by @jerop in - [#17593](https://github.com/google-gemini/gemini-cli/pull/17593) -- feat(ui): add solid background color option for input prompt by @jacob314 in - [#16563](https://github.com/google-gemini/gemini-cli/pull/16563) -- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by - @jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585) -- feat(cli): add global setting to disable UI spinners by @galz10 in - [#17234](https://github.com/google-gemini/gemini-cli/pull/17234) -- fix(security): enforce strict policy directory permissions by @yunaseoul in - [#17353](https://github.com/google-gemini/gemini-cli/pull/17353) -- test(core): fix tests in windows by @scidomino in - [#17592](https://github.com/google-gemini/gemini-cli/pull/17592) -- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers - included in an extension( Issue #11057 & #17402) by @jasmeetsb in - [#17434](https://github.com/google-gemini/gemini-cli/pull/17434) -- Always map mac keys, even on other platforms by @scidomino in - [#17618](https://github.com/google-gemini/gemini-cli/pull/17618) -- Ctrl-O by @jacob314 in - [#17617](https://github.com/google-gemini/gemini-cli/pull/17617) -- feat(plan): update cycling order of approval modes by @Adib234 in - [#17622](https://github.com/google-gemini/gemini-cli/pull/17622) -- fix(cli): restore 'Modify with editor' option in external terminals by - @abhipatel12 in - [#17621](https://github.com/google-gemini/gemini-cli/pull/17621) -- Slash command for helping in debugging by @gundermanc in - [#17609](https://github.com/google-gemini/gemini-cli/pull/17609) -- feat: add double-click to expand/collapse large paste placeholders by - @jackwotherspoon in - [#17471](https://github.com/google-gemini/gemini-cli/pull/17471) -- refactor(cli): migrate non-interactive flow to event-driven scheduler by - @abhipatel12 in - [#17572](https://github.com/google-gemini/gemini-cli/pull/17572) -- fix: loadcodeassist eligible tiers getting ignored for unlicensed users - (regression) by @gsquared94 in - [#17581](https://github.com/google-gemini/gemini-cli/pull/17581) -- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in - [#17573](https://github.com/google-gemini/gemini-cli/pull/17573) -- feat(core): enforce server prefixes for MCP tools in agent definitions by - @abhipatel12 in - [#17574](https://github.com/google-gemini/gemini-cli/pull/17574) -- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in - [#14863](https://github.com/google-gemini/gemini-cli/pull/14863) -- feat(ui): pretty JSON rendering tool outputs by @medic-code in - [#9767](https://github.com/google-gemini/gemini-cli/pull/9767) -- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in - [#17634](https://github.com/google-gemini/gemini-cli/pull/17634) -- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in - [#14479](https://github.com/google-gemini/gemini-cli/pull/14479) -- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in - [#17293](https://github.com/google-gemini/gemini-cli/pull/17293) -- paste transform followup by @jacob314 in - [#17624](https://github.com/google-gemini/gemini-cli/pull/17624) -- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in - [#14997](https://github.com/google-gemini/gemini-cli/pull/14997) -- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon - in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663) -- feat(cli): add oncall command for issue triage by @sehoon38 in - [#17661](https://github.com/google-gemini/gemini-cli/pull/17661) -- Fix sidebar issue for extensions link by @chrstnb in - [#17668](https://github.com/google-gemini/gemini-cli/pull/17668) -- Change formatting to prevent UI redressing attacks by @scidomino in - [#17611](https://github.com/google-gemini/gemini-cli/pull/17611) -- Fix cluster of bugs in the settings dialog. by @jacob314 in - [#17628](https://github.com/google-gemini/gemini-cli/pull/17628) -- Update sidebar to resolve site build issues by @chrstnb in - [#17674](https://github.com/google-gemini/gemini-cli/pull/17674) -- fix(admin): fix a few bugs related to admin controls by @skeshive in - [#17590](https://github.com/google-gemini/gemini-cli/pull/17590) -- revert bad changes to tests by @scidomino in - [#17673](https://github.com/google-gemini/gemini-cli/pull/17673) -- feat(cli): show candidate issue state reason and duplicate status in triage by - @sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676) -- Fix missing slash commands when Gemini CLI is in a project with a package.json - that doesn't follow semantic versioning by @Adib234 in - [#17561](https://github.com/google-gemini/gemini-cli/pull/17561) -- feat(core): Model family-specific system prompts by @joshualitt in - [#17614](https://github.com/google-gemini/gemini-cli/pull/17614) -- Sub-agents documentation. by @gundermanc in - [#16639](https://github.com/google-gemini/gemini-cli/pull/16639) -- feat: wire up AskUserTool with dialog by @jackwotherspoon in - [#17411](https://github.com/google-gemini/gemini-cli/pull/17411) -- Load extension settings for hooks, agents, skills by @chrstnb in - [#17245](https://github.com/google-gemini/gemini-cli/pull/17245) -- Fix issue where Gemini CLI can make changes when simply asked a question by - @gundermanc in - [#17608](https://github.com/google-gemini/gemini-cli/pull/17608) -- Update docs-writer skill for editing and add style guide for reference. by - @g-samroberts in - [#17669](https://github.com/google-gemini/gemini-cli/pull/17669) -- fix(ux): have user message display a short path for pasted images by @devr0306 - in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613) -- feat(plan): enable AskUser tool in Plan mode for clarifying questions by - @jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694) -- GEMINI.md polish by @jacob314 in - [#17680](https://github.com/google-gemini/gemini-cli/pull/17680) -- refactor(core): centralize path validation and allow temp dir access for tools - by @NTaylorMullen in - [#17185](https://github.com/google-gemini/gemini-cli/pull/17185) -- feat(skills): promote Agent Skills to stable by @abhipatel12 in - [#17693](https://github.com/google-gemini/gemini-cli/pull/17693) -- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in - [#17414](https://github.com/google-gemini/gemini-cli/pull/17414) -- docs: Add Experimental Remote Agent Docs by @adamfweidman in - [#17697](https://github.com/google-gemini/gemini-cli/pull/17697) -- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in - [#17712](https://github.com/google-gemini/gemini-cli/pull/17712) -- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer - mode. by @jacob314 in - [#17640](https://github.com/google-gemini/gemini-cli/pull/17640) -- feat(skills): promote skills settings to stable by @abhipatel12 in - [#17713](https://github.com/google-gemini/gemini-cli/pull/17713) -- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in - [#17701](https://github.com/google-gemini/gemini-cli/pull/17701) -- feat(ui): add terminal cursor support by @jacob314 in - [#17711](https://github.com/google-gemini/gemini-cli/pull/17711) -- docs(skills): remove experimental labels and update tutorials by @abhipatel12 - in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714) -- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in - [#17660](https://github.com/google-gemini/gemini-cli/pull/17660) -- Add support for an additional exclusion file besides .gitignore and - .geminiignore by @alisa-alisa in - [#16487](https://github.com/google-gemini/gemini-cli/pull/16487) -- feat: add review-frontend-and-fix command by @galz10 in - [#17707](https://github.com/google-gemini/gemini-cli/pull/17707) + [#17852](https://github.com/google-gemini/gemini-cli/pull/17852) +- fix(cli): resolve environment loading and auth validation issues in ACP mode + by @bdmorgan in + [#18025](https://github.com/google-gemini/gemini-cli/pull/18025) +- feat(core): add .agents/skills directory alias for skill discovery by + @NTaylorMullen in + [#18151](https://github.com/google-gemini/gemini-cli/pull/18151) +- chore(core): reassign telemetry keys to avoid server conflict by @mattKorwel + in [#18161](https://github.com/google-gemini/gemini-cli/pull/18161) +- Add link to rewind doc in commands.md by @Adib234 in + [#17961](https://github.com/google-gemini/gemini-cli/pull/17961) +- feat(core): add draft-2020-12 JSON Schema support with lenient fallback by + @afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060) +- refactor(core): robust trimPreservingTrailingNewline and regression test by + @adamfweidman in + [#18196](https://github.com/google-gemini/gemini-cli/pull/18196) +- Remove MCP servers on extension uninstall by @chrstnb in + [#18121](https://github.com/google-gemini/gemini-cli/pull/18121) +- refactor: localize ACP error parsing logic to cli package by @bdmorgan in + [#18193](https://github.com/google-gemini/gemini-cli/pull/18193) +- feat(core): Add A2A auth config types by @adamfweidman in + [#18205](https://github.com/google-gemini/gemini-cli/pull/18205) +- Set default max attempts to 3 and use the common variable by @sehoon38 in + [#18209](https://github.com/google-gemini/gemini-cli/pull/18209) +- feat(plan): add exit_plan_mode ui and prompt by @jerop in + [#18162](https://github.com/google-gemini/gemini-cli/pull/18162) +- fix(test): improve test isolation and enable subagent evaluations by + @cocosheng-g in + [#18138](https://github.com/google-gemini/gemini-cli/pull/18138) +- feat(plan): use custom deny messages in plan mode policies by @Adib234 in + [#18195](https://github.com/google-gemini/gemini-cli/pull/18195) +- Match on extension ID when stopping extensions by @chrstnb in + [#18218](https://github.com/google-gemini/gemini-cli/pull/18218) +- fix(core): Respect user's .gitignore preference by @xyrolle in + [#15482](https://github.com/google-gemini/gemini-cli/pull/15482) +- docs: document GEMINI_CLI_HOME environment variable by @adamfweidman in + [#18219](https://github.com/google-gemini/gemini-cli/pull/18219) +- chore(core): explicitly state plan storage path in prompt by @jerop in + [#18222](https://github.com/google-gemini/gemini-cli/pull/18222) +- A2a admin setting by @DavidAPierce in + [#17868](https://github.com/google-gemini/gemini-cli/pull/17868) +- feat(a2a): Add pluggable auth provider infrastructure by @adamfweidman in + [#17934](https://github.com/google-gemini/gemini-cli/pull/17934) +- Fix handling of empty settings by @chrstnb in + [#18131](https://github.com/google-gemini/gemini-cli/pull/18131) +- Reload skills when extensions change by @chrstnb in + [#18225](https://github.com/google-gemini/gemini-cli/pull/18225) +- feat: Add markdown rendering to ask_user tool by @jackwotherspoon in + [#18211](https://github.com/google-gemini/gemini-cli/pull/18211) +- Add telemetry to rewind by @Adib234 in + [#18122](https://github.com/google-gemini/gemini-cli/pull/18122) +- feat(admin): add support for MCP configuration via admin controls (pt1) by + @skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223) +- feat(core): require user consent before MCP server OAuth by @ehedlund in + [#18132](https://github.com/google-gemini/gemini-cli/pull/18132) +- fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars + by @skeshive in + [#18231](https://github.com/google-gemini/gemini-cli/pull/18231) +- feat(ui): move user identity display to header by @sehoon38 in + [#18216](https://github.com/google-gemini/gemini-cli/pull/18216) +- fix: enforce folder trust for workspace settings, skills, and context by + @galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596) -**Full changelog**: -https://github.com/google-gemini/gemini-cli/compare/v0.26.0...v0.27.0 +**Full Changelog**: +https://github.com/google-gemini/gemini-cli/compare/v0.27.0...v0.28.0 diff --git a/docs/changelogs/preview.md b/docs/changelogs/preview.md index 93ed5a2a9c..cab75c4446 100644 --- a/docs/changelogs/preview.md +++ b/docs/changelogs/preview.md @@ -1,6 +1,6 @@ -# Preview release: Release v0.28.0-preview.0 +# Preview release: Release v0.29.0-preview.0 -Released: February 3, 2026 +Released: February 10, 2026 Our preview release includes the latest, new, and experimental features. This release may not be as stable as our [latest weekly release](latest.md). @@ -13,295 +13,355 @@ npm install -g @google/gemini-cli@preview ## Highlights -- **Improved Hooks Management:** Hooks enable/disable functionality now aligns - with skills and offers improved completion. -- **Custom Themes for Extensions:** Extensions can now support custom themes, - allowing for greater personalization. -- **User Identity Display:** User identity information (auth, email, tier) is - now displayed on startup and in the `stats` command. -- **Plan Mode Enhancements:** Plan mode has been improved with a generic - `Checklist` component and refactored `Todo`. -- **Background Shell Commands:** Implementation of background shell commands. +- **Plan Mode Enhancements**: Significant updates to Plan Mode, including new + commands, support for MCP servers, integration of planning artifacts, and + improved iteration guidance. +- **Core Agent Improvements**: Enhancements to the core agent, including better + system prompt rigor, improved subagent definitions, and enhanced tool + execution limits. +- **CLI UX/UI Updates**: Various UI and UX improvements, such as autocomplete in + the input prompt, updated approval mode labels, DevTools integration, and + improved header spacing. +- **Tooling & Extension Updates**: Improvements to existing tools like + `ask_user` and `grep_search`, and new features for extension management. +- **Bug Fixes**: Numerous bug fixes across the CLI and core, addressing issues + with interactive commands, memory leaks, permission checks, and more. +- **Context and Tool Output Management**: Features for observation masking for + tool outputs, session-linked tool output storage, and persistence for masked + tool outputs. ## What's Changed -- feat(commands): add /prompt-suggest slash command by NTaylorMullen in - [#17264](https://github.com/google-gemini/gemini-cli/pull/17264) -- feat(cli): align hooks enable/disable with skills and improve completion by - sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822) -- docs: add CLI reference documentation by leochiu-a in - [#17504](https://github.com/google-gemini/gemini-cli/pull/17504) -- chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by +- fix: remove ask_user tool from non-interactive modes by jackwotherspoon in + [#18154](https://github.com/google-gemini/gemini-cli/pull/18154) +- fix(cli): allow restricted .env loading in untrusted sandboxed folders by + galz10 in [#17806](https://github.com/google-gemini/gemini-cli/pull/17806) +- Encourage agent to utilize ecosystem tools to perform work by gundermanc in + [#17881](https://github.com/google-gemini/gemini-cli/pull/17881) +- feat(plan): unify workflow location in system prompt to optimize caching by + jerop in [#18258](https://github.com/google-gemini/gemini-cli/pull/18258) +- feat(core): enable getUserTierName in config by sehoon38 in + [#18265](https://github.com/google-gemini/gemini-cli/pull/18265) +- feat(core): add default execution limits for subagents by abhipatel12 in + [#18274](https://github.com/google-gemini/gemini-cli/pull/18274) +- Fix issue where agent gets stuck at interactive commands. by gundermanc in + [#18272](https://github.com/google-gemini/gemini-cli/pull/18272) +- chore(release): bump version to 0.29.0-nightly.20260203.71f46f116 by gemini-cli-robot in - [#17725](https://github.com/google-gemini/gemini-cli/pull/17725) -- feat(skills): final stable promotion cleanup by abhipatel12 in - [#17726](https://github.com/google-gemini/gemini-cli/pull/17726) -- test(core): mock fetch in OAuth transport fallback tests by jw409 in - [#17059](https://github.com/google-gemini/gemini-cli/pull/17059) -- feat(cli): include auth method in /bug by erikus in - [#17569](https://github.com/google-gemini/gemini-cli/pull/17569) -- Add a email privacy note to bug_report template by nemyung in - [#17474](https://github.com/google-gemini/gemini-cli/pull/17474) -- Rewind documentation by Adib234 in - [#17446](https://github.com/google-gemini/gemini-cli/pull/17446) -- fix: verify audio/video MIME types with content check by maru0804 in - [#16907](https://github.com/google-gemini/gemini-cli/pull/16907) -- feat(core): add support for positron ide (#15045) by kapsner in - [#15047](https://github.com/google-gemini/gemini-cli/pull/15047) -- /oncall dedup - wrap texts to nextlines by sehoon38 in - [#17782](https://github.com/google-gemini/gemini-cli/pull/17782) -- fix(admin): rename advanced features admin setting by skeshive in - [#17786](https://github.com/google-gemini/gemini-cli/pull/17786) -- [extension config] Make breaking optional value non-optional by chrstnb in - [#17785](https://github.com/google-gemini/gemini-cli/pull/17785) -- Fix docs-writer skill issues by g-samroberts in - [#17734](https://github.com/google-gemini/gemini-cli/pull/17734) -- fix(core): suppress duplicate hook failure warnings during streaming by + [#18243](https://github.com/google-gemini/gemini-cli/pull/18243) +- feat(core): remove hardcoded policy bypass for local subagents by abhipatel12 + in [#18153](https://github.com/google-gemini/gemini-cli/pull/18153) +- feat(plan): implement plan slash command by Adib234 in + [#17698](https://github.com/google-gemini/gemini-cli/pull/17698) +- feat: increase ask_user label limit to 16 characters by jackwotherspoon in + [#18320](https://github.com/google-gemini/gemini-cli/pull/18320) +- Add information about the agent skills lifecycle and clarify docs-writer skill + metadata. by g-samroberts in + [#18234](https://github.com/google-gemini/gemini-cli/pull/18234) +- feat(core): add enter_plan_mode tool by jerop in + [#18324](https://github.com/google-gemini/gemini-cli/pull/18324) +- Stop showing an error message in /plan by Adib234 in + [#18333](https://github.com/google-gemini/gemini-cli/pull/18333) +- fix(hooks): remove unnecessary logging for hook registration by abhipatel12 in + [#18332](https://github.com/google-gemini/gemini-cli/pull/18332) +- fix(mcp): ensure MCP transport is closed to prevent memory leaks by cbcoutinho + in [#18054](https://github.com/google-gemini/gemini-cli/pull/18054) +- feat(skills): implement linking for agent skills by MushuEE in + [#18295](https://github.com/google-gemini/gemini-cli/pull/18295) +- Changelogs for 0.27.0 and 0.28.0-preview0 by g-samroberts in + [#18336](https://github.com/google-gemini/gemini-cli/pull/18336) +- chore: correct docs as skills and hooks are stable by jackwotherspoon in + [#18358](https://github.com/google-gemini/gemini-cli/pull/18358) +- feat(admin): Implement admin allowlist for MCP server configurations by + skeshive in [#18311](https://github.com/google-gemini/gemini-cli/pull/18311) +- fix(core): add retry logic for transient SSL/TLS errors + ([#17318](https://github.com/google-gemini/gemini-cli/pull/17318)) by + ppgranger in [#18310](https://github.com/google-gemini/gemini-cli/pull/18310) +- Add support for /extensions config command by chrstnb in + [#17895](https://github.com/google-gemini/gemini-cli/pull/17895) +- fix(core): handle non-compliant mcpbridge responses from Xcode 26.3 by + peterfriese in + [#18376](https://github.com/google-gemini/gemini-cli/pull/18376) +- feat(cli): Add W, B, E Vim motions and operator support by ademuri in + [#16209](https://github.com/google-gemini/gemini-cli/pull/16209) +- fix: Windows Specific Agent Quality & System Prompt by scidomino in + [#18351](https://github.com/google-gemini/gemini-cli/pull/18351) +- feat(plan): support replace tool in plan mode to edit plans by jerop in + [#18379](https://github.com/google-gemini/gemini-cli/pull/18379) +- Improving memory tool instructions and eval testing by alisa-alisa in + [#18091](https://github.com/google-gemini/gemini-cli/pull/18091) +- fix(cli): color extension link success message green by MushuEE in + [#18386](https://github.com/google-gemini/gemini-cli/pull/18386) +- undo by jacob314 in + [#18147](https://github.com/google-gemini/gemini-cli/pull/18147) +- feat(plan): add guidance on iterating on approved plans vs creating new plans + by jerop in [#18346](https://github.com/google-gemini/gemini-cli/pull/18346) +- feat(plan): fix invalid tool calls in plan mode by Adib234 in + [#18352](https://github.com/google-gemini/gemini-cli/pull/18352) +- feat(plan): integrate planning artifacts and tools into primary workflows by + jerop in [#18375](https://github.com/google-gemini/gemini-cli/pull/18375) +- Fix permission check by scidomino in + [#18395](https://github.com/google-gemini/gemini-cli/pull/18395) +- ux(polish) autocomplete in the input prompt by jacob314 in + [#18181](https://github.com/google-gemini/gemini-cli/pull/18181) +- fix: resolve infinite loop when using 'Modify with external editor' by + ppgranger in [#17453](https://github.com/google-gemini/gemini-cli/pull/17453) +- feat: expand verify-release to macOS and Windows by yunaseoul in + [#18145](https://github.com/google-gemini/gemini-cli/pull/18145) +- feat(plan): implement support for MCP servers in Plan mode by Adib234 in + [#18229](https://github.com/google-gemini/gemini-cli/pull/18229) +- chore: update folder trust error messaging by galz10 in + [#18402](https://github.com/google-gemini/gemini-cli/pull/18402) +- feat(plan): create a metric for execution of plans generated in plan mode by + Adib234 in [#18236](https://github.com/google-gemini/gemini-cli/pull/18236) +- perf(ui): optimize stripUnsafeCharacters with regex by gsquared94 in + [#18413](https://github.com/google-gemini/gemini-cli/pull/18413) +- feat(context): implement observation masking for tool outputs by abhipatel12 + in [#18389](https://github.com/google-gemini/gemini-cli/pull/18389) +- feat(core,cli): implement session-linked tool output storage and cleanup by abhipatel12 in - [#17727](https://github.com/google-gemini/gemini-cli/pull/17727) -- test: add more tests for AskUser by jackwotherspoon in - [#17720](https://github.com/google-gemini/gemini-cli/pull/17720) -- feat(cli): enable activity logging for non-interactive mode and evals by - SandyTao520 in - [#17703](https://github.com/google-gemini/gemini-cli/pull/17703) -- feat(core): add support for custom deny messages in policy rules by - allenhutchison in - [#17427](https://github.com/google-gemini/gemini-cli/pull/17427) -- Fix unintended credential exposure to MCP Servers by Adib234 in - [#17311](https://github.com/google-gemini/gemini-cli/pull/17311) -- feat(extensions): add support for custom themes in extensions by spencer426 in - [#17327](https://github.com/google-gemini/gemini-cli/pull/17327) -- fix: persist and restore workspace directories on session resume by - korade-krushna in - [#17454](https://github.com/google-gemini/gemini-cli/pull/17454) -- Update release notes pages for 0.26.0 and 0.27.0-preview. by g-samroberts in - [#17744](https://github.com/google-gemini/gemini-cli/pull/17744) -- feat(ux): update cell border color and created test file for table rendering - by devr0306 in - [#17798](https://github.com/google-gemini/gemini-cli/pull/17798) -- Change height for the ToolConfirmationQueue. by jacob314 in - [#17799](https://github.com/google-gemini/gemini-cli/pull/17799) -- feat(cli): add user identity info to stats command by sehoon38 in - [#17612](https://github.com/google-gemini/gemini-cli/pull/17612) -- fix(ux): fixed off-by-some wrapping caused by fixed-width characters by - devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816) -- feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and - Shift+Cmd+Z/Shift+Alt+Z by scidomino in - [#17800](https://github.com/google-gemini/gemini-cli/pull/17800) -- fix(evals): use absolute path for activity log directory by SandyTao520 in - [#17830](https://github.com/google-gemini/gemini-cli/pull/17830) -- test: add integration test to verify stdout/stderr routing by ved015 in - [#17280](https://github.com/google-gemini/gemini-cli/pull/17280) -- fix(cli): list installed extensions when update target missing by tt-a1i in - [#17082](https://github.com/google-gemini/gemini-cli/pull/17082) -- fix(cli): handle PAT tokens and credentials in git remote URL parsing by - afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650) -- fix(core): use returnDisplay for error result display by Nubebuster in - [#14994](https://github.com/google-gemini/gemini-cli/pull/14994) -- Fix detection of bun as package manager by Randomblock1 in - [#17462](https://github.com/google-gemini/gemini-cli/pull/17462) -- feat(cli): show hooksConfig.enabled in settings dialog by abhipatel12 in - [#17810](https://github.com/google-gemini/gemini-cli/pull/17810) -- feat(cli): Display user identity (auth, email, tier) on startup by yunaseoul - in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591) -- fix: prevent ghost border for AskUserDialog by jackwotherspoon in - [#17788](https://github.com/google-gemini/gemini-cli/pull/17788) -- docs: mark A2A subagents as experimental in subagents.md by adamfweidman in - [#17863](https://github.com/google-gemini/gemini-cli/pull/17863) -- Resolve error thrown for sensitive values by chrstnb in - [#17826](https://github.com/google-gemini/gemini-cli/pull/17826) -- fix(admin): Rename secureModeEnabled to strictModeDisabled by skeshive in - [#17789](https://github.com/google-gemini/gemini-cli/pull/17789) -- feat(ux): update truncate dots to be shorter in tables by devr0306 in - [#17825](https://github.com/google-gemini/gemini-cli/pull/17825) -- fix(core): resolve DEP0040 punycode deprecation via patch-package by - ATHARVA262005 in - [#17692](https://github.com/google-gemini/gemini-cli/pull/17692) -- feat(plan): create generic Checklist component and refactor Todo by Adib234 in - [#17741](https://github.com/google-gemini/gemini-cli/pull/17741) -- Cleanup post delegate_to_agent removal by gundermanc in - [#17875](https://github.com/google-gemini/gemini-cli/pull/17875) -- fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration - - Fixes #17877 by cocosheng-g in - [#17803](https://github.com/google-gemini/gemini-cli/pull/17803) -- Disable mouse tracking e2e by alisa-alisa in - [#17880](https://github.com/google-gemini/gemini-cli/pull/17880) -- fix(cli): use correct setting key for Cloud Shell auth by sehoon38 in - [#17884](https://github.com/google-gemini/gemini-cli/pull/17884) -- chore: revert IDE specific ASCII logo by jackwotherspoon in - [#17887](https://github.com/google-gemini/gemini-cli/pull/17887) -- Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by - sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898) -- Refactoring of disabling of mouse tracking in e2e tests by alisa-alisa in - [#17902](https://github.com/google-gemini/gemini-cli/pull/17902) -- feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by deyim - in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177) -- feat(core): Isolate and cleanup truncated tool outputs by SandyTao520 in - [#17594](https://github.com/google-gemini/gemini-cli/pull/17594) -- Create skills page, update commands, refine docs by g-samroberts in - [#17842](https://github.com/google-gemini/gemini-cli/pull/17842) -- feat: preserve EOL in files by Thomas-Shephard in - [#16087](https://github.com/google-gemini/gemini-cli/pull/16087) -- Fix HalfLinePaddedBox in screenreader mode. by jacob314 in - [#17914](https://github.com/google-gemini/gemini-cli/pull/17914) -- bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X - in vim mode. by jacob314 in - [#17938](https://github.com/google-gemini/gemini-cli/pull/17938) -- feat(core): implement interactive and non-interactive consent for OAuth by - ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699) -- perf(core): optimize token calculation and add support for multimodal tool - responses by abhipatel12 in - [#17835](https://github.com/google-gemini/gemini-cli/pull/17835) -- refactor(hooks): remove legacy tools.enableHooks setting by abhipatel12 in - [#17867](https://github.com/google-gemini/gemini-cli/pull/17867) -- feat(ci): add npx smoke test to verify installability by bdmorgan in - [#17927](https://github.com/google-gemini/gemini-cli/pull/17927) -- feat(core): implement dynamic policy registration for subagents by abhipatel12 - in [#17838](https://github.com/google-gemini/gemini-cli/pull/17838) -- feat: Implement background shell commands by galz10 in - [#14849](https://github.com/google-gemini/gemini-cli/pull/14849) -- feat(admin): provide actionable error messages for disabled features by - skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815) -- Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by - jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940) -- Fix broken link in docs by chrstnb in - [#17959](https://github.com/google-gemini/gemini-cli/pull/17959) -- feat(plan): reuse standard tool confirmation for AskUser tool by jerop in - [#17864](https://github.com/google-gemini/gemini-cli/pull/17864) -- feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by - lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942) -- run npx pointing to the specific commit SHA by sehoon38 in - [#17970](https://github.com/google-gemini/gemini-cli/pull/17970) -- Add allowedExtensions setting by kevinjwang1 in - [#17695](https://github.com/google-gemini/gemini-cli/pull/17695) -- feat(plan): refactor ToolConfirmationPayload to union type by jerop in - [#17980](https://github.com/google-gemini/gemini-cli/pull/17980) -- lower the default max retries to reduce contention by sehoon38 in - [#17975](https://github.com/google-gemini/gemini-cli/pull/17975) -- fix(core): ensure YOLO mode auto-approves complex shell commands when parsing - fails by abhipatel12 in - [#17920](https://github.com/google-gemini/gemini-cli/pull/17920) -- Fix broken link. by g-samroberts in - [#17972](https://github.com/google-gemini/gemini-cli/pull/17972) -- Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over - AppContainer for input handling. by jacob314 in - [#17993](https://github.com/google-gemini/gemini-cli/pull/17993) -- Fix truncation for AskQuestion by jacob314 in - [#18001](https://github.com/google-gemini/gemini-cli/pull/18001) -- fix(workflow): update maintainer check logic to be inclusive and - case-insensitive by bdmorgan in - [#18009](https://github.com/google-gemini/gemini-cli/pull/18009) -- Fix Esc cancel during streaming by LyalinDotCom in - [#18039](https://github.com/google-gemini/gemini-cli/pull/18039) -- feat(acp): add session resume support by bdmorgan in - [#18043](https://github.com/google-gemini/gemini-cli/pull/18043) -- fix(ci): prevent stale PR closer from incorrectly closing new PRs by bdmorgan - in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069) -- chore: delete autoAccept setting unused in production by victorvianna in - [#17862](https://github.com/google-gemini/gemini-cli/pull/17862) -- feat(plan): use placeholder for choice question "Other" option by jerop in - [#18101](https://github.com/google-gemini/gemini-cli/pull/18101) -- docs: update clearContext to hookSpecificOutput by jackwotherspoon in - [#18024](https://github.com/google-gemini/gemini-cli/pull/18024) -- docs-writer skill: Update docs writer skill by jkcinouye in - [#17928](https://github.com/google-gemini/gemini-cli/pull/17928) -- Sehoon/oncall filter by sehoon38 in - [#18105](https://github.com/google-gemini/gemini-cli/pull/18105) -- feat(core): add setting to disable loop detection by SandyTao520 in - [#18008](https://github.com/google-gemini/gemini-cli/pull/18008) -- Docs: Revise docs/index.md by jkcinouye in - [#17879](https://github.com/google-gemini/gemini-cli/pull/17879) -- Fix up/down arrow regression and add test. by jacob314 in - [#18108](https://github.com/google-gemini/gemini-cli/pull/18108) -- fix(ui): prevent content leak in MaxSizedBox bottom overflow by jerop in - [#17991](https://github.com/google-gemini/gemini-cli/pull/17991) -- refactor: migrate checks.ts utility to core and deduplicate by jerop in - [#18139](https://github.com/google-gemini/gemini-cli/pull/18139) -- feat(core): implement tool name aliasing for backward compatibility by - SandyTao520 in - [#17974](https://github.com/google-gemini/gemini-cli/pull/17974) -- docs: fix help-wanted label spelling by pavan-sh in - [#18114](https://github.com/google-gemini/gemini-cli/pull/18114) -- feat(cli): implement automatic theme switching based on terminal background by + [#18416](https://github.com/google-gemini/gemini-cli/pull/18416) +- Shorten temp directory by joshualitt in + [#17901](https://github.com/google-gemini/gemini-cli/pull/17901) +- feat(plan): add behavioral evals for plan mode by jerop in + [#18437](https://github.com/google-gemini/gemini-cli/pull/18437) +- Add extension registry client by chrstnb in + [#18396](https://github.com/google-gemini/gemini-cli/pull/18396) +- Enable extension config by default by chrstnb in + [#18447](https://github.com/google-gemini/gemini-cli/pull/18447) +- Automatically generate change logs on release by g-samroberts in + [#18401](https://github.com/google-gemini/gemini-cli/pull/18401) +- Remove previewFeatures and default to Gemini 3 by sehoon38 in + [#18414](https://github.com/google-gemini/gemini-cli/pull/18414) +- feat(admin): apply MCP allowlist to extensions & gemini mcp list command by + skeshive in [#18442](https://github.com/google-gemini/gemini-cli/pull/18442) +- fix(cli): improve focus navigation for interactive and background shells by + galz10 in [#18343](https://github.com/google-gemini/gemini-cli/pull/18343) +- Add shortcuts hint and panel for discoverability by LyalinDotCom in + [#18035](https://github.com/google-gemini/gemini-cli/pull/18035) +- fix(config): treat system settings as read-only during migration and warn user + by spencer426 in + [#18277](https://github.com/google-gemini/gemini-cli/pull/18277) +- feat(plan): add positive test case and update eval stability policy by jerop + in [#18457](https://github.com/google-gemini/gemini-cli/pull/18457) +- fix- windows: add shell: true for spawnSync to fix EINVAL with .cmd editors by + zackoch in [#18408](https://github.com/google-gemini/gemini-cli/pull/18408) +- bug(core): Fix bug when saving plans. by joshualitt in + [#18465](https://github.com/google-gemini/gemini-cli/pull/18465) +- Refactor atCommandProcessor by scidomino in + [#18461](https://github.com/google-gemini/gemini-cli/pull/18461) +- feat(core): implement persistence and resumption for masked tool outputs by + abhipatel12 in + [#18451](https://github.com/google-gemini/gemini-cli/pull/18451) +- refactor: simplify tool output truncation to single config by SandyTao520 in + [#18446](https://github.com/google-gemini/gemini-cli/pull/18446) +- bug(core): Ensure storage is initialized early, even if config is not. by + joshualitt in [#18471](https://github.com/google-gemini/gemini-cli/pull/18471) +- chore: Update build-and-start script to support argument forwarding by Abhijit-2592 in - [#17976](https://github.com/google-gemini/gemini-cli/pull/17976) -- fix(ide): no-op refactoring that moves the connection logic to helper - functions by skeshive in - [#18118](https://github.com/google-gemini/gemini-cli/pull/18118) -- feat: update review-frontend-and-fix slash command to review-and-fix by galz10 - in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146) -- fix: improve Ctrl+R reverse search by jackwotherspoon in - [#18075](https://github.com/google-gemini/gemini-cli/pull/18075) -- feat(plan): handle inconsistency in schedulers by Adib234 in - [#17813](https://github.com/google-gemini/gemini-cli/pull/17813) -- feat(plan): add core logic and exit_plan_mode tool definition by jerop in - [#18110](https://github.com/google-gemini/gemini-cli/pull/18110) -- feat(core): rename search_file_content tool to grep_search and add legacy - alias by SandyTao520 in - [#18003](https://github.com/google-gemini/gemini-cli/pull/18003) -- fix(core): prioritize detailed error messages for code assist setup by - gsquared94 in [#17852](https://github.com/google-gemini/gemini-cli/pull/17852) -- fix(cli): resolve environment loading and auth validation issues in ACP mode - by bdmorgan in - [#18025](https://github.com/google-gemini/gemini-cli/pull/18025) -- feat(core): add .agents/skills directory alias for skill discovery by + [#18241](https://github.com/google-gemini/gemini-cli/pull/18241) +- fix(core): prevent subagent bypass in plan mode by jerop in + [#18484](https://github.com/google-gemini/gemini-cli/pull/18484) +- feat(cli): add WebSocket-based network logging and streaming chunk support by + SandyTao520 in + [#18383](https://github.com/google-gemini/gemini-cli/pull/18383) +- feat(cli): update approval modes UI by jerop in + [#18476](https://github.com/google-gemini/gemini-cli/pull/18476) +- fix(cli): reload skills and agents on extension restart by NTaylorMullen in + [#18411](https://github.com/google-gemini/gemini-cli/pull/18411) +- fix(core): expand excludeTools with legacy aliases for renamed tools by + SandyTao520 in + [#18498](https://github.com/google-gemini/gemini-cli/pull/18498) +- feat(core): overhaul system prompt for rigor, integrity, and intent alignment + by NTaylorMullen in + [#17263](https://github.com/google-gemini/gemini-cli/pull/17263) +- Patch for generate changelog docs yaml file by g-samroberts in + [#18496](https://github.com/google-gemini/gemini-cli/pull/18496) +- Code review fixes for show question mark pr. by jacob314 in + [#18480](https://github.com/google-gemini/gemini-cli/pull/18480) +- fix(cli): add SS3 Shift+Tab support for Windows terminals by ThanhNguyxn in + [#18187](https://github.com/google-gemini/gemini-cli/pull/18187) +- chore: remove redundant planning prompt from final shell by jerop in + [#18528](https://github.com/google-gemini/gemini-cli/pull/18528) +- docs: require pr-creator skill for PR generation by NTaylorMullen in + [#18536](https://github.com/google-gemini/gemini-cli/pull/18536) +- chore: update colors for ask_user dialog by jackwotherspoon in + [#18543](https://github.com/google-gemini/gemini-cli/pull/18543) +- feat(core): exempt high-signal tools from output masking by abhipatel12 in + [#18545](https://github.com/google-gemini/gemini-cli/pull/18545) +- refactor(core): remove memory tool instructions from Gemini 3 prompt by NTaylorMullen in - [#18151](https://github.com/google-gemini/gemini-cli/pull/18151) -- chore(core): reassign telemetry keys to avoid server conflict by mattKorwel in - [#18161](https://github.com/google-gemini/gemini-cli/pull/18161) -- Add link to rewind doc in commands.md by Adib234 in - [#17961](https://github.com/google-gemini/gemini-cli/pull/17961) -- feat(core): add draft-2020-12 JSON Schema support with lenient fallback by - afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060) -- refactor(core): robust trimPreservingTrailingNewline and regression test by + [#18559](https://github.com/google-gemini/gemini-cli/pull/18559) +- chore: remove feedback instruction from system prompt by NTaylorMullen in + [#18560](https://github.com/google-gemini/gemini-cli/pull/18560) +- feat(context): add remote configuration for tool output masking thresholds by + abhipatel12 in + [#18553](https://github.com/google-gemini/gemini-cli/pull/18553) +- feat(core): pause agent timeout budget while waiting for tool confirmation by + abhipatel12 in + [#18415](https://github.com/google-gemini/gemini-cli/pull/18415) +- refactor(config): remove experimental.enableEventDrivenScheduler setting by + abhipatel12 in + [#17924](https://github.com/google-gemini/gemini-cli/pull/17924) +- feat(cli): truncate shell output in UI history and improve active shell + display by jwhelangoog in + [#17438](https://github.com/google-gemini/gemini-cli/pull/17438) +- refactor(cli): switch useToolScheduler to event-driven engine by abhipatel12 + in [#18565](https://github.com/google-gemini/gemini-cli/pull/18565) +- fix(core): correct escaped interpolation in system prompt by NTaylorMullen in + [#18557](https://github.com/google-gemini/gemini-cli/pull/18557) +- propagate abortSignal by scidomino in + [#18477](https://github.com/google-gemini/gemini-cli/pull/18477) +- feat(core): conditionally include ctrl+f prompt based on interactive shell + setting by NTaylorMullen in + [#18561](https://github.com/google-gemini/gemini-cli/pull/18561) +- fix(core): ensure enter_plan_mode tool registration respects experimental.plan + by jerop in [#18587](https://github.com/google-gemini/gemini-cli/pull/18587) +- feat(core): transition sub-agents to XML format and improve definitions by + NTaylorMullen in + [#18555](https://github.com/google-gemini/gemini-cli/pull/18555) +- docs: Add Plan Mode documentation by jerop in + [#18582](https://github.com/google-gemini/gemini-cli/pull/18582) +- chore: strengthen validation guidance in system prompt by NTaylorMullen in + [#18544](https://github.com/google-gemini/gemini-cli/pull/18544) +- Fix newline insertion bug in replace tool by werdnum in + [#18595](https://github.com/google-gemini/gemini-cli/pull/18595) +- fix(evals): update save_memory evals and simplify tool description by + NTaylorMullen in + [#18610](https://github.com/google-gemini/gemini-cli/pull/18610) +- chore(evals): update validation_fidelity_pre_existing_errors to USUALLY_PASSES + by NTaylorMullen in + [#18617](https://github.com/google-gemini/gemini-cli/pull/18617) +- fix: shorten tool call IDs and fix duplicate tool name in truncated output + filenames by SandyTao520 in + [#18600](https://github.com/google-gemini/gemini-cli/pull/18600) +- feat(cli): implement atomic writes and safety checks for trusted folders by + galz10 in [#18406](https://github.com/google-gemini/gemini-cli/pull/18406) +- Remove relative docs links by chrstnb in + [#18650](https://github.com/google-gemini/gemini-cli/pull/18650) +- docs: add legacy snippets convention to GEMINI.md by NTaylorMullen in + [#18597](https://github.com/google-gemini/gemini-cli/pull/18597) +- fix(chore): Support linting for cjs by aswinashok44 in + [#18639](https://github.com/google-gemini/gemini-cli/pull/18639) +- feat: move shell efficiency guidelines to tool description by NTaylorMullen in + [#18614](https://github.com/google-gemini/gemini-cli/pull/18614) +- Added "" as default value, since getText() used to expect a string only and + thus crashed when undefined... Fixes #18076 by 019-Abhi in + [#18099](https://github.com/google-gemini/gemini-cli/pull/18099) +- Allow @-includes outside of workspaces (with permission) by scidomino in + [#18470](https://github.com/google-gemini/gemini-cli/pull/18470) +- chore: make ask_user header description more clear by jackwotherspoon in + [#18657](https://github.com/google-gemini/gemini-cli/pull/18657) +- refactor(core): model-dependent tool definitions by aishaneeshah in + [#18563](https://github.com/google-gemini/gemini-cli/pull/18563) +- Harded code assist converter. by jacob314 in + [#18656](https://github.com/google-gemini/gemini-cli/pull/18656) +- bug(core): Fix minor bug in migration logic. by joshualitt in + [#18661](https://github.com/google-gemini/gemini-cli/pull/18661) +- feat: enable plan mode experiment in settings by jerop in + [#18636](https://github.com/google-gemini/gemini-cli/pull/18636) +- refactor: push isValidPath() into parsePastedPaths() by scidomino in + [#18664](https://github.com/google-gemini/gemini-cli/pull/18664) +- fix(cli): correct 'esc to cancel' position and restore duration display by + NTaylorMullen in + [#18534](https://github.com/google-gemini/gemini-cli/pull/18534) +- feat(cli): add DevTools integration with gemini-cli-devtools by SandyTao520 in + [#18648](https://github.com/google-gemini/gemini-cli/pull/18648) +- chore: remove unused exports and redundant hook files by SandyTao520 in + [#18681](https://github.com/google-gemini/gemini-cli/pull/18681) +- Fix number of lines being reported in rewind confirmation dialog by Adib234 in + [#18675](https://github.com/google-gemini/gemini-cli/pull/18675) +- feat(cli): disable folder trust in headless mode by galz10 in + [#18407](https://github.com/google-gemini/gemini-cli/pull/18407) +- Disallow unsafe type assertions by gundermanc in + [#18688](https://github.com/google-gemini/gemini-cli/pull/18688) +- Change event type for release by g-samroberts in + [#18693](https://github.com/google-gemini/gemini-cli/pull/18693) +- feat: handle multiple dynamic context filenames in system prompt by + NTaylorMullen in + [#18598](https://github.com/google-gemini/gemini-cli/pull/18598) +- Properly parse at-commands with narrow non-breaking spaces by scidomino in + [#18677](https://github.com/google-gemini/gemini-cli/pull/18677) +- refactor(core): centralize core tool definitions and support model-specific + schemas by aishaneeshah in + [#18662](https://github.com/google-gemini/gemini-cli/pull/18662) +- feat(core): Render memory hierarchically in context. by joshualitt in + [#18350](https://github.com/google-gemini/gemini-cli/pull/18350) +- feat: Ctrl+O to expand paste placeholder by jackwotherspoon in + [#18103](https://github.com/google-gemini/gemini-cli/pull/18103) +- fix(cli): Improve header spacing by NTaylorMullen in + [#18531](https://github.com/google-gemini/gemini-cli/pull/18531) +- Feature/quota visibility 16795 by spencer426 in + [#18203](https://github.com/google-gemini/gemini-cli/pull/18203) +- Inline thinking bubbles with summary/full modes by LyalinDotCom in + [#18033](https://github.com/google-gemini/gemini-cli/pull/18033) +- docs: remove TOC marker from Plan Mode header by jerop in + [#18678](https://github.com/google-gemini/gemini-cli/pull/18678) +- fix(ui): remove redundant newlines in Gemini messages by NTaylorMullen in + [#18538](https://github.com/google-gemini/gemini-cli/pull/18538) +- test(cli): fix AppContainer act() warnings and improve waitFor resilience by + NTaylorMullen in + [#18676](https://github.com/google-gemini/gemini-cli/pull/18676) +- refactor(core): refine Security & System Integrity section in system prompt by + NTaylorMullen in + [#18601](https://github.com/google-gemini/gemini-cli/pull/18601) +- Fix layout rounding. by gundermanc in + [#18667](https://github.com/google-gemini/gemini-cli/pull/18667) +- docs(skills): enhance pr-creator safety and interactivity by NTaylorMullen in + [#18616](https://github.com/google-gemini/gemini-cli/pull/18616) +- test(core): remove hardcoded model from TestRig by NTaylorMullen in + [#18710](https://github.com/google-gemini/gemini-cli/pull/18710) +- feat(core): optimize sub-agents system prompt intro by NTaylorMullen in + [#18608](https://github.com/google-gemini/gemini-cli/pull/18608) +- feat(cli): update approval mode labels and shortcuts per latest UX spec by + jerop in [#18698](https://github.com/google-gemini/gemini-cli/pull/18698) +- fix(plan): update persistent approval mode setting by Adib234 in + [#18638](https://github.com/google-gemini/gemini-cli/pull/18638) +- fix: move toasts location to left side by jackwotherspoon in + [#18705](https://github.com/google-gemini/gemini-cli/pull/18705) +- feat(routing): restrict numerical routing to Gemini 3 family by mattKorwel in + [#18478](https://github.com/google-gemini/gemini-cli/pull/18478) +- fix(ide): fix ide nudge setting by skeshive in + [#18733](https://github.com/google-gemini/gemini-cli/pull/18733) +- fix(core): standardize tool formatting in system prompts by NTaylorMullen in + [#18615](https://github.com/google-gemini/gemini-cli/pull/18615) +- chore: consolidate to green in ask user dialog by jackwotherspoon in + [#18734](https://github.com/google-gemini/gemini-cli/pull/18734) +- feat: add extensionsExplore setting to enable extensions explore UI. by + sripasg in [#18686](https://github.com/google-gemini/gemini-cli/pull/18686) +- feat(cli): defer devtools startup and integrate with F12 by SandyTao520 in + [#18695](https://github.com/google-gemini/gemini-cli/pull/18695) +- ui: update & subdue footer colors and animate progress indicator by + keithguerin in + [#18570](https://github.com/google-gemini/gemini-cli/pull/18570) +- test: add model-specific snapshots for coreTools by aishaneeshah in + [#18707](https://github.com/google-gemini/gemini-cli/pull/18707) +- ci: shard windows tests and fix event listener leaks by NTaylorMullen in + [#18670](https://github.com/google-gemini/gemini-cli/pull/18670) +- fix: allow ask_user tool in yolo mode by jackwotherspoon in + [#18541](https://github.com/google-gemini/gemini-cli/pull/18541) +- feat: redact disabled tools from system prompt + ([#13597](https://github.com/google-gemini/gemini-cli/pull/13597)) by + NTaylorMullen in + [#18613](https://github.com/google-gemini/gemini-cli/pull/18613) +- Update Gemini.md to use the curent year on creating new files by sehoon38 in + [#18460](https://github.com/google-gemini/gemini-cli/pull/18460) +- Code review cleanup for thinking display by jacob314 in + [#18720](https://github.com/google-gemini/gemini-cli/pull/18720) +- fix(cli): hide scrollbars when in alternate buffer copy mode by werdnum in + [#18354](https://github.com/google-gemini/gemini-cli/pull/18354) +- Fix issues with rip grep by gundermanc in + [#18756](https://github.com/google-gemini/gemini-cli/pull/18756) +- fix(cli): fix history navigation regression after prompt autocomplete by + sehoon38 in [#18752](https://github.com/google-gemini/gemini-cli/pull/18752) +- chore: cleanup unused and add unlisted dependencies in packages/cli by adamfweidman in - [#18196](https://github.com/google-gemini/gemini-cli/pull/18196) -- Remove MCP servers on extension uninstall by chrstnb in - [#18121](https://github.com/google-gemini/gemini-cli/pull/18121) -- refactor: localize ACP error parsing logic to cli package by bdmorgan in - [#18193](https://github.com/google-gemini/gemini-cli/pull/18193) -- feat(core): Add A2A auth config types by adamfweidman in - [#18205](https://github.com/google-gemini/gemini-cli/pull/18205) -- Set default max attempts to 3 and use the common variable by sehoon38 in - [#18209](https://github.com/google-gemini/gemini-cli/pull/18209) -- feat(plan): add exit_plan_mode ui and prompt by jerop in - [#18162](https://github.com/google-gemini/gemini-cli/pull/18162) -- fix(test): improve test isolation and enable subagent evaluations by - cocosheng-g in - [#18138](https://github.com/google-gemini/gemini-cli/pull/18138) -- feat(plan): use custom deny messages in plan mode policies by Adib234 in - [#18195](https://github.com/google-gemini/gemini-cli/pull/18195) -- Match on extension ID when stopping extensions by chrstnb in - [#18218](https://github.com/google-gemini/gemini-cli/pull/18218) -- fix(core): Respect user's .gitignore preference by xyrolle in - [#15482](https://github.com/google-gemini/gemini-cli/pull/15482) -- docs: document GEMINI_CLI_HOME environment variable by adamfweidman in - [#18219](https://github.com/google-gemini/gemini-cli/pull/18219) -- chore(core): explicitly state plan storage path in prompt by jerop in - [#18222](https://github.com/google-gemini/gemini-cli/pull/18222) -- A2a admin setting by DavidAPierce in - [#17868](https://github.com/google-gemini/gemini-cli/pull/17868) -- feat(a2a): Add pluggable auth provider infrastructure by adamfweidman in - [#17934](https://github.com/google-gemini/gemini-cli/pull/17934) -- Fix handling of empty settings by chrstnb in - [#18131](https://github.com/google-gemini/gemini-cli/pull/18131) -- Reload skills when extensions change by chrstnb in - [#18225](https://github.com/google-gemini/gemini-cli/pull/18225) -- feat: Add markdown rendering to ask_user tool by jackwotherspoon in - [#18211](https://github.com/google-gemini/gemini-cli/pull/18211) -- Add telemetry to rewind by Adib234 in - [#18122](https://github.com/google-gemini/gemini-cli/pull/18122) -- feat(admin): add support for MCP configuration via admin controls (pt1) by - skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223) -- feat(core): require user consent before MCP server OAuth by ehedlund in - [#18132](https://github.com/google-gemini/gemini-cli/pull/18132) -- fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars - by skeshive in - [#18231](https://github.com/google-gemini/gemini-cli/pull/18231) -- feat(ui): move user identity display to header by sehoon38 in - [#18216](https://github.com/google-gemini/gemini-cli/pull/18216) -- fix: enforce folder trust for workspace settings, skills, and context by - galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596) + [#18749](https://github.com/google-gemini/gemini-cli/pull/18749) +- Fix issue where Gemini CLI creates tests in a new file by gundermanc in + [#18409](https://github.com/google-gemini/gemini-cli/pull/18409) +- feat(telemetry): Ensure experiment IDs are included in OpenTelemetry logs by + kevin-ramdass in + [#18747](https://github.com/google-gemini/gemini-cli/pull/18747) **Full changelog**: -https://github.com/google-gemini/gemini-cli/compare/v0.27.0-preview.8...v0.28.0-preview.0 +https://github.com/google-gemini/gemini-cli/compare/v0.28.0-preview.0...v0.29.0-preview.0 diff --git a/docs/cli/cli-reference.md b/docs/cli/cli-reference.md index d1094a15e2..8199445625 100644 --- a/docs/cli/cli-reference.md +++ b/docs/cli/cli-reference.md @@ -27,29 +27,29 @@ and parameters. ## CLI Options -| Option | Alias | Type | Default | Description | -| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | -| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging | -| `--version` | `-v` | - | - | Show CLI version number and exit | -| `--help` | `-h` | - | - | Show help information | -| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. | -| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. | -| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode | -| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution | -| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` | -| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. | -| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** | -| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** | -| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) | -| `--allowed-tools` | - | array | - | Tools that are allowed to run without confirmation (comma-separated or multiple flags) | -| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) | -| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit | -| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) | -| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit | -| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) | -| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) | -| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility | -| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` | +| Option | Alias | Type | Default | Description | +| -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging | +| `--version` | `-v` | - | - | Show CLI version number and exit | +| `--help` | `-h` | - | - | Show help information | +| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. | +| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. | +| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode | +| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution | +| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` | +| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. | +| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** | +| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** | +| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) | +| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../core/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) | +| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) | +| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit | +| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) | +| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit | +| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) | +| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) | +| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility | +| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` | ## Model selection diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 6e563cda11..c5e6b6747f 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -120,6 +120,8 @@ Slash commands provide meta-level control over the CLI itself. - **`/shortcuts`** - **Description:** Toggle the shortcuts panel above the input. - **Shortcut:** Press `?` when the prompt is empty. + - **Note:** This is separate from the clean UI detail toggle on double-`Tab`, + which switches between minimal and full UI chrome. - **`/hooks`** - **Description:** Manage hooks, which allow you to intercept and customize diff --git a/docs/cli/enterprise.md b/docs/cli/enterprise.md index f22ec81c37..861fc68c71 100644 --- a/docs/cli/enterprise.md +++ b/docs/cli/enterprise.md @@ -223,9 +223,9 @@ gemini ## Restricting tool access You can significantly enhance security by controlling which tools the Gemini -model can use. This is achieved through the `tools.core` and `tools.exclude` -settings. For a list of available tools, see the -[Tools documentation](../tools/index.md). +model can use. This is achieved through the `tools.core` setting and the +[Policy Engine](../core/policy-engine.md). For a list of available tools, see +the [Tools documentation](../tools/index.md). ### Allowlisting with `coreTools` @@ -243,7 +243,10 @@ on the approved list. } ``` -### Blocklisting with `excludeTools` +### Blocklisting with `excludeTools` (Deprecated) + +> **Deprecated:** Use the [Policy Engine](../core/policy-engine.md) for more +> robust control. Alternatively, you can add specific tools that are considered dangerous in your environment to a blocklist. diff --git a/docs/cli/keyboard-shortcuts.md b/docs/cli/keyboard-shortcuts.md index d377cfd3e2..938bc6ff7d 100644 --- a/docs/cli/keyboard-shortcuts.md +++ b/docs/cli/keyboard-shortcuts.md @@ -8,12 +8,12 @@ available combinations. #### Basic Controls -| Action | Keys | -| --------------------------------------------------------------- | ---------- | -| Confirm the current selection or choice. | `Enter` | -| Dismiss dialogs or cancel the current focus. | `Esc` | -| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` | -| Exit the CLI when the input buffer is empty. | `Ctrl + D` | +| Action | Keys | +| --------------------------------------------------------------- | --------------------- | +| Confirm the current selection or choice. | `Enter` | +| Dismiss dialogs or cancel the current focus. | `Esc`
`Ctrl + [` | +| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` | +| Exit the CLI when the input buffer is empty. | `Ctrl + D` | #### Cursor Movement @@ -96,31 +96,31 @@ available combinations. #### App Controls -| Action | Keys | -| ----------------------------------------------------------------------------------------------------- | -------------------------- | -| Toggle detailed error information. | `F12` | -| Toggle the full TODO list. | `Ctrl + T` | -| Show IDE context details. | `Ctrl + G` | -| Toggle Markdown rendering. | `Alt + M` | -| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` | -| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` | -| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` | -| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`
`Ctrl + S` | -| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` | -| Toggle current background shell visibility. | `Ctrl + B` | -| Toggle background shell list. | `Ctrl + L` | -| Kill the active background shell. | `Ctrl + K` | -| Confirm selection in background shell list. | `Enter` | -| Dismiss background shell list. | `Esc` | -| Move focus from background shell to Gemini. | `Shift + Tab` | -| Move focus from background shell list to Gemini. | `Tab (no Shift)` | -| Show warning when trying to unfocus background shell via Tab. | `Tab (no Shift)` | -| Show warning when trying to unfocus shell input via Tab. | `Tab (no Shift)` | -| Move focus from Gemini to the active shell. | `Tab (no Shift)` | -| Move focus from the shell back to Gemini. | `Shift + Tab` | -| Clear the terminal screen and redraw the UI. | `Ctrl + L` | -| Restart the application. | `R` | -| Suspend the application (not yet implemented). | `Ctrl + Z` | +| Action | Keys | +| ----------------------------------------------------------------------------------------------------- | ---------------- | +| Toggle detailed error information. | `F12` | +| Toggle the full TODO list. | `Ctrl + T` | +| Show IDE context details. | `Ctrl + G` | +| Toggle Markdown rendering. | `Alt + M` | +| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` | +| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` | +| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` | +| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` | +| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` | +| Toggle current background shell visibility. | `Ctrl + B` | +| Toggle background shell list. | `Ctrl + L` | +| Kill the active background shell. | `Ctrl + K` | +| Confirm selection in background shell list. | `Enter` | +| Dismiss background shell list. | `Esc` | +| Move focus from background shell to Gemini. | `Shift + Tab` | +| Move focus from background shell list to Gemini. | `Tab (no Shift)` | +| Show warning when trying to move focus away from background shell. | `Tab (no Shift)` | +| Show warning when trying to move focus away from shell input. | `Tab (no Shift)` | +| Move focus from Gemini to the active shell. | `Tab (no Shift)` | +| Move focus from the shell back to Gemini. | `Shift + Tab` | +| Clear the terminal screen and redraw the UI. | `Ctrl + L` | +| Restart the application. | `R` | +| Suspend the CLI and move it to the background. | `Ctrl + Z` | @@ -130,9 +130,15 @@ available combinations. terminal isn't configured to send Meta with Option. - `!` on an empty prompt: Enter or exit shell mode. - `?` on an empty prompt: Toggle the shortcuts panel above the input. Press - `Esc`, `Backspace`, or any printable key to close it. Press `?` again to close - the panel and insert a `?` into the prompt. You can hide only the hint text - via `ui.showShortcutsHint`, without changing this keyboard behavior. + `Esc`, `Backspace`, any printable key, or a registered app hotkey to close it. + The panel also auto-hides while the agent is running/streaming or when + action-required dialogs are shown. Press `?` again to close the panel and + insert a `?` into the prompt. +- `Tab` + `Tab` (while typing in the prompt): Toggle between minimal and full UI + details when no completion/search interaction is active. The selected mode is + remembered for future sessions. Full UI remains the default on first run, and + single `Tab` keeps its existing completion/focus behavior. +- `Shift + Tab` (while typing in the prompt): Cycle approval modes. - `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line mode. - `Esc` pressed twice quickly: Clear the input prompt if it is not empty, diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 6c85515755..1e88560f7a 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -68,7 +68,7 @@ You can enter Plan Mode in three ways: ### The Planning Workflow -1. **Requirements:** The agent clarifies goals using `ask_user`. +1. **Requirements:** The agent clarifies goals using [`ask_user`]. 2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map the codebase and validate assumptions. 3. **Design:** The agent proposes alternative approaches with a recommended @@ -95,11 +95,11 @@ These are the only allowed tools: - **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`] - **Search:** [`grep_search`], [`google_web_search`] -- **Interaction:** `ask_user` +- **Interaction:** [`ask_user`] - **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`, `postgres_read_schema`) are allowed. - **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md` - files in the `~/.gemini/tmp//plans/` directory. + files in the `~/.gemini/tmp///plans/` directory. - **Skills:** [`activate_skill`] (allows loading specialized instructions and resources in a read-only manner) @@ -183,3 +183,4 @@ Guide]. [Policy Engine Guide]: /docs/core/policy-engine.md [`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode [`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode +[`ask_user`]: /docs/tools/ask-user.md diff --git a/docs/cli/sandbox.md b/docs/cli/sandbox.md index 28b54851c2..9f632693c7 100644 --- a/docs/cli/sandbox.md +++ b/docs/cli/sandbox.md @@ -82,10 +82,11 @@ gemini -p "run the test suite" Built-in profiles (set via `SEATBELT_PROFILE` env var): - `permissive-open` (default): Write restrictions, network allowed -- `permissive-closed`: Write restrictions, no network - `permissive-proxied`: Write restrictions, network via proxy - `restrictive-open`: Strict restrictions, network allowed -- `restrictive-closed`: Maximum restrictions +- `restrictive-proxied`: Strict restrictions, network via proxy +- `strict-open`: Read and write restrictions, network allowed +- `strict-proxied`: Read and write restrictions, network via proxy ### Custom sandbox flags diff --git a/docs/cli/telemetry.md b/docs/cli/telemetry.md index 407ba101f2..ca44bccaf0 100644 --- a/docs/cli/telemetry.md +++ b/docs/cli/telemetry.md @@ -275,9 +275,9 @@ For local development and debugging, you can capture telemetry data locally: The following section describes the structure of logs and metrics generated for Gemini CLI. -The `session.id`, `installation.id`, and `user.email` (available only when -authenticated with a Google account) are included as common attributes on all -logs and metrics. +The `session.id`, `installation.id`, `active_approval_mode`, and `user.email` +(available only when authenticated with a Google account) are included as common +attributes on all logs and metrics. ### Logs @@ -360,7 +360,21 @@ Captures tool executions, output truncation, and Edit behavior. - `extension_name` (string, if applicable) - `extension_id` (string, if applicable) - `content_length` (int, if applicable) - - `metadata` (if applicable) + - `metadata` (if applicable), which includes for the `AskUser` tool: + - `ask_user` (object): + - `question_types` (array of strings) + - `ask_user_dismissed` (boolean) + - `ask_user_empty_submission` (boolean) + - `ask_user_answer_count` (number) + - `diffStat` (if applicable), which includes: + - `model_added_lines` (number) + - `model_removed_lines` (number) + - `model_added_chars` (number) + - `model_removed_chars` (number) + - `user_added_lines` (number) + - `user_removed_lines` (number) + - `user_added_chars` (number) + - `user_removed_chars` (number) - `gemini_cli.tool_output_truncated`: Output of a tool call was truncated. - **Attributes**: diff --git a/docs/get-started/configuration-v1.md b/docs/get-started/configuration-v1.md index 050dce32b6..cd1325b977 100644 --- a/docs/get-started/configuration-v1.md +++ b/docs/get-started/configuration-v1.md @@ -166,19 +166,21 @@ a few things you can try in order of recommendation: - **Default:** All tools available for use by the Gemini model. - **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`. -- **`allowedTools`** (array of strings): +- **`allowedTools`** (array of strings) [DEPRECATED]: - **Default:** `undefined` - **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. The - match semantics are the same as `coreTools`. + match semantics are the same as `coreTools`. **Deprecated**: Use the + [Policy Engine](../core/policy-engine.md) instead. - **Example:** `"allowedTools": ["ShellTool(git status)"]`. -- **`excludeTools`** (array of strings): +- **`excludeTools`** (array of strings) [DEPRECATED]: - **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command. + **Deprecated**: Use the [Policy Engine](../core/policy-engine.md) instead. - **Default**: No tools excluded. - **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`. - **Security Note:** Command-specific restrictions in `excludeTools` for diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 84818a59be..afbdac94a6 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -96,6 +96,13 @@ their corresponding top-level category object in your `settings.json` file. +#### `policyPaths` + +- **`policyPaths`** (array): + - **Description:** Additional policy files or directories to load. + - **Default:** `[]` + - **Requires restart:** Yes + #### `general` - **`general.preferredEditor`** (string): @@ -447,6 +454,12 @@ their corresponding top-level category object in your `settings.json` file. "model": "gemini-2.5-flash" } }, + "gemini-3-flash-base": { + "extends": "base", + "modelConfig": { + "model": "gemini-3-flash-preview" + } + }, "classifier": { "extends": "base", "modelConfig": { @@ -502,7 +515,7 @@ their corresponding top-level category object in your `settings.json` file. } }, "web-search": { - "extends": "gemini-2.5-flash-base", + "extends": "gemini-3-flash-base", "modelConfig": { "generateContentConfig": { "tools": [ @@ -514,7 +527,7 @@ their corresponding top-level category object in your `settings.json` file. } }, "web-fetch": { - "extends": "gemini-2.5-flash-base", + "extends": "gemini-3-flash-base", "modelConfig": { "generateContentConfig": { "tools": [ @@ -526,25 +539,25 @@ their corresponding top-level category object in your `settings.json` file. } }, "web-fetch-fallback": { - "extends": "gemini-2.5-flash-base", + "extends": "gemini-3-flash-base", "modelConfig": {} }, "loop-detection": { - "extends": "gemini-2.5-flash-base", + "extends": "gemini-3-flash-base", "modelConfig": {} }, "loop-detection-double-check": { "extends": "base", "modelConfig": { - "model": "gemini-2.5-pro" + "model": "gemini-3-pro-preview" } }, "llm-edit-fixer": { - "extends": "gemini-2.5-flash-base", + "extends": "gemini-3-flash-base", "modelConfig": {} }, "next-speaker-checker": { - "extends": "gemini-2.5-flash-base", + "extends": "gemini-3-flash-base", "modelConfig": {} }, "chat-compression-3-pro": { @@ -574,7 +587,7 @@ their corresponding top-level category object in your `settings.json` file. }, "chat-compression-default": { "modelConfig": { - "model": "gemini-2.5-pro" + "model": "gemini-3-pro-preview" } } } @@ -1284,7 +1297,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file. few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. - - `strict`: Uses a strict profile that declines operations by default. + - `restrictive-open`: Declines operations by default, allows network. + - `strict-open`: Restricts both reads and writes to the working directory, + allows network. + - `strict-proxied`: Same as `strict-open` but routes network through proxy. - ``: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-.sb` in your project's `.gemini/` directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`). diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md index ef1d6b0ec8..1acf497659 100644 --- a/docs/get-started/installation.md +++ b/docs/get-started/installation.md @@ -1,43 +1,98 @@ -# Gemini CLI installation, execution, and deployment +# Gemini CLI installation, execution, and releases -Install and run Gemini CLI. This document provides an overview of Gemini CLI's -installation methods and deployment architecture. +This document provides an overview of Gemini CLI's sytem requriements, +installation methods, and release types. -## How to install and/or run Gemini CLI +## Recommended system specifications -There are several ways to run Gemini CLI. The recommended option depends on how -you intend to use Gemini CLI. +- **Operating System:** + - macOS 15+ + - Windows 11 24H2+ + - Ubuntu 20.04+ +- **Hardware:** + - "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits) + - "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context) +- **Runtime:** Node.js 20.0.0+ +- **Shell:** Bash or Zsh +- **Location:** + [Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas) +- **Internet connection required** -- As a standard installation. This is the most straightforward method of using - Gemini CLI. +## Install Gemini CLI + +We recommend most users install Gemini CLI using one of the following +installation methods: + +- npm +- Homebrew +- MacPorts +- Anaconda + +Note that Gemini CLI comes pre-installed on +[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and +[**Cloud Workstations**](https://cloud.google.com/workstations). + +### Install globally with npm + +```bash +npm install -g @google/gemini-cli +``` + +### Install globally with Homebrew (macOS/Linux) + +```bash +brew install gemini-cli +``` + +### Install globally with MacPorts (macOS) + +```bash +sudo port install gemini-cli +``` + +### Install with Anaconda (for restricted environments) + +```bash +# Create and activate a new environment +conda create -y -n gemini_env -c conda-forge nodejs +conda activate gemini_env + +# Install Gemini CLI globally via npm (inside the environment) +npm install -g @google/gemini-cli +``` + +## Run Gemini CLI + +For most users, we recommend running Gemini CLI with the `gemini` command: + +```bash +gemini +``` + +For a list of options and additional commands, see the +[CLI cheatsheet](/docs/cli/cli-reference.md). + +You can also run Gemini CLI using one of the following advanced methods: + +- Run instantly with npx. You can run Gemini CLI without permanent installation. - In a sandbox. This method offers increased security and isolation. - From the source. This is recommended for contributors to the project. -### 1. Standard installation (recommended for standard users) +### Run instantly with npx -This is the recommended way for end-users to install Gemini CLI. It involves -downloading the Gemini CLI package from the NPM registry. +```bash +# Using npx (no installation required) +npx @google/gemini-cli +``` -- **Global install:** +You can also execute the CLI directly from the main branch on GitHub, which is +helpful for testing features still in development: - ```bash - npm install -g @google/gemini-cli - ``` +```bash +npx https://github.com/google-gemini/gemini-cli +``` - Then, run the CLI from anywhere: - - ```bash - gemini - ``` - -- **NPX execution:** - - ```bash - # Execute the latest version from NPM without a global install - npx @google/gemini-cli - ``` - -### 2. Run in a sandbox (Docker/Podman) +### Run in a sandbox (Docker/Podman) For security and isolation, Gemini CLI can be run inside a container. This is the default way that the CLI executes tools that might have side effects. @@ -56,7 +111,7 @@ the default way that the CLI executes tools that might have side effects. gemini --sandbox -y -p "your prompt here" ``` -### 3. Run from source (recommended for Gemini CLI contributors) +### Run from source (recommended for Gemini CLI contributors) Contributors to the project will want to run the CLI directly from the source code. @@ -79,63 +134,41 @@ code. gemini ``` ---- +## Releases -### 4. Running the latest Gemini CLI commit from GitHub +Gemini CLI has three release channels: nightly, preview, and stable. For most +users, we recommend the stable release, which is the default installation. -You can run the most recently committed version of Gemini CLI directly from the -GitHub repository. This is useful for testing features still in development. +### Stable + +New stable releases are published each week. The stable release is the promotion +of last week's `preview` release along with any bug fixes. The stable release +uses `latest` tag, but omitting the tag also installs the latest stable release +by default: ```bash -# Execute the CLI directly from the main branch on GitHub -npx https://github.com/google-gemini/gemini-cli +# Both commands install the latest stable release. +npm install -g @google/gemini-cli +npm install -g @google/gemini-cli@latest ``` -## Deployment architecture +### Preview -The execution methods described above are made possible by the following -architectural components and processes: +New preview releases will be published each week. These releases are not fully +vetted and may contain regressions or other outstanding issues. Try out the +preview release by using the `preview` tag: -**NPM packages** +```bash +npm install -g @google/gemini-cli@preview +``` -Gemini CLI project is a monorepo that publishes two core packages to the NPM -registry: +### Nightly -- `@google/gemini-cli-core`: The backend, handling logic and tool execution. -- `@google/gemini-cli`: The user-facing frontend. +Nightly releases are published every day. The nightly release includes all +changes from the main branch at time of release. It should be assumed there are +pending validations and issues. You can help test the latest changes by +installing with the `nightly` tag: -These packages are used when performing the standard installation and when -running Gemini CLI from the source. - -**Build and packaging processes** - -There are two distinct build processes used, depending on the distribution -channel: - -- **NPM publication:** For publishing to the NPM registry, the TypeScript source - code in `@google/gemini-cli-core` and `@google/gemini-cli` is transpiled into - standard JavaScript using the TypeScript Compiler (`tsc`). The resulting - `dist/` directory is what gets published in the NPM package. This is a - standard approach for TypeScript libraries. - -- **GitHub `npx` execution:** When running the latest version of Gemini CLI - directly from GitHub, a different process is triggered by the `prepare` script - in `package.json`. This script uses `esbuild` to bundle the entire application - and its dependencies into a single, self-contained JavaScript file. This - bundle is created on-the-fly on the user's machine and is not checked into the - repository. - -**Docker sandbox image** - -The Docker-based execution method is supported by the `gemini-cli-sandbox` -container image. This image is published to a container registry and contains a -pre-installed, global version of Gemini CLI. - -## Release process - -The release process is automated through GitHub Actions. The release workflow -performs the following actions: - -1. Build the NPM packages using `tsc`. -2. Publish the NPM packages to the artifact registry. -3. Create GitHub releases with bundled assets. +```bash +npm install -g @google/gemini-cli@nightly +``` diff --git a/docs/sidebar.json b/docs/sidebar.json index d6f884204a..376fba2acd 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -45,6 +45,10 @@ { "label": "Checkpointing", "slug": "docs/cli/checkpointing" }, { "label": "Custom commands", "slug": "docs/cli/custom-commands" }, { "label": "Enterprise features", "slug": "docs/cli/enterprise" }, + { + "label": "Enterprise admin controls", + "slug": "docs/admin/enterprise-controls" + }, { "label": "Headless mode & scripting", "slug": "docs/cli/headless" }, { "label": "Sandboxing", "slug": "docs/cli/sandbox" }, { "label": "System prompt override", "slug": "docs/cli/system-prompt" }, diff --git a/docs/tools/ask-user.md b/docs/tools/ask-user.md new file mode 100644 index 0000000000..ad6c3b5a06 --- /dev/null +++ b/docs/tools/ask-user.md @@ -0,0 +1,95 @@ +# Ask User Tool + +The `ask_user` tool allows the agent to ask you one or more questions to gather +preferences, clarify requirements, or make decisions. It supports multiple +question types including multiple-choice, free-form text, and Yes/No +confirmation. + +## `ask_user` (Ask User) + +- **Tool name:** `ask_user` +- **Display name:** Ask User +- **File:** `ask-user.ts` +- **Parameters:** + - `questions` (array of objects, required): A list of 1 to 4 questions to ask. + Each question object has the following properties: + - `question` (string, required): The complete question text. + - `header` (string, required): A short label (max 16 chars) displayed as a + chip/tag (e.g., "Auth", "Database"). + - `type` (string, optional): The type of question. Defaults to `'choice'`. + - `'choice'`: Multiple-choice with options (supports multi-select). + - `'text'`: Free-form text input. + - `'yesno'`: Yes/No confirmation. + - `options` (array of objects, optional): Required for `'choice'` type. 2-4 + selectable options. + - `label` (string, required): Display text (1-5 words). + - `description` (string, required): Brief explanation. + - `multiSelect` (boolean, optional): For `'choice'` type, allows selecting + multiple options. + - `placeholder` (string, optional): Hint text for input fields. + +- **Behavior:** + - Presents an interactive dialog to the user with the specified questions. + - Pauses execution until the user provides answers or dismisses the dialog. + - Returns the user's answers to the model. + +- **Output (`llmContent`):** A JSON string containing the user's answers, + indexed by question position (e.g., + `{"answers":{"0": "Option A", "1": "Some text"}}`). + +- **Confirmation:** Yes. The tool inherently involves user interaction. + +## Usage Examples + +### Multiple Choice Question + +```json +{ + "questions": [ + { + "header": "Database", + "question": "Which database would you like to use?", + "type": "choice", + "options": [ + { + "label": "PostgreSQL", + "description": "Powerful, open source object-relational database system." + }, + { + "label": "SQLite", + "description": "C-library that implements a SQL database engine." + } + ] + } + ] +} +``` + +### Text Input Question + +```json +{ + "questions": [ + { + "header": "Project Name", + "question": "What is the name of your new project?", + "type": "text", + "placeholder": "e.g., my-awesome-app" + } + ] +} +``` + +### Yes/No Question + +```json +{ + "questions": [ + { + "header": "Deploy", + "question": "Do you want to deploy the application now?", + "type": "yesno" + } + ] +} +``` diff --git a/docs/tools/index.md b/docs/tools/index.md index ff594056ac..c7b2c1fc72 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -87,6 +87,8 @@ Gemini CLI's built-in tools can be broadly categorized as follows: - **[Todo Tool](./todos.md) (`write_todos`):** For managing subtasks of complex requests. - **[Planning Tools](./planning.md):** For entering and exiting Plan Mode. +- **[Ask User Tool](./ask-user.md) (`ask_user`):** For gathering user input and + making decisions. Additionally, these tools incorporate: diff --git a/docs/tools/mcp-server.md b/docs/tools/mcp-server.md index eb246fd86f..dd3842759c 100644 --- a/docs/tools/mcp-server.md +++ b/docs/tools/mcp-server.md @@ -739,21 +739,10 @@ The MCP integration tracks several states: cautiously and only for servers you completely control - **Access tokens:** Be security-aware when configuring environment variables containing API keys or tokens -- **Environment variable redaction:** By default, the Gemini CLI redacts - sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`, - and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when - spawning MCP servers using the `stdio` transport. This prevents unintended - exposure of your credentials to third-party servers. -- **Explicit environment variables:** If you need to pass a specific environment - variable to an MCP server, you should define it explicitly in the `env` - property of the server configuration in `settings.json`. - **Sandbox compatibility:** When using sandboxing, ensure MCP servers are - available within the sandbox environment. + available within the sandbox environment - **Private data:** Using broadly scoped personal access tokens can lead to information leakage between repositories. -- **Untrusted servers:** Be extremely cautious when adding MCP servers from - untrusted or third-party sources. Malicious servers could attempt to - exfiltrate data or perform unauthorized actions through the tools they expose. ### Performance and resource management diff --git a/docs/tools/shell.md b/docs/tools/shell.md index 0bb4b68244..48854e82f1 100644 --- a/docs/tools/shell.md +++ b/docs/tools/shell.md @@ -167,10 +167,11 @@ configuration file. `"tools": {"core": ["run_shell_command(git)"]}` will only allow `git` commands. Including the generic `run_shell_command` acts as a wildcard, allowing any command not explicitly blocked. -- `tools.exclude`: To block specific commands, add entries to the `exclude` list - under the `tools` category in the format `run_shell_command()`. For - example, `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` - commands. +- `tools.exclude` [DEPRECATED]: To block specific commands, use the + [Policy Engine](../core/policy-engine.md). Historically, this setting allowed + adding entries to the `exclude` list under the `tools` category in the format + `run_shell_command()`. For example, + `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands. The validation logic is designed to be secure and flexible: diff --git a/eslint.config.js b/eslint.config.js index 7839ae78f6..48af3775f2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -239,6 +239,18 @@ export default tseslint.config( ], }, }, + { + files: ['packages/sdk/src/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + { + name: '@google/gemini-cli-sdk', + message: 'Please use relative imports within the @google/gemini-cli-sdk package.', + }, + ], + }, + }, { files: ['packages/*/src/**/*.test.{ts,tsx}'], plugins: { diff --git a/evals/frugalSearch.eval.ts b/evals/frugalSearch.eval.ts index e4f3e85956..11c51e8529 100644 --- a/evals/frugalSearch.eval.ts +++ b/evals/frugalSearch.eval.ts @@ -7,6 +7,11 @@ import { describe, expect } from 'vitest'; import { evalTest } from './test-helper.js'; +/** + * Evals to verify that the agent uses search tools efficiently (frugally) + * by utilizing limiting parameters like `total_max_matches` and `max_matches_per_file`. + * This ensures the agent doesn't flood the context window with unnecessary search results. + */ describe('Frugal Search', () => { const getGrepParams = (call: any): any => { let args = call.toolRequest.args; @@ -112,21 +117,26 @@ describe('Frugal Search', () => { expect(grepCalls.length).toBeGreaterThan(0); - const hasFrugalLimit = grepCalls.some((call) => { - const params = getGrepParams(call); - // Check for explicitly set small limit for "sample" or "example" requests - return ( - params.total_max_matches !== undefined && - params.total_max_matches <= 100 - ); - }); + const grepParams = grepCalls.map(getGrepParams); + const hasTotalMaxLimit = grepParams.some( + (p) => p.total_max_matches !== undefined && p.total_max_matches <= 100, + ); expect( - hasFrugalLimit, - `Expected agent to use a small total_max_matches for a sample usage request. Params used: ${JSON.stringify( - grepCalls.map(getGrepParams), - null, - 2, + hasTotalMaxLimit, + `Expected agent to use a small total_max_matches (<= 100) for a sample usage request. Actual values: ${JSON.stringify( + grepParams.map((p) => p.total_max_matches), + )}`, + ).toBe(true); + + const hasMaxMatchesPerFileLimit = grepParams.some( + (p) => + p.max_matches_per_file !== undefined && p.max_matches_per_file <= 5, + ); + expect( + hasMaxMatchesPerFileLimit, + `Expected agent to use a small max_matches_per_file (<= 5) for a sample usage request. Actual values: ${JSON.stringify( + grepParams.map((p) => p.max_matches_per_file), )}`, ).toBe(true); }, diff --git a/package-lock.json b/package-lock.json index e8bb6e6902..6450eced3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1389,6 +1389,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@google/gemini-cli-sdk": { + "resolved": "packages/sdk", + "link": true + }, "node_modules/@google/gemini-cli-test-utils": { "resolved": "packages/test-utils", "link": true @@ -17245,6 +17249,7 @@ "@google/gemini-cli-core": "file:../core", "express": "^5.1.0", "fs-extra": "^11.3.0", + "strip-json-comments": "^3.1.1", "tar": "^7.5.2", "uuid": "^13.0.0", "winston": "^3.17.0" @@ -17253,6 +17258,7 @@ "gemini-cli-a2a-server": "dist/a2a-server.mjs" }, "devDependencies": { + "@google/genai": "^1.30.0", "@types/express": "^5.0.3", "@types/fs-extra": "^11.0.4", "@types/supertest": "^6.0.3", @@ -17555,6 +17561,23 @@ "uuid": "dist-node/bin/uuid" } }, + "packages/sdk": { + "name": "@google/gemini-cli-sdk", + "version": "0.29.0-nightly.20260203.71f46f116", + "license": "Apache-2.0", + "dependencies": { + "@google/gemini-cli-core": "file:../core", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.1" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } + }, "packages/test-utils": { "name": "@google/gemini-cli-test-utils", "version": "0.30.0-nightly.20260210.a2174751d", diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index 774b2f5c83..96001898a0 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -30,11 +30,13 @@ "@google/gemini-cli-core": "file:../core", "express": "^5.1.0", "fs-extra": "^11.3.0", + "strip-json-comments": "^3.1.1", "tar": "^7.5.2", "uuid": "^13.0.0", "winston": "^3.17.0" }, "devDependencies": { + "@google/genai": "^1.30.0", "@types/express": "^5.0.3", "@types/fs-extra": "^11.0.4", "@types/supertest": "^6.0.3", diff --git a/packages/cli/src/commands/mcp/add.ts b/packages/cli/src/commands/mcp/add.ts index 7d744a1daa..98e6a70879 100644 --- a/packages/cli/src/commands/mcp/add.ts +++ b/packages/cli/src/commands/mcp/add.ts @@ -128,13 +128,6 @@ async function addMcpServer( settings.setValue(settingsScope, 'mcpServers', mcpServers); - if (transport === 'stdio') { - debugLogger.warn( - 'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' + - 'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.', - ); - } - if (isExistingServer) { debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`); } else { diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 6614fe2af0..8c3cd9900c 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -141,6 +141,10 @@ vi.mock('@google/gemini-cli-core', async () => { defaultDecision: ServerConfig.PolicyDecision.ASK_USER, approvalMode: ServerConfig.ApprovalMode.DEFAULT, })), + getAdminErrorMessage: vi.fn( + (_feature) => + `YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli`, + ), isHeadlessMode: vi.fn((opts) => { if (process.env['VITEST'] === 'true') { return ( @@ -3192,6 +3196,26 @@ describe('Policy Engine Integration in loadCliConfig', () => { expect.anything(), ); }); + + it('should pass user-provided policy paths from --policy flag to createPolicyEngineConfig', async () => { + process.argv = [ + 'node', + 'script.js', + '--policy', + '/path/to/policy1.toml,/path/to/policy2.toml', + ]; + const settings = createTestMergedSettings(); + const argv = await parseArguments(settings); + + await loadCliConfig(settings, 'test-session', argv); + + expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith( + expect.objectContaining({ + policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'], + }), + expect.anything(), + ); + }); }); describe('loadCliConfig disableYoloMode', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 87eb1e8fa7..b7b5dfc7d9 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -75,6 +75,7 @@ export interface CliArgs { yolo: boolean | undefined; approvalMode: string | undefined; + policy: string[] | undefined; allowedMcpServerNames: string[] | undefined; allowedTools: string[] | undefined; experimentalAcp: boolean | undefined; @@ -158,6 +159,21 @@ export async function parseArguments( description: 'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools), plan (read-only mode)', }) + .option('policy', { + type: 'array', + string: true, + nargs: 1, + description: + 'Additional policy files or directories to load (comma-separated or multiple --policy)', + coerce: (policies: string[]) => + // Handle comma-separated values + policies.flatMap((p) => + p + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + ), + }) .option('experimental-acp', { type: 'boolean', description: 'Starts the agent in ACP mode', @@ -177,7 +193,8 @@ export async function parseArguments( type: 'array', string: true, nargs: 1, - description: 'Tools that are allowed to run without confirmation', + description: + '[DEPRECATED: Use Policy Engine instead See https://geminicli.com/docs/core/policy-engine] Tools that are allowed to run without confirmation', coerce: (tools: string[]) => // Handle comma-separated values tools.flatMap((tool) => tool.split(',').map((t) => t.trim())), @@ -445,7 +462,11 @@ export async function loadCliConfig( process.env['VITEST'] === 'true' ? false : (settings.security?.folderTrust?.enabled ?? false); - const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false; + const trustedFolder = + isWorkspaceTrusted(settings, cwd, undefined, { + prompt: argv.prompt, + query: argv.query, + })?.isTrusted ?? false; // Set the context filename in the server's memoryTool module BEFORE loading memory // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed @@ -602,8 +623,7 @@ export async function loadCliConfig( const interactive = !!argv.promptInteractive || !!argv.experimentalAcp || - (!isHeadlessMode({ prompt: argv.prompt }) && - !argv.query && + (!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) && !argv.isCommand); const allowedTools = argv.allowedTools || settings.tools?.allowed || []; @@ -666,6 +686,7 @@ export async function loadCliConfig( ...settings.mcp, allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed, }, + policyPaths: argv.policy, }; const policyEngineConfig = await createPolicyEngineConfig( diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index 96e50f36d6..94ceba1858 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -129,7 +129,7 @@ export type KeyBindingConfig = { export const defaultKeyBindings: KeyBindingConfig = { // Basic Controls [Command.RETURN]: [{ key: 'return' }], - [Command.ESCAPE]: [{ key: 'escape' }], + [Command.ESCAPE]: [{ key: 'escape' }, { key: '[', ctrl: true }], [Command.QUIT]: [{ key: 'c', ctrl: true }], [Command.EXIT]: [{ key: 'd', ctrl: true }], @@ -286,10 +286,7 @@ export const defaultKeyBindings: KeyBindingConfig = { [Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [{ key: 'tab', shift: false }], [Command.BACKGROUND_SHELL_SELECT]: [{ key: 'return' }], [Command.BACKGROUND_SHELL_ESCAPE]: [{ key: 'escape' }], - [Command.SHOW_MORE_LINES]: [ - { key: 'o', ctrl: true }, - { key: 's', ctrl: true }, - ], + [Command.SHOW_MORE_LINES]: [{ key: 'o', ctrl: true }], [Command.EXPAND_PASTE]: [{ key: 'o', ctrl: true }], [Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }], [Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab', shift: true }], @@ -501,7 +498,7 @@ export const commandDescriptions: Readonly> = { [Command.CYCLE_APPROVAL_MODE]: 'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).', [Command.SHOW_MORE_LINES]: - 'Expand a height-constrained response to show additional lines when not in alternate buffer mode.', + 'Expand and collapse blocks of content when not in alternate buffer mode.', [Command.EXPAND_PASTE]: 'Expand or collapse a paste placeholder when cursor is over placeholder.', [Command.BACKGROUND_SHELL_SELECT]: @@ -516,12 +513,12 @@ export const commandDescriptions: Readonly> = { [Command.UNFOCUS_BACKGROUND_SHELL_LIST]: 'Move focus from background shell list to Gemini.', [Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: - 'Show warning when trying to unfocus background shell via Tab.', + 'Show warning when trying to move focus away from background shell.', [Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: - 'Show warning when trying to unfocus shell input via Tab.', + 'Show warning when trying to move focus away from shell input.', [Command.FOCUS_SHELL_INPUT]: 'Move focus from Gemini to the active shell.', [Command.UNFOCUS_SHELL_INPUT]: 'Move focus from the shell back to Gemini.', [Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.', [Command.RESTART_APP]: 'Restart the application.', - [Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).', + [Command.SUSPEND_APP]: 'Suspend the CLI and move it to the background.', }; diff --git a/packages/cli/src/config/policy-engine.integration.test.ts b/packages/cli/src/config/policy-engine.integration.test.ts index 0568aa62bc..2c7ce599da 100644 --- a/packages/cli/src/config/policy-engine.integration.test.ts +++ b/packages/cli/src/config/policy-engine.integration.test.ts @@ -336,9 +336,9 @@ describe('Policy Engine Integration Tests', () => { // Valid plan file paths const validPaths = [ - '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/my-plan.md', - '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/feature_auth.md', - '/home/user/.gemini/tmp/new-temp_dir_123/plans/plan.md', // new style of temp directory + '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md', + '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md', + '/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory ]; for (const file_path of validPaths) { @@ -365,7 +365,6 @@ describe('Policy Engine Integration Tests', () => { '/project/src/file.ts', // Workspace '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal - '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/subdir/plan.md', // Subdirectory '/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir ]; diff --git a/packages/cli/src/config/policy.ts b/packages/cli/src/config/policy.ts index e9f19f43db..70536070eb 100644 --- a/packages/cli/src/config/policy.ts +++ b/packages/cli/src/config/policy.ts @@ -25,6 +25,7 @@ export async function createPolicyEngineConfig( mcp: settings.mcp, tools: settings.tools, mcpServers: settings.mcpServers, + policyPaths: settings.policyPaths, }; return createCorePolicyEngineConfig(policySettings, approvalMode); diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 721458952f..e88c9104dd 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2546,6 +2546,50 @@ describe('Settings Loading and Merging', () => { }); }); + describe('Reactivity & Snapshots', () => { + let loadedSettings: LoadedSettings; + + beforeEach(() => { + const emptySettingsFile: SettingsFile = { + path: '/mock/path', + settings: {}, + originalSettings: {}, + }; + + loadedSettings = new LoadedSettings( + { ...emptySettingsFile, path: getSystemSettingsPath() }, + { ...emptySettingsFile, path: getSystemDefaultsPath() }, + { ...emptySettingsFile, path: USER_SETTINGS_PATH }, + { ...emptySettingsFile, path: MOCK_WORKSPACE_SETTINGS_PATH }, + true, // isTrusted + [], + ); + }); + + it('getSnapshot() should return stable reference if no changes occur', () => { + const snap1 = loadedSettings.getSnapshot(); + const snap2 = loadedSettings.getSnapshot(); + expect(snap1).toBe(snap2); + }); + + it('setValue() should create a new snapshot reference and emit event', () => { + const oldSnapshot = loadedSettings.getSnapshot(); + const oldUserRef = oldSnapshot.user.settings; + + loadedSettings.setValue(SettingScope.User, 'ui.theme', 'high-contrast'); + + const newSnapshot = loadedSettings.getSnapshot(); + + expect(newSnapshot).not.toBe(oldSnapshot); + expect(newSnapshot.user.settings).not.toBe(oldUserRef); + expect(newSnapshot.user.settings.ui?.theme).toBe('high-contrast'); + + expect(newSnapshot.system.settings).not.toBe(oldSnapshot.system.settings); + + expect(mockCoreEvents.emitSettingsChanged).toHaveBeenCalled(); + }); + }); + describe('Security and Sandbox', () => { let originalArgv: string[]; let originalEnv: NodeJS.ProcessEnv; diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 8e9ff7380f..b2b526a010 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -10,6 +10,7 @@ import { platform } from 'node:os'; import * as dotenv from 'dotenv'; import process from 'node:process'; import { + CoreEvent, FatalConfigError, GEMINI_DIR, getErrorMessage, @@ -284,6 +285,20 @@ export function createTestMergedSettings( ) as MergedSettings; } +/** + * An immutable snapshot of settings state. + * Used with useSyncExternalStore for reactive updates. + */ +export interface LoadedSettingsSnapshot { + system: SettingsFile; + systemDefaults: SettingsFile; + user: SettingsFile; + workspace: SettingsFile; + isTrusted: boolean; + errors: SettingsError[]; + merged: MergedSettings; +} + export class LoadedSettings { constructor( system: SettingsFile, @@ -303,6 +318,7 @@ export class LoadedSettings { : this.createEmptyWorkspace(workspace); this.errors = errors; this._merged = this.computeMergedSettings(); + this._snapshot = this.computeSnapshot(); } readonly system: SettingsFile; @@ -314,6 +330,7 @@ export class LoadedSettings { private _workspaceFile: SettingsFile; private _merged: MergedSettings; + private _snapshot: LoadedSettingsSnapshot; private _remoteAdminSettings: Partial | undefined; get merged(): MergedSettings { @@ -368,6 +385,36 @@ export class LoadedSettings { return merged; } + private computeSnapshot(): LoadedSettingsSnapshot { + const cloneSettingsFile = (file: SettingsFile): SettingsFile => ({ + path: file.path, + rawJson: file.rawJson, + settings: structuredClone(file.settings), + originalSettings: structuredClone(file.originalSettings), + }); + return { + system: cloneSettingsFile(this.system), + systemDefaults: cloneSettingsFile(this.systemDefaults), + user: cloneSettingsFile(this.user), + workspace: cloneSettingsFile(this.workspace), + isTrusted: this.isTrusted, + errors: [...this.errors], + merged: structuredClone(this._merged), + }; + } + + // Passing this along with getSnapshot to useSyncExternalStore allows for idiomatic reactivity on settings changes + // React will pass a listener fn into this subscribe fn + // that listener fn will perform an object identity check on the snapshot and trigger a React re render if the snapshot has changed + subscribe(listener: () => void): () => void { + coreEvents.on(CoreEvent.SettingsChanged, listener); + return () => coreEvents.off(CoreEvent.SettingsChanged, listener); + } + + getSnapshot(): LoadedSettingsSnapshot { + return this._snapshot; + } + forScope(scope: LoadableSettingScope): SettingsFile { switch (scope) { case SettingScope.User: @@ -409,6 +456,7 @@ export class LoadedSettings { } this._merged = this.computeMergedSettings(); + this._snapshot = this.computeSnapshot(); coreEvents.emitSettingsChanged(); } diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 07d2faec49..b486956211 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -152,6 +152,18 @@ const SETTINGS_SCHEMA = { }, }, + policyPaths: { + type: 'array', + label: 'Policy Paths', + category: 'Advanced', + requiresRestart: true, + default: [] as string[], + description: 'Additional policy files or directories to load.', + showInDialog: false, + items: { type: 'string' }, + mergeStrategy: MergeStrategy.UNION, + }, + general: { type: 'object', label: 'General', diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index dff4610b90..892cd86e4b 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -449,6 +449,14 @@ describe('Trusted Folders', () => { false, ); }); + + it('should return true for isPathTrusted when isHeadlessMode is true', async () => { + const geminiCore = await import('@google/gemini-cli-core'); + vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true); + + const folders = loadTrustedFolders(); + expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true); + }); }); describe('Trusted Folders Caching', () => { diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 1f85684900..761bc368d3 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -17,6 +17,7 @@ import { homedir, isHeadlessMode, coreEvents, + type HeadlessModeOptions, } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; import stripJsonComments from 'strip-json-comments'; @@ -128,7 +129,11 @@ export class LoadedTrustedFolders { isPathTrusted( location: string, config?: Record, + headlessOptions?: HeadlessModeOptions, ): boolean | undefined { + if (isHeadlessMode(headlessOptions)) { + return true; + } const configToUse = config ?? this.user.config; // Resolve location to its realpath for canonical comparison @@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean { function getWorkspaceTrustFromLocalConfig( workspaceDir: string, trustConfig?: Record, + headlessOptions?: HeadlessModeOptions, ): TrustResult { const folders = loadTrustedFolders(); const configToUse = trustConfig ?? folders.user.config; @@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig( ); } - const isTrusted = folders.isPathTrusted(workspaceDir, configToUse); + const isTrusted = folders.isPathTrusted( + workspaceDir, + configToUse, + headlessOptions, + ); return { isTrusted, source: isTrusted !== undefined ? 'file' : undefined, @@ -357,8 +367,9 @@ export function isWorkspaceTrusted( settings: Settings, workspaceDir: string = process.cwd(), trustConfig?: Record, + headlessOptions?: HeadlessModeOptions, ): TrustResult { - if (isHeadlessMode()) { + if (isHeadlessMode(headlessOptions)) { return { isTrusted: true, source: undefined }; } @@ -372,5 +383,9 @@ export function isWorkspaceTrusted( } // Fall back to the local user configuration - return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig); + return getWorkspaceTrustFromLocalConfig( + workspaceDir, + trustConfig, + headlessOptions, + ); } diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 2e55c9b25d..9dac908a97 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -464,6 +464,7 @@ describe('gemini.tsx main function kitty protocol', () => { query: undefined, yolo: undefined, approvalMode: undefined, + policy: undefined, allowedMcpServerNames: undefined, allowedTools: undefined, experimentalAcp: undefined, diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index a18f3ace37..7b385453bf 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -89,7 +89,6 @@ import { SessionStatsProvider } from './ui/contexts/SessionContext.js'; import { VimModeProvider } from './ui/contexts/VimModeContext.js'; import { KeypressProvider } from './ui/contexts/KeypressContext.js'; import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js'; -import { useTerminalSize } from './ui/hooks/useTerminalSize.js'; import { relaunchAppInChildProcess, relaunchOnExitCode, @@ -104,6 +103,7 @@ import { TerminalProvider } from './ui/contexts/TerminalContext.js'; import { setupTerminalAndTheme } from './utils/terminalTheme.js'; import { profiler } from './ui/components/DebugProfiler.js'; import { runDeferredCommand } from './deferred.js'; +import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js'; const SLOW_RENDER_MS = 200; @@ -220,7 +220,6 @@ export async function startInteractiveUI( // Create wrapper component to use hooks inside render const AppWrapper = () => { useKittyKeyboardProtocol(); - const { columns, rows } = useTerminalSize(); return ( @@ -239,7 +238,6 @@ export async function startInteractiveUI( slashCommandConflictHandler.stop()); + const loadSettingsHandle = startupProfiler.start('load_settings'); const settings = loadSettings(); loadSettingsHandle?.end(); @@ -361,6 +364,26 @@ export async function main() { const argv = await parseArguments(settings.merged); parseArgsHandle?.end(); + if ( + (argv.allowedTools && argv.allowedTools.length > 0) || + (settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0) + ) { + coreEvents.emitFeedback( + 'warning', + 'Warning: --allowed-tools cli argument and tools.allowed in settings.json are deprecated and will be removed in 1.0: Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/', + ); + } + + if ( + settings.merged.tools?.exclude && + settings.merged.tools.exclude.length > 0 + ) { + coreEvents.emitFeedback( + 'warning', + 'Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/', + ); + } + if (argv.startupMessages) { argv.startupMessages.forEach((msg) => { coreEvents.emitFeedback('info', msg); diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts index 31dfdcace8..ea906a3da6 100644 --- a/packages/cli/src/services/CommandService.test.ts +++ b/packages/cli/src/services/CommandService.test.ts @@ -350,4 +350,117 @@ describe('CommandService', () => { expect(deployExtension).toBeDefined(); expect(deployExtension?.description).toBe('[gcp] Deploy to Google Cloud'); }); + + it('should report conflicts via getConflicts', async () => { + const builtinCommand = createMockCommand('deploy', CommandKind.BUILT_IN); + const extensionCommand = { + ...createMockCommand('deploy', CommandKind.FILE), + extensionName: 'firebase', + }; + + const mockLoader = new MockCommandLoader([ + builtinCommand, + extensionCommand, + ]); + + const service = await CommandService.create( + [mockLoader], + new AbortController().signal, + ); + + const conflicts = service.getConflicts(); + expect(conflicts).toHaveLength(1); + + expect(conflicts[0]).toMatchObject({ + name: 'deploy', + winner: builtinCommand, + losers: [ + { + renamedTo: 'firebase.deploy', + command: expect.objectContaining({ + name: 'deploy', + extensionName: 'firebase', + }), + }, + ], + }); + }); + + it('should report extension vs extension conflicts correctly', async () => { + // Both extensions try to register 'deploy' + const extension1Command = { + ...createMockCommand('deploy', CommandKind.FILE), + extensionName: 'firebase', + }; + const extension2Command = { + ...createMockCommand('deploy', CommandKind.FILE), + extensionName: 'aws', + }; + + const mockLoader = new MockCommandLoader([ + extension1Command, + extension2Command, + ]); + + const service = await CommandService.create( + [mockLoader], + new AbortController().signal, + ); + + const conflicts = service.getConflicts(); + expect(conflicts).toHaveLength(1); + + expect(conflicts[0]).toMatchObject({ + name: 'deploy', + winner: expect.objectContaining({ + name: 'deploy', + extensionName: 'firebase', + }), + losers: [ + { + renamedTo: 'aws.deploy', // ext2 is 'aws' and it lost because it was second in the list + command: expect.objectContaining({ + name: 'deploy', + extensionName: 'aws', + }), + }, + ], + }); + }); + + it('should report multiple conflicts for the same command name', async () => { + const builtinCommand = createMockCommand('deploy', CommandKind.BUILT_IN); + const ext1 = { + ...createMockCommand('deploy', CommandKind.FILE), + extensionName: 'ext1', + }; + const ext2 = { + ...createMockCommand('deploy', CommandKind.FILE), + extensionName: 'ext2', + }; + + const mockLoader = new MockCommandLoader([builtinCommand, ext1, ext2]); + + const service = await CommandService.create( + [mockLoader], + new AbortController().signal, + ); + + const conflicts = service.getConflicts(); + expect(conflicts).toHaveLength(1); + expect(conflicts[0].name).toBe('deploy'); + expect(conflicts[0].losers).toHaveLength(2); + expect(conflicts[0].losers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + renamedTo: 'ext1.deploy', + command: expect.objectContaining({ extensionName: 'ext1' }), + }), + expect.objectContaining({ + renamedTo: 'ext2.deploy', + command: expect.objectContaining({ extensionName: 'ext2' }), + }), + ]), + ); + }); }); diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts index 0e29a81d00..bd42226a32 100644 --- a/packages/cli/src/services/CommandService.ts +++ b/packages/cli/src/services/CommandService.ts @@ -4,10 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { debugLogger } from '@google/gemini-cli-core'; +import { debugLogger, coreEvents } from '@google/gemini-cli-core'; import type { SlashCommand } from '../ui/commands/types.js'; import type { ICommandLoader } from './types.js'; +export interface CommandConflict { + name: string; + winner: SlashCommand; + losers: Array<{ + command: SlashCommand; + renamedTo: string; + }>; +} + /** * Orchestrates the discovery and loading of all slash commands for the CLI. * @@ -23,8 +32,12 @@ export class CommandService { /** * Private constructor to enforce the use of the async factory. * @param commands A readonly array of the fully loaded and de-duplicated commands. + * @param conflicts A readonly array of conflicts that occurred during loading. */ - private constructor(private readonly commands: readonly SlashCommand[]) {} + private constructor( + private readonly commands: readonly SlashCommand[], + private readonly conflicts: readonly CommandConflict[], + ) {} /** * Asynchronously creates and initializes a new CommandService instance. @@ -63,11 +76,14 @@ export class CommandService { } const commandMap = new Map(); + const conflictsMap = new Map(); + for (const cmd of allCommands) { let finalName = cmd.name; // Extension commands get renamed if they conflict with existing commands if (cmd.extensionName && commandMap.has(cmd.name)) { + const winner = commandMap.get(cmd.name)!; let renamedName = `${cmd.extensionName}.${cmd.name}`; let suffix = 1; @@ -78,6 +94,19 @@ export class CommandService { } finalName = renamedName; + + if (!conflictsMap.has(cmd.name)) { + conflictsMap.set(cmd.name, { + name: cmd.name, + winner, + losers: [], + }); + } + + conflictsMap.get(cmd.name)!.losers.push({ + command: cmd, + renamedTo: finalName, + }); } commandMap.set(finalName, { @@ -86,8 +115,23 @@ export class CommandService { }); } + const conflicts = Array.from(conflictsMap.values()); + if (conflicts.length > 0) { + coreEvents.emitSlashCommandConflicts( + conflicts.flatMap((c) => + c.losers.map((l) => ({ + name: c.name, + renamedTo: l.renamedTo, + loserExtensionName: l.command.extensionName, + winnerExtensionName: c.winner.extensionName, + })), + ), + ); + } + const finalCommands = Object.freeze(Array.from(commandMap.values())); - return new CommandService(finalCommands); + const finalConflicts = Object.freeze(conflicts); + return new CommandService(finalCommands, finalConflicts); } /** @@ -101,4 +145,13 @@ export class CommandService { getCommands(): readonly SlashCommand[] { return this.commands; } + + /** + * Retrieves the list of conflicts that occurred during command loading. + * + * @returns A readonly array of command conflicts. + */ + getConflicts(): readonly CommandConflict[] { + return this.conflicts; + } } diff --git a/packages/cli/src/services/SlashCommandConflictHandler.ts b/packages/cli/src/services/SlashCommandConflictHandler.ts new file mode 100644 index 0000000000..31e110732b --- /dev/null +++ b/packages/cli/src/services/SlashCommandConflictHandler.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + coreEvents, + CoreEvent, + type SlashCommandConflictsPayload, +} from '@google/gemini-cli-core'; + +export class SlashCommandConflictHandler { + private notifiedConflicts = new Set(); + + constructor() { + this.handleConflicts = this.handleConflicts.bind(this); + } + + start() { + coreEvents.on(CoreEvent.SlashCommandConflicts, this.handleConflicts); + } + + stop() { + coreEvents.off(CoreEvent.SlashCommandConflicts, this.handleConflicts); + } + + private handleConflicts(payload: SlashCommandConflictsPayload) { + const newConflicts = payload.conflicts.filter((c) => { + const key = `${c.name}:${c.loserExtensionName}`; + if (this.notifiedConflicts.has(key)) { + return false; + } + this.notifiedConflicts.add(key); + return true; + }); + + if (newConflicts.length > 0) { + const conflictMessages = newConflicts + .map((c) => { + const winnerSource = c.winnerExtensionName + ? `extension '${c.winnerExtensionName}'` + : 'an existing command'; + return `- Command '/${c.name}' from extension '${c.loserExtensionName}' was renamed to '/${c.renamedTo}' because it conflicts with ${winnerSource}.`; + }) + .join('\n'); + + coreEvents.emitFeedback( + 'info', + `Command conflicts detected:\n${conflictMessages}`, + ); + } + } +} diff --git a/packages/cli/src/test-utils/mockConfig.ts b/packages/cli/src/test-utils/mockConfig.ts index ac2176c0e3..0a02e01889 100644 --- a/packages/cli/src/test-utils/mockConfig.ts +++ b/packages/cli/src/test-utils/mockConfig.ts @@ -18,6 +18,7 @@ export const createMockConfig = (overrides: Partial = {}): Config => getSandbox: vi.fn(() => undefined), getQuestion: vi.fn(() => ''), isInteractive: vi.fn(() => false), + isInitialized: vi.fn(() => true), setTerminalBackground: vi.fn(), storage: { getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'), diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 0c8eac325e..de0afc9c50 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -33,6 +33,9 @@ import { makeFakeConfig, type Config } from '@google/gemini-cli-core'; import { FakePersistentState } from './persistentStateFake.js'; import { AppContext, type AppState } from '../ui/contexts/AppContext.js'; import { createMockSettings } from './settings.js'; +import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js'; +import { DefaultLight } from '../ui/themes/default-light.js'; +import { pickDefaultThemeName } from '../ui/themes/theme.js'; export const persistentStateMock = new FakePersistentState(); @@ -150,7 +153,8 @@ const baseMockUiState = { terminalWidth: 120, terminalHeight: 40, currentModel: 'gemini-pro', - terminalBackgroundColor: undefined, + terminalBackgroundColor: 'black', + cleanUiDetailsVisible: false, activePtyId: undefined, backgroundShells: new Map(), backgroundShellHeight: 0, @@ -204,6 +208,10 @@ const mockUIActions: UIActions = { handleApiKeyCancel: vi.fn(), setBannerVisible: vi.fn(), setShortcutsHelpVisible: vi.fn(), + setCleanUiDetailsVisible: vi.fn(), + toggleCleanUiDetailsVisible: vi.fn(), + revealCleanUiDetailsTemporarily: vi.fn(), + handleWarning: vi.fn(), setEmbeddedShellFocused: vi.fn(), dismissBackgroundShell: vi.fn(), setActiveBackgroundShellPid: vi.fn(), @@ -293,6 +301,15 @@ export const renderWithProviders = ( mainAreaWidth, }; + themeManager.setTerminalBackground(baseState.terminalBackgroundColor); + const themeName = pickDefaultThemeName( + baseState.terminalBackgroundColor, + themeManager.getAllThemes(), + DEFAULT_THEME.name, + DefaultLight.name, + ); + themeManager.setActiveTheme(themeName); + const finalUIActions = { ...mockUIActions, ...uiActions }; const allToolCalls = (finalUiState.pendingHistoryItems || []) diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 6a19d80184..475a04e18e 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -66,6 +66,7 @@ describe('App', () => { const mockUIState: Partial = { streamingState: StreamingState.Idle, + cleanUiDetailsVisible: true, quittingMessages: null, dialogsVisible: false, mainControlsRef: { @@ -220,10 +221,6 @@ describe('App', () => { } as UIState; const configWithExperiment = makeFakeConfig(); - vi.spyOn( - configWithExperiment, - 'isEventDrivenSchedulerEnabled', - ).mockReturnValue(true); vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true); vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false); diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index b6fdd53325..028584537d 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -14,13 +14,13 @@ import { type Mock, type MockedObject, } from 'vitest'; -import { render } from '../test-utils/render.js'; +import { render, persistentStateMock } from '../test-utils/render.js'; import { waitFor } from '../test-utils/async.js'; import { cleanup } from 'ink-testing-library'; import { act, useContext, type ReactElement } from 'react'; import { AppContainer } from './AppContainer.js'; import { SettingsContext } from './contexts/SettingsContext.js'; -import { type TrackedToolCall } from './hooks/useReactToolScheduler.js'; +import { type TrackedToolCall } from './hooks/useToolScheduler.js'; import { type Config, makeFakeConfig, @@ -135,6 +135,7 @@ vi.mock('./hooks/vim.js'); vi.mock('./hooks/useFocus.js'); vi.mock('./hooks/useBracketedPaste.js'); vi.mock('./hooks/useLoadingIndicator.js'); +vi.mock('./hooks/useSuspend.js'); vi.mock('./hooks/useFolderTrust.js'); vi.mock('./hooks/useIdeTrustListener.js'); vi.mock('./hooks/useMessageQueue.js'); @@ -197,7 +198,9 @@ import { useTextBuffer } from './components/shared/text-buffer.js'; import { useLogger } from './hooks/useLogger.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; -import { useKeypress } from './hooks/useKeypress.js'; +import { useKeypress, type Key } from './hooks/useKeypress.js'; +import * as useKeypressModule from './hooks/useKeypress.js'; +import { useSuspend } from './hooks/useSuspend.js'; import { measureElement } from 'ink'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { @@ -270,6 +273,7 @@ describe('AppContainer State Management', () => { const mockedUseTextBuffer = useTextBuffer as Mock; const mockedUseLogger = useLogger as Mock; const mockedUseLoadingIndicator = useLoadingIndicator as Mock; + const mockedUseSuspend = useSuspend as Mock; const mockedUseInputHistoryStore = useInputHistoryStore as Mock; const mockedUseHookDisplayState = useHookDisplayState as Mock; const mockedUseTerminalTheme = useTerminalTheme as Mock; @@ -295,6 +299,7 @@ describe('AppContainer State Management', () => { }; beforeEach(() => { + persistentStateMock.reset(); vi.clearAllMocks(); mockIdeClient.getInstance.mockReturnValue(new Promise(() => {})); @@ -401,6 +406,9 @@ describe('AppContainer State Management', () => { elapsedTime: '0.0s', currentLoadingPhrase: '', }); + mockedUseSuspend.mockReturnValue({ + handleSuspend: vi.fn(), + }); mockedUseHookDisplayState.mockReturnValue([]); mockedUseTerminalTheme.mockReturnValue(undefined); mockedUseShellInactivityStatus.mockReturnValue({ @@ -440,8 +448,8 @@ describe('AppContainer State Management', () => { ...defaultMergedSettings.ui, showStatusInTitle: false, hideWindowTitle: false, + useAlternateBuffer: false, }, - useAlternateBuffer: false, }, } as unknown as LoadedSettings; @@ -481,6 +489,37 @@ describe('AppContainer State Management', () => { await waitFor(() => expect(capturedUIState).toBeTruthy()); unmount!(); }); + + it('shows full UI details by default', async () => { + let unmount: () => void; + await act(async () => { + const result = renderAppContainer(); + unmount = result.unmount; + }); + + await waitFor(() => { + expect(capturedUIState.cleanUiDetailsVisible).toBe(true); + }); + unmount!(); + }); + + it('starts in minimal UI mode when Focus UI preference is persisted', async () => { + persistentStateMock.get.mockReturnValueOnce(true); + + let unmount: () => void; + await act(async () => { + const result = renderAppContainer({ + settings: mockSettings, + }); + unmount = result.unmount; + }); + + await waitFor(() => { + expect(capturedUIState.cleanUiDetailsVisible).toBe(false); + }); + expect(persistentStateMock.get).toHaveBeenCalledWith('focusUiEnabled'); + unmount!(); + }); }); describe('State Initialization', () => { @@ -727,10 +766,10 @@ describe('AppContainer State Management', () => { getChatRecordingService: vi.fn(() => mockChatRecordingService), }; - const configWithRecording = { - ...mockConfig, - getGeminiClient: vi.fn(() => mockGeminiClient), - } as unknown as Config; + const configWithRecording = makeFakeConfig(); + vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue( + mockGeminiClient as unknown as ReturnType, + ); expect(() => { renderAppContainer({ @@ -761,11 +800,13 @@ describe('AppContainer State Management', () => { setHistory: vi.fn(), }; - const configWithRecording = { - ...mockConfig, - getGeminiClient: vi.fn(() => mockGeminiClient), - getSessionId: vi.fn(() => 'test-session-123'), - } as unknown as Config; + const configWithRecording = makeFakeConfig(); + vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue( + mockGeminiClient as unknown as ReturnType, + ); + vi.spyOn(configWithRecording, 'getSessionId').mockReturnValue( + 'test-session-123', + ); expect(() => { renderAppContainer({ @@ -801,10 +842,10 @@ describe('AppContainer State Management', () => { getUserTier: vi.fn(), }; - const configWithRecording = { - ...mockConfig, - getGeminiClient: vi.fn(() => mockGeminiClient), - } as unknown as Config; + const configWithRecording = makeFakeConfig(); + vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue( + mockGeminiClient as unknown as ReturnType, + ); renderAppContainer({ config: configWithRecording, @@ -835,10 +876,10 @@ describe('AppContainer State Management', () => { })), }; - const configWithClient = { - ...mockConfig, - getGeminiClient: vi.fn(() => mockGeminiClient), - } as unknown as Config; + const configWithClient = makeFakeConfig(); + vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue( + mockGeminiClient as unknown as ReturnType, + ); const resumedData = { conversation: { @@ -891,10 +932,10 @@ describe('AppContainer State Management', () => { getChatRecordingService: vi.fn(), }; - const configWithClient = { - ...mockConfig, - getGeminiClient: vi.fn(() => mockGeminiClient), - } as unknown as Config; + const configWithClient = makeFakeConfig(); + vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue( + mockGeminiClient as unknown as ReturnType, + ); const resumedData = { conversation: { @@ -944,10 +985,10 @@ describe('AppContainer State Management', () => { getUserTier: vi.fn(), }; - const configWithRecording = { - ...mockConfig, - getGeminiClient: vi.fn(() => mockGeminiClient), - } as unknown as Config; + const configWithRecording = makeFakeConfig(); + vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue( + mockGeminiClient as unknown as ReturnType, + ); renderAppContainer({ config: configWithRecording, @@ -1942,6 +1983,19 @@ describe('AppContainer State Management', () => { }); }); + describe('CTRL+Z', () => { + it('should call handleSuspend', async () => { + const handleSuspend = vi.fn(); + mockedUseSuspend.mockReturnValue({ handleSuspend }); + await setupKeypressTest(); + + pressKey('\x1A'); // Ctrl+Z + + expect(handleSuspend).toHaveBeenCalledTimes(1); + unmount(); + }); + }); + describe('Focus Handling (Tab / Shift+Tab)', () => { beforeEach(() => { // Mock activePtyId to enable focus @@ -2091,6 +2145,128 @@ describe('AppContainer State Management', () => { }); }); + describe('Shortcuts Help Visibility', () => { + let handleGlobalKeypress: (key: Key) => boolean; + let mockedUseKeypress: Mock; + let rerender: () => void; + let unmount: () => void; + + const setupShortcutsVisibilityTest = async () => { + const renderResult = renderAppContainer(); + await act(async () => { + vi.advanceTimersByTime(0); + }); + rerender = () => renderResult.rerender(getAppContainer()); + unmount = renderResult.unmount; + }; + + const pressKey = (key: Partial) => { + act(() => { + handleGlobalKeypress({ + name: 'r', + shift: false, + alt: false, + ctrl: false, + cmd: false, + insertable: false, + sequence: '', + ...key, + } as Key); + }); + rerender(); + }; + + beforeEach(() => { + mockedUseKeypress = vi.spyOn(useKeypressModule, 'useKeypress') as Mock; + mockedUseKeypress.mockImplementation( + (callback: (key: Key) => boolean, options: { isActive: boolean }) => { + // AppContainer registers multiple keypress handlers; capture only + // active handlers so inactive copy-mode handler doesn't override. + if (options?.isActive) { + handleGlobalKeypress = callback; + } + }, + ); + vi.useFakeTimers(); + }); + + afterEach(() => { + mockedUseKeypress.mockRestore(); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('dismisses shortcuts help when a registered hotkey is pressed', async () => { + await setupShortcutsVisibilityTest(); + + act(() => { + capturedUIActions.setShortcutsHelpVisible(true); + }); + rerender(); + expect(capturedUIState.shortcutsHelpVisible).toBe(true); + + pressKey({ name: 'r', ctrl: true, sequence: '\x12' }); // Ctrl+R + expect(capturedUIState.shortcutsHelpVisible).toBe(false); + + unmount(); + }); + + it('dismisses shortcuts help when streaming starts', async () => { + await setupShortcutsVisibilityTest(); + + act(() => { + capturedUIActions.setShortcutsHelpVisible(true); + }); + rerender(); + expect(capturedUIState.shortcutsHelpVisible).toBe(true); + + mockedUseGeminiStream.mockReturnValue({ + ...DEFAULT_GEMINI_STREAM_MOCK, + streamingState: 'responding', + }); + + await act(async () => { + rerender(); + }); + await waitFor(() => { + expect(capturedUIState.shortcutsHelpVisible).toBe(false); + }); + + unmount(); + }); + + it('dismisses shortcuts help when action-required confirmation appears', async () => { + await setupShortcutsVisibilityTest(); + + act(() => { + capturedUIActions.setShortcutsHelpVisible(true); + }); + rerender(); + expect(capturedUIState.shortcutsHelpVisible).toBe(true); + + mockedUseSlashCommandProcessor.mockReturnValue({ + handleSlashCommand: vi.fn(), + slashCommands: [], + pendingHistoryItems: [], + commandContext: {}, + shellConfirmationRequest: null, + confirmationRequest: { + prompt: 'Confirm this action?', + onConfirm: vi.fn(), + }, + }); + + await act(async () => { + rerender(); + }); + await waitFor(() => { + expect(capturedUIState.shortcutsHelpVisible).toBe(false); + }); + + unmount(); + }); + }); + describe('Copy Mode (CTRL+S)', () => { let rerender: () => void; let unmount: () => void; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 72fdb0ce48..1d91d44256 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -12,7 +12,14 @@ import { useRef, useLayoutEffect, } from 'react'; -import { type DOMElement, measureElement } from 'ink'; +import { + type DOMElement, + measureElement, + useApp, + useStdout, + useStdin, + type AppProps, +} from 'ink'; import { App } from './App.js'; import { AppContext } from './contexts/AppContext.js'; import { UIStateContext, type UIState } from './contexts/UIStateContext.js'; @@ -42,6 +49,7 @@ import { type UserTierId, type UserFeedbackPayload, type AgentDefinition, + type ApprovalMode, IdeClient, ideContextStore, getErrorMessage, @@ -87,7 +95,6 @@ import { useVimMode } from './contexts/VimModeContext.js'; import { useConsoleMessages } from './hooks/useConsoleMessages.js'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { calculatePromptWidths } from './components/InputPrompt.js'; -import { useApp, useStdout, useStdin } from 'ink'; import { calculateMainAreaWidth } from './utils/ui-sizing.js'; import ansiEscapes from 'ansi-escapes'; import { basename } from 'node:path'; @@ -127,6 +134,7 @@ import { ShellFocusContext } from './contexts/ShellFocusContext.js'; import { type ExtensionManager } from '../config/extension-manager.js'; import { requestConsentInteractive } from '../config/extensions/consent.js'; import { useSessionBrowser } from './hooks/useSessionBrowser.js'; +import { persistentState } from '../utils/persistentState.js'; import { useSessionResume } from './hooks/useSessionResume.js'; import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js'; import { isWorkspaceTrusted } from '../config/trustedFolders.js'; @@ -146,7 +154,8 @@ import { NewAgentsChoice } from './components/NewAgentsNotification.js'; import { isSlashCommand } from './utils/commandUtils.js'; import { useTerminalTheme } from './hooks/useTerminalTheme.js'; import { useTimedMessage } from './hooks/useTimedMessage.js'; -import { isITerm2 } from './utils/terminalUtils.js'; +import { shouldDismissShortcutsHelpOnHotkey } from './utils/shortcutsHelp.js'; +import { useSuspend } from './hooks/useSuspend.js'; function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) { return pendingHistoryItems.some((item) => { @@ -177,6 +186,9 @@ interface AppContainerProps { resumedSessionData?: ResumedSessionData; } +const APPROVAL_MODE_REVEAL_DURATION_MS = 1200; +const FOCUS_UI_ENABLED_STATE_KEY = 'focusUiEnabled'; + /** * The fraction of the terminal width to allocate to the shell. * This provides horizontal padding. @@ -200,6 +212,7 @@ export const AppContainer = (props: AppContainerProps) => { useMemoryMonitor(historyManager); const isAlternateBuffer = useAlternateBuffer(); const [corgiMode, setCorgiMode] = useState(false); + const [forceRerenderKey, setForceRerenderKey] = useState(0); const [debugMessage, setDebugMessage] = useState(''); const [quittingMessages, setQuittingMessages] = useState< HistoryItem[] | null @@ -346,7 +359,7 @@ export const AppContainer = (props: AppContainerProps) => { const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize(); const { stdin, setRawMode } = useStdin(); const { stdout } = useStdout(); - const app = useApp(); + const app: AppProps = useApp(); // Additional hooks moved from App.tsx const { stats: sessionStats } = useSessionStats(); @@ -483,7 +496,7 @@ export const AppContainer = (props: AppContainerProps) => { ); coreEvents.off(CoreEvent.AgentsDiscovered, handleAgentsDiscovered); }; - }, []); + }, [settings]); const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } = useConsoleMessages(); @@ -535,10 +548,13 @@ export const AppContainer = (props: AppContainerProps) => { setHistoryRemountKey((prev) => prev + 1); }, [setHistoryRemountKey, isAlternateBuffer, stdout]); + const shouldUseAlternateScreen = shouldEnterAlternateScreen( + isAlternateBuffer, + config.getScreenReader(), + ); + const handleEditorClose = useCallback(() => { - if ( - shouldEnterAlternateScreen(isAlternateBuffer, config.getScreenReader()) - ) { + if (shouldUseAlternateScreen) { // The editor may have exited alternate buffer mode so we need to // enter it again to be safe. enterAlternateScreen(); @@ -548,7 +564,7 @@ export const AppContainer = (props: AppContainerProps) => { } terminalCapabilityManager.enableSupportedModes(); refreshStatic(); - }, [refreshStatic, isAlternateBuffer, app, config]); + }, [refreshStatic, shouldUseAlternateScreen, app]); const [editorError, setEditorError] = useState(null); const { @@ -596,7 +612,7 @@ export const AppContainer = (props: AppContainerProps) => { ); // Poll for terminal background color changes to auto-switch theme - useTerminalTheme(handleThemeSelect, config); + useTerminalTheme(handleThemeSelect, config, refreshStatic); const { authState, @@ -785,7 +801,65 @@ Logging in with Google... Restarting Gemini CLI to continue. const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>( () => {}, ); + const [focusUiEnabledByDefault] = useState( + () => persistentState.get(FOCUS_UI_ENABLED_STATE_KEY) === true, + ); const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false); + const [cleanUiDetailsVisible, setCleanUiDetailsVisibleState] = useState( + !focusUiEnabledByDefault, + ); + const modeRevealTimeoutRef = useRef(null); + const cleanUiDetailsPinnedRef = useRef(!focusUiEnabledByDefault); + + const clearModeRevealTimeout = useCallback(() => { + if (modeRevealTimeoutRef.current) { + clearTimeout(modeRevealTimeoutRef.current); + modeRevealTimeoutRef.current = null; + } + }, []); + + const persistFocusUiPreference = useCallback((isFullUiVisible: boolean) => { + persistentState.set(FOCUS_UI_ENABLED_STATE_KEY, !isFullUiVisible); + }, []); + + const setCleanUiDetailsVisible = useCallback( + (visible: boolean) => { + clearModeRevealTimeout(); + cleanUiDetailsPinnedRef.current = visible; + setCleanUiDetailsVisibleState(visible); + persistFocusUiPreference(visible); + }, + [clearModeRevealTimeout, persistFocusUiPreference], + ); + + const toggleCleanUiDetailsVisible = useCallback(() => { + clearModeRevealTimeout(); + setCleanUiDetailsVisibleState((visible) => { + const nextVisible = !visible; + cleanUiDetailsPinnedRef.current = nextVisible; + persistFocusUiPreference(nextVisible); + return nextVisible; + }); + }, [clearModeRevealTimeout, persistFocusUiPreference]); + + const revealCleanUiDetailsTemporarily = useCallback( + (durationMs: number = APPROVAL_MODE_REVEAL_DURATION_MS) => { + if (cleanUiDetailsPinnedRef.current) { + return; + } + clearModeRevealTimeout(); + setCleanUiDetailsVisibleState(true); + modeRevealTimeoutRef.current = setTimeout(() => { + if (!cleanUiDetailsPinnedRef.current) { + setCleanUiDetailsVisibleState(false); + } + modeRevealTimeoutRef.current = null; + }, durationMs); + }, + [clearModeRevealTimeout], + ); + + useEffect(() => () => clearModeRevealTimeout(), [clearModeRevealTimeout]); const slashCommandActions = useMemo( () => ({ @@ -1046,11 +1120,25 @@ Logging in with Google... Restarting Gemini CLI to continue. const shouldShowActionRequiredTitle = inactivityStatus === 'action_required'; const shouldShowSilentWorkingTitle = inactivityStatus === 'silent_working'; + const handleApprovalModeChangeWithUiReveal = useCallback( + (mode: ApprovalMode) => { + void handleApprovalModeChange(mode); + if (!cleanUiDetailsVisible) { + revealCleanUiDetailsTemporarily(APPROVAL_MODE_REVEAL_DURATION_MS); + } + }, + [ + handleApprovalModeChange, + cleanUiDetailsVisible, + revealCleanUiDetailsTemporarily, + ], + ); + // Auto-accept indicator const showApprovalModeIndicator = useApprovalModeIndicator({ config, addItem: historyManager.addItem, - onApprovalModeChange: handleApprovalModeChange, + onApprovalModeChange: handleApprovalModeChangeWithUiReveal, isActive: !embeddedShellFocused, }); @@ -1366,9 +1454,30 @@ Logging in with Google... Restarting Gemini CLI to continue. if (tabFocusTimeoutRef.current) { clearTimeout(tabFocusTimeoutRef.current); } + if (modeRevealTimeoutRef.current) { + clearTimeout(modeRevealTimeoutRef.current); + } }; }, [showTransientMessage]); + const handleWarning = useCallback( + (message: string) => { + showTransientMessage({ + text: message, + type: TransientMessageType.Warning, + }); + }, + [showTransientMessage], + ); + + const { handleSuspend } = useSuspend({ + handleWarning, + setRawMode, + refreshStatic, + setForceRerenderKey, + shouldUseAlternateScreen, + }); + useEffect(() => { if (ideNeedsRestart) { // IDE trust changed, force a restart. @@ -1489,6 +1598,10 @@ Logging in with Google... Restarting Gemini CLI to continue. debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key)); } + if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) { + setShortcutsHelpVisible(false); + } + if (isAlternateBuffer && keyMatchers[Command.TOGGLE_COPY_MODE](key)) { setCopyModeEnabled(true); disableMouseEvents(); @@ -1505,6 +1618,17 @@ Logging in with Google... Restarting Gemini CLI to continue. } else if (keyMatchers[Command.EXIT](key)) { setCtrlDPressCount((prev) => prev + 1); return true; + } else if (keyMatchers[Command.SUSPEND_APP](key)) { + handleSuspend(); + } else if ( + keyMatchers[Command.TOGGLE_COPY_MODE](key) && + !isAlternateBuffer + ) { + showTransientMessage({ + text: 'Use Ctrl+O to expand and collapse blocks of content.', + type: TransientMessageType.Warning, + }); + return true; } let enteringConstrainHeightMode = false; @@ -1530,15 +1654,6 @@ Logging in with Google... Restarting Gemini CLI to continue. setShowErrorDetails((prev) => !prev); } return true; - } else if (keyMatchers[Command.SUSPEND_APP](key)) { - const undoMessage = isITerm2() - ? 'Undo has been moved to Option + Z' - : 'Undo has been moved to Alt/Option + Z or Cmd + Z'; - showTransientMessage({ - text: undoMessage, - type: TransientMessageType.Warning, - }); - return true; } else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) { setShowFullTodos((prev) => !prev); return true; @@ -1647,18 +1762,20 @@ Logging in with Google... Restarting Gemini CLI to continue. handleSlashCommand, cancelOngoingRequest, activePtyId, + handleSuspend, embeddedShellFocused, settings.merged.general.debugKeystrokeLogging, refreshStatic, setCopyModeEnabled, + tabFocusTimeoutRef, isAlternateBuffer, + shortcutsHelpVisible, backgroundCurrentShell, toggleBackgroundShell, backgroundShells, isBackgroundShellVisible, setIsBackgroundShellListOpen, lastOutputTimeRef, - tabFocusTimeoutRef, showTransientMessage, settings.merged.general.devtools, showErrorDetails, @@ -1811,6 +1928,36 @@ Logging in with Google... Restarting Gemini CLI to continue. [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], ); + const hasPendingToolConfirmation = useMemo( + () => isToolAwaitingConfirmation(pendingHistoryItems), + [pendingHistoryItems], + ); + + const hasPendingActionRequired = + hasPendingToolConfirmation || + !!commandConfirmationRequest || + !!authConsentRequest || + confirmUpdateExtensionRequests.length > 0 || + !!loopDetectionConfirmationRequest || + !!proQuotaRequest || + !!validationRequest || + !!customDialog; + + const isPassiveShortcutsHelpState = + isInputActive && + streamingState === StreamingState.Idle && + !hasPendingActionRequired; + + useEffect(() => { + if (shortcutsHelpVisible && !isPassiveShortcutsHelpState) { + setShortcutsHelpVisible(false); + } + }, [ + shortcutsHelpVisible, + isPassiveShortcutsHelpState, + setShortcutsHelpVisible, + ]); + const allToolCalls = useMemo( () => pendingHistoryItems @@ -1918,6 +2065,7 @@ Logging in with Google... Restarting Gemini CLI to continue. ctrlDPressedOnce: ctrlDPressCount >= 1, showEscapePrompt, shortcutsHelpVisible, + cleanUiDetailsVisible, isFocused, elapsedTime, currentLoadingPhrase, @@ -2028,6 +2176,7 @@ Logging in with Google... Restarting Gemini CLI to continue. ctrlDPressCount, showEscapePrompt, shortcutsHelpVisible, + cleanUiDetailsVisible, isFocused, elapsedTime, currentLoadingPhrase, @@ -2129,6 +2278,10 @@ Logging in with Google... Restarting Gemini CLI to continue. handleApiKeyCancel, setBannerVisible, setShortcutsHelpVisible, + setCleanUiDetailsVisible, + toggleCleanUiDetailsVisible, + revealCleanUiDetailsTemporarily, + handleWarning, setEmbeddedShellFocused, dismissBackgroundShell, setActiveBackgroundShellPid, @@ -2205,6 +2358,10 @@ Logging in with Google... Restarting Gemini CLI to continue. handleApiKeyCancel, setBannerVisible, setShortcutsHelpVisible, + setCleanUiDetailsVisible, + toggleCleanUiDetailsVisible, + revealCleanUiDetailsTemporarily, + handleWarning, setEmbeddedShellFocused, dismissBackgroundShell, setActiveBackgroundShellPid, @@ -2240,7 +2397,7 @@ Logging in with Google... Restarting Gemini CLI to continue. > - + diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts index 87ec04b730..0825527cf5 100644 --- a/packages/cli/src/ui/colors.ts +++ b/packages/cli/src/ui/colors.ts @@ -15,7 +15,7 @@ export const Colors: ColorsTheme = { return themeManager.getActiveTheme().colors.Foreground; }, get Background() { - return themeManager.getActiveTheme().colors.Background; + return themeManager.getColors().Background; }, get LightBlue() { return themeManager.getActiveTheme().colors.LightBlue; @@ -51,7 +51,7 @@ export const Colors: ColorsTheme = { return themeManager.getActiveTheme().colors.Gray; }, get DarkGray() { - return themeManager.getActiveTheme().colors.DarkGray; + return themeManager.getColors().DarkGray; }, get GradientColors() { return themeManager.getActiveTheme().colors.GradientColors; diff --git a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx index 68b662df7b..4edd41d66c 100644 --- a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx +++ b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx @@ -182,7 +182,6 @@ describe('AlternateBufferQuittingDisplay', () => { type: 'info', title: 'Confirm Tool', prompt: 'Confirm this action?', - onConfirm: async () => {}, }, }, ], diff --git a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx index fec35d46c3..8e0ede2e09 100644 --- a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx +++ b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.tsx @@ -12,18 +12,15 @@ import { QuittingDisplay } from './QuittingDisplay.js'; import { useAppContext } from '../contexts/AppContext.js'; import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js'; import { useConfirmingTool } from '../hooks/useConfirmingTool.js'; -import { useConfig } from '../contexts/ConfigContext.js'; import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js'; import { theme } from '../semantic-colors.js'; export const AlternateBufferQuittingDisplay = () => { const { version } = useAppContext(); const uiState = useUIState(); - const config = useConfig(); const confirmingTool = useConfirmingTool(); - const showPromptedTool = - config.isEventDrivenSchedulerEnabled() && confirmingTool !== null; + const showPromptedTool = confirmingTool !== null; // We render the entire chat history and header here to ensure that the // conversation history is visible to the user after the app quits and the @@ -56,7 +53,6 @@ export const AlternateBufferQuittingDisplay = () => { terminalWidth={uiState.mainAreaWidth} item={{ ...item, id: 0 }} isPending={true} - isFocused={false} activeShellPtyId={uiState.activePtyId} embeddedShellFocused={uiState.embeddedShellFocused} /> diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index 38b0f9b468..ad5e2f67d2 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -17,9 +17,10 @@ import { useTips } from '../hooks/useTips.js'; interface AppHeaderProps { version: string; + showDetails?: boolean; } -export const AppHeader = ({ version }: AppHeaderProps) => { +export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => { const settings = useSettings(); const config = useConfig(); const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState(); @@ -27,6 +28,14 @@ export const AppHeader = ({ version }: AppHeaderProps) => { const { bannerText } = useBanner(bannerData); const { showTips } = useTips(); + if (!showDetails) { + return ( + +
+ + ); + } + return ( {!(settings.merged.ui.hideBanner || config.getScreenReader()) && ( diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index b93db2a2af..b03f375f71 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -37,6 +37,7 @@ describe('AskUserDialog', () => { { question: 'Which authentication method should we use?', header: 'Auth', + type: QuestionType.CHOICE, options: [ { label: 'OAuth 2.0', description: 'Industry standard, supports SSO' }, { label: 'JWT tokens', description: 'Stateless, good for APIs' }, @@ -74,6 +75,7 @@ describe('AskUserDialog', () => { { question: 'Which features?', header: 'Features', + type: QuestionType.CHOICE, options: [ { label: 'TypeScript', description: '' }, { label: 'ESLint', description: '' }, @@ -171,6 +173,7 @@ describe('AskUserDialog', () => { { question: 'Which authentication method?', header: 'Auth', + type: QuestionType.CHOICE, options: [{ label: 'OAuth 2.0', description: '' }], multiSelect: false, }, @@ -228,6 +231,7 @@ describe('AskUserDialog', () => { { question: 'Choose an option', header: 'Scroll Test', + type: QuestionType.CHOICE, options: Array.from({ length: 15 }, (_, i) => ({ label: `Option ${i + 1}`, description: `Description ${i + 1}`, @@ -296,6 +300,7 @@ describe('AskUserDialog', () => { { question: 'Which database should we use?', header: 'Database', + type: QuestionType.CHOICE, options: [ { label: 'PostgreSQL', description: 'Relational database' }, { label: 'MongoDB', description: 'Document database' }, @@ -305,6 +310,7 @@ describe('AskUserDialog', () => { { question: 'Which ORM do you prefer?', header: 'ORM', + type: QuestionType.CHOICE, options: [ { label: 'Prisma', description: 'Type-safe ORM' }, { label: 'Drizzle', description: 'Lightweight ORM' }, @@ -359,12 +365,14 @@ describe('AskUserDialog', () => { { question: 'Which testing framework?', header: 'Testing', + type: QuestionType.CHOICE, options: [{ label: 'Vitest', description: 'Fast unit testing' }], multiSelect: false, }, { question: 'Which CI provider?', header: 'CI', + type: QuestionType.CHOICE, options: [ { label: 'GitHub Actions', description: 'Built into GitHub' }, ], @@ -402,12 +410,14 @@ describe('AskUserDialog', () => { { question: 'Which package manager?', header: 'Package', + type: QuestionType.CHOICE, options: [{ label: 'pnpm', description: 'Fast, disk efficient' }], multiSelect: false, }, { question: 'Which bundler?', header: 'Bundler', + type: QuestionType.CHOICE, options: [{ label: 'Vite', description: 'Next generation bundler' }], multiSelect: false, }, @@ -465,6 +475,7 @@ describe('AskUserDialog', () => { { question: 'Which framework?', header: 'Framework', + type: QuestionType.CHOICE, options: [ { label: 'React', description: 'Component library' }, { label: 'Vue', description: 'Progressive framework' }, @@ -474,6 +485,7 @@ describe('AskUserDialog', () => { { question: 'Which styling?', header: 'Styling', + type: QuestionType.CHOICE, options: [ { label: 'Tailwind', description: 'Utility-first CSS' }, { label: 'CSS Modules', description: 'Scoped styles' }, @@ -500,12 +512,14 @@ describe('AskUserDialog', () => { { question: 'Create tests?', header: 'Tests', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: 'Generate test files' }], multiSelect: false, }, { question: 'Add documentation?', header: 'Docs', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: 'Generate JSDoc comments' }], multiSelect: false, }, @@ -545,12 +559,14 @@ describe('AskUserDialog', () => { { question: 'Which license?', header: 'License', + type: QuestionType.CHOICE, options: [{ label: 'MIT', description: 'Permissive license' }], multiSelect: false, }, { question: 'Include README?', header: 'README', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: 'Generate README.md' }], multiSelect: false, }, @@ -580,12 +596,14 @@ describe('AskUserDialog', () => { { question: 'Target Node version?', header: 'Node', + type: QuestionType.CHOICE, options: [{ label: 'Node 20', description: 'LTS version' }], multiSelect: false, }, { question: 'Enable strict mode?', header: 'Strict', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: 'Strict TypeScript' }], multiSelect: false, }, @@ -727,6 +745,7 @@ describe('AskUserDialog', () => { { question: 'Should it be async?', header: 'Async', + type: QuestionType.CHOICE, options: [ { label: 'Yes', description: 'Use async/await' }, { label: 'No', description: 'Synchronous hook' }, @@ -773,6 +792,7 @@ describe('AskUserDialog', () => { { question: 'Which styling approach?', header: 'Style', + type: QuestionType.CHOICE, options: [ { label: 'CSS Modules', description: 'Scoped CSS' }, { label: 'Tailwind', description: 'Utility classes' }, @@ -895,6 +915,7 @@ describe('AskUserDialog', () => { { question: 'Choice Q?', header: 'Choice', + type: QuestionType.CHOICE, options: [{ label: 'Option 1', description: '' }], multiSelect: false, }, @@ -952,12 +973,14 @@ describe('AskUserDialog', () => { { question: 'Question 1?', header: 'Q1', + type: QuestionType.CHOICE, options: [{ label: 'A1', description: '' }], multiSelect: false, }, { question: 'Question 2?', header: 'Q2', + type: QuestionType.CHOICE, options: [{ label: 'A2', description: '' }], multiSelect: false, }, @@ -1008,6 +1031,7 @@ describe('AskUserDialog', () => { { question: 'Which option do you prefer?', header: 'Test', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: '' }], multiSelect: false, }, @@ -1036,6 +1060,7 @@ describe('AskUserDialog', () => { { question: 'Is **this** working?', header: 'Test', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: '' }], multiSelect: false, }, @@ -1067,6 +1092,7 @@ describe('AskUserDialog', () => { { question: 'Is **this** working?', header: 'Test', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: '' }], multiSelect: false, }, @@ -1096,6 +1122,7 @@ describe('AskUserDialog', () => { { question: 'Run `npm start`?', header: 'Test', + type: QuestionType.CHOICE, options: [{ label: 'Yes', description: '' }], multiSelect: false, }, @@ -1126,6 +1153,7 @@ describe('AskUserDialog', () => { { question: 'Choose an option', header: 'Context Test', + type: QuestionType.CHOICE, options: Array.from({ length: 10 }, (_, i) => ({ label: `Option ${i + 1}`, description: `Description ${i + 1}`, @@ -1162,6 +1190,7 @@ describe('AskUserDialog', () => { { question: longQuestion, header: 'Alternate Buffer Test', + type: QuestionType.CHOICE, options: [{ label: 'Option 1', description: 'Desc 1' }], multiSelect: false, }, @@ -1195,6 +1224,7 @@ describe('AskUserDialog', () => { { question: 'Select your preferred language:', header: 'Language', + type: QuestionType.CHOICE, options: [ { label: 'TypeScript', description: '' }, { label: 'JavaScript', description: '' }, @@ -1228,6 +1258,7 @@ describe('AskUserDialog', () => { { question: 'Select your preferred language:', header: 'Language', + type: QuestionType.CHOICE, options: [ { label: 'TypeScript', description: '' }, { label: 'JavaScript', description: '' }, diff --git a/packages/cli/src/ui/components/BubblingRegression.test.tsx b/packages/cli/src/ui/components/BubblingRegression.test.tsx index f91f6fe2dc..b91943b019 100644 --- a/packages/cli/src/ui/components/BubblingRegression.test.tsx +++ b/packages/cli/src/ui/components/BubblingRegression.test.tsx @@ -9,7 +9,7 @@ import { act } from 'react'; import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { AskUserDialog } from './AskUserDialog.js'; -import type { Question } from '@google/gemini-cli-core'; +import { QuestionType, type Question } from '@google/gemini-cli-core'; describe('Key Bubbling Regression', () => { afterEach(() => { @@ -20,6 +20,7 @@ describe('Key Bubbling Regression', () => { { question: 'Choice Q?', header: 'Choice', + type: QuestionType.CHOICE, options: [ { label: 'Option 1', description: '' }, { label: 'Option 2', description: '' }, diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index ee3a441c04..353e1ad535 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -4,9 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, afterEach } from 'vitest'; +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; import { render } from '../../test-utils/render.js'; import { Box, Text } from 'ink'; +import { useEffect } from 'react'; import { Composer } from './Composer.js'; import { UIStateContext, type UIState } from '../contexts/UIStateContext.js'; import { @@ -23,13 +24,18 @@ vi.mock('../contexts/VimModeContext.js', () => ({ vimMode: 'INSERT', })), })); -import { ApprovalMode } from '@google/gemini-cli-core'; +import { ApprovalMode, tokenLimit } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core'; import { StreamingState, ToolCallStatus } from '../types.js'; import { TransientMessageType } from '../../utils/events.js'; import type { LoadedSettings } from '../../config/settings.js'; import type { SessionMetrics } from '../contexts/SessionContext.js'; +const composerTestControls = vi.hoisted(() => ({ + suggestionsVisible: false, + isAlternateBuffer: false, +})); + // Mock child components vi.mock('./LoadingIndicator.js', () => ({ LoadingIndicator: ({ @@ -90,9 +96,19 @@ vi.mock('./DetailedMessagesDisplay.js', () => ({ })); vi.mock('./InputPrompt.js', () => ({ - InputPrompt: ({ placeholder }: { placeholder?: string }) => ( - InputPrompt: {placeholder} - ), + InputPrompt: ({ + placeholder, + onSuggestionsVisibilityChange, + }: { + placeholder?: string; + onSuggestionsVisibilityChange?: (visible: boolean) => void; + }) => { + useEffect(() => { + onSuggestionsVisibilityChange?.(composerTestControls.suggestionsVisible); + }, [onSuggestionsVisibilityChange]); + + return InputPrompt: {placeholder}; + }, calculatePromptWidths: vi.fn(() => ({ inputWidth: 80, suggestionsWidth: 40, @@ -100,6 +116,10 @@ vi.mock('./InputPrompt.js', () => ({ })), })); +vi.mock('../hooks/useAlternateBuffer.js', () => ({ + useAlternateBuffer: () => composerTestControls.isAlternateBuffer, +})); + vi.mock('./Footer.js', () => ({ Footer: () => Footer, })); @@ -154,15 +174,19 @@ const createMockUIState = (overrides: Partial = {}): UIState => ctrlDPressedOnce: false, showEscapePrompt: false, shortcutsHelpVisible: false, + cleanUiDetailsVisible: true, ideContextState: null, geminiMdFileCount: 0, renderMarkdown: true, filteredConsoleMessages: [], history: [], sessionStats: { + sessionId: 'test-session', + sessionStartTime: new Date(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metrics: {} as any, lastPromptTokenCount: 0, - sessionTokenCount: 0, - totalPrompts: 0, + promptCount: 0, }, branchName: 'main', debugMessage: '', @@ -187,8 +211,12 @@ const createMockUIActions = (): UIActions => handleFinalSubmit: vi.fn(), handleClearScreen: vi.fn(), setShellModeActive: vi.fn(), + setCleanUiDetailsVisible: vi.fn(), + toggleCleanUiDetailsVisible: vi.fn(), + revealCleanUiDetailsTemporarily: vi.fn(), onEscapePromptChange: vi.fn(), vimHandleInput: vi.fn(), + setShortcutsHelpVisible: vi.fn(), }) as Partial as UIActions; const createMockConfig = (overrides = {}): Config => @@ -232,6 +260,11 @@ const renderComposer = ( ); describe('Composer', () => { + beforeEach(() => { + composerTestControls.suggestionsVisible = false; + composerTestControls.isAlternateBuffer = false; + }); + afterEach(() => { vi.restoreAllMocks(); }); @@ -337,17 +370,18 @@ describe('Composer', () => { expect(output).toContain('LoadingIndicator: Thinking ...'); }); - it('keeps shortcuts hint visible while loading', () => { + it('hides shortcuts hint while loading', () => { const uiState = createMockUIState({ streamingState: StreamingState.Responding, elapsedTime: 1, + cleanUiDetailsVisible: false, }); const { lastFrame } = renderComposer(uiState); const output = lastFrame(); expect(output).toContain('LoadingIndicator'); - expect(output).toContain('ShortcutsHint'); + expect(output).not.toContain('ShortcutsHint'); }); it('renders LoadingIndicator without thought when accessibility disables loading phrases', () => { @@ -513,6 +547,21 @@ describe('Composer', () => { }); describe('Input and Indicators', () => { + it('hides non-essential UI details in clean mode', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + }); + + const { lastFrame } = renderComposer(uiState); + + const output = lastFrame(); + expect(output).toContain('ShortcutsHint'); + expect(output).toContain('InputPrompt'); + expect(output).not.toContain('Footer'); + expect(output).not.toContain('ApprovalModeIndicator'); + expect(output).not.toContain('ContextSummaryDisplay'); + }); + it('renders InputPrompt when input is active', () => { const uiState = createMockUIState({ isInputActive: true, @@ -581,6 +630,92 @@ describe('Composer', () => { expect(lastFrame()).not.toContain('raw markdown mode'); }); + + it.each([ + [ApprovalMode.YOLO, 'YOLO'], + [ApprovalMode.PLAN, 'plan'], + [ApprovalMode.AUTO_EDIT, 'auto edit'], + ])( + 'shows minimal mode badge "%s" when clean UI details are hidden', + (mode, label) => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + showApprovalModeIndicator: mode, + }); + + const { lastFrame } = renderComposer(uiState); + expect(lastFrame()).toContain(label); + }, + ); + + it('hides minimal mode badge while loading in clean mode', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + streamingState: StreamingState.Responding, + elapsedTime: 1, + showApprovalModeIndicator: ApprovalMode.PLAN, + }); + + const { lastFrame } = renderComposer(uiState); + const output = lastFrame(); + expect(output).toContain('LoadingIndicator'); + expect(output).not.toContain('plan'); + expect(output).not.toContain('ShortcutsHint'); + }); + + it('hides minimal mode badge while action-required state is active', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + showApprovalModeIndicator: ApprovalMode.PLAN, + customDialog: ( + + Prompt + + ), + }); + + const { lastFrame } = renderComposer(uiState); + const output = lastFrame(); + expect(output).not.toContain('plan'); + expect(output).not.toContain('ShortcutsHint'); + }); + + it('shows Esc rewind prompt in minimal mode without showing full UI', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + showEscapePrompt: true, + history: [{ id: 1, type: 'user', text: 'msg' }], + }); + + const { lastFrame } = renderComposer(uiState); + const output = lastFrame(); + expect(output).toContain('ToastDisplay'); + expect(output).not.toContain('ContextSummaryDisplay'); + }); + + it('shows context usage bleed-through when over 60%', () => { + const model = 'gemini-2.5-pro'; + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + currentModel: model, + sessionStats: { + sessionId: 'test-session', + sessionStartTime: new Date(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metrics: {} as any, + lastPromptTokenCount: Math.floor(tokenLimit(model) * 0.7), + promptCount: 0, + }, + }); + const settings = createMockSettings({ + ui: { + footer: { hideContextPercentage: false }, + }, + }); + + const { lastFrame } = renderComposer(uiState, settings); + expect(lastFrame()).toContain('%'); + }); }); describe('Error Details Display', () => { @@ -679,11 +814,127 @@ describe('Composer', () => { }); it('keeps shortcuts hint visible when no action is required', () => { - const uiState = createMockUIState(); + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).toContain('ShortcutsHint'); + }); + + it('shows shortcuts hint when full UI details are visible', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: true, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).toContain('ShortcutsHint'); + }); + + it('hides shortcuts hint while loading in minimal mode', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + streamingState: StreamingState.Responding, + elapsedTime: 1, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).not.toContain('ShortcutsHint'); + }); + + it('shows shortcuts help in minimal mode when toggled on', () => { + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + shortcutsHelpVisible: true, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).toContain('ShortcutsHelp'); + }); + + it('hides shortcuts hint when suggestions are visible above input in alternate buffer', () => { + composerTestControls.isAlternateBuffer = true; + composerTestControls.suggestionsVisible = true; + + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + showApprovalModeIndicator: ApprovalMode.PLAN, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).not.toContain('ShortcutsHint'); + expect(lastFrame()).not.toContain('plan'); + }); + + it('hides approval mode indicator when suggestions are visible above input in alternate buffer', () => { + composerTestControls.isAlternateBuffer = true; + composerTestControls.suggestionsVisible = true; + + const uiState = createMockUIState({ + cleanUiDetailsVisible: true, + showApprovalModeIndicator: ApprovalMode.YOLO, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).not.toContain('ApprovalModeIndicator'); + }); + + it('keeps shortcuts hint when suggestions are visible below input in regular buffer', () => { + composerTestControls.isAlternateBuffer = false; + composerTestControls.suggestionsVisible = true; + + const uiState = createMockUIState({ + cleanUiDetailsVisible: false, + }); const { lastFrame } = renderComposer(uiState); expect(lastFrame()).toContain('ShortcutsHint'); }); }); + + describe('Shortcuts Help', () => { + it('shows shortcuts help in passive state', () => { + const uiState = createMockUIState({ + shortcutsHelpVisible: true, + streamingState: StreamingState.Idle, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).toContain('ShortcutsHelp'); + }); + + it('hides shortcuts help while streaming', () => { + const uiState = createMockUIState({ + shortcutsHelpVisible: true, + streamingState: StreamingState.Responding, + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).not.toContain('ShortcutsHelp'); + }); + + it('hides shortcuts help when action is required', () => { + const uiState = createMockUIState({ + shortcutsHelpVisible: true, + customDialog: ( + + Dialog content + + ), + }); + + const { lastFrame } = renderComposer(uiState); + + expect(lastFrame()).not.toContain('ShortcutsHelp'); + }); + }); }); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index e87e86e801..8101e7303c 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useState } from 'react'; -import { Box, useIsScreenReaderEnabled } from 'ink'; +import { useState, useEffect, useMemo } from 'react'; +import { Box, Text, useIsScreenReaderEnabled } from 'ink'; +import { ApprovalMode, tokenLimit } from '@google/gemini-cli-core'; import { LoadingIndicator } from './LoadingIndicator.js'; import { StatusDisplay } from './StatusDisplay.js'; import { ToastDisplay, shouldShowToast } from './ToastDisplay.js'; @@ -19,6 +20,7 @@ import { InputPrompt } from './InputPrompt.js'; import { Footer } from './Footer.js'; import { ShowMoreLines } from './ShowMoreLines.js'; import { QueuedMessageDisplay } from './QueuedMessageDisplay.js'; +import { ContextUsageDisplay } from './ContextUsageDisplay.js'; import { HorizontalLine } from './shared/HorizontalLine.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; @@ -28,10 +30,15 @@ import { useVimMode } from '../contexts/VimModeContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; -import { StreamingState, ToolCallStatus } from '../types.js'; +import { + StreamingState, + type HistoryItemToolGroup, + ToolCallStatus, +} from '../types.js'; import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; import { TodoTray } from './messages/Todo.js'; import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js'; +import { theme } from '../semantic-colors.js'; export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { const config = useConfig(); @@ -48,14 +55,23 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { const isAlternateBuffer = useAlternateBuffer(); const { showApprovalModeIndicator } = uiState; + const showUiDetails = uiState.cleanUiDetailsVisible; const suggestionsPosition = isAlternateBuffer ? 'above' : 'below'; const hideContextSummary = suggestionsVisible && suggestionsPosition === 'above'; - const hasPendingToolConfirmation = (uiState.pendingHistoryItems ?? []).some( - (item) => - item.type === 'tool_group' && - item.tools.some((tool) => tool.status === ToolCallStatus.Confirming), + + const hasPendingToolConfirmation = useMemo( + () => + (uiState.pendingHistoryItems ?? []) + .filter( + (item): item is HistoryItemToolGroup => item.type === 'tool_group', + ) + .some((item) => + item.tools.some((tool) => tool.status === ToolCallStatus.Confirming), + ), + [uiState.pendingHistoryItems], ); + const hasPendingActionRequired = hasPendingToolConfirmation || Boolean(uiState.commandConfirmationRequest) || @@ -65,13 +81,81 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { Boolean(uiState.quota.proQuotaRequest) || Boolean(uiState.quota.validationRequest) || Boolean(uiState.customDialog); + const isPassiveShortcutsHelpState = + uiState.isInputActive && + uiState.streamingState === StreamingState.Idle && + !hasPendingActionRequired; + + const { setShortcutsHelpVisible } = uiActions; + + useEffect(() => { + if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) { + setShortcutsHelpVisible(false); + } + }, [ + uiState.shortcutsHelpVisible, + isPassiveShortcutsHelpState, + setShortcutsHelpVisible, + ]); + + const showShortcutsHelp = + uiState.shortcutsHelpVisible && + uiState.streamingState === StreamingState.Idle && + !hasPendingActionRequired; const hasToast = shouldShowToast(uiState); const showLoadingIndicator = (!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) && uiState.streamingState === StreamingState.Responding && !hasPendingActionRequired; - const showApprovalIndicator = !uiState.shellModeActive; + const hideUiDetailsForSuggestions = + suggestionsVisible && suggestionsPosition === 'above'; + const showApprovalIndicator = + !uiState.shellModeActive && !hideUiDetailsForSuggestions; const showRawMarkdownIndicator = !uiState.renderMarkdown; + const modeBleedThrough = + showApprovalModeIndicator === ApprovalMode.YOLO + ? { text: 'YOLO', color: theme.status.error } + : showApprovalModeIndicator === ApprovalMode.PLAN + ? { text: 'plan', color: theme.status.success } + : showApprovalModeIndicator === ApprovalMode.AUTO_EDIT + ? { text: 'auto edit', color: theme.status.warning } + : null; + const hideMinimalModeHintWhileBusy = + !showUiDetails && (showLoadingIndicator || hasPendingActionRequired); + const minimalModeBleedThrough = hideMinimalModeHintWhileBusy + ? null + : modeBleedThrough; + const hasMinimalStatusBleedThrough = shouldShowToast(uiState); + const contextTokenLimit = + typeof uiState.currentModel === 'string' && uiState.currentModel.length > 0 + ? tokenLimit(uiState.currentModel) + : 0; + const showMinimalContextBleedThrough = + !settings.merged.ui.footer.hideContextPercentage && + typeof uiState.currentModel === 'string' && + uiState.currentModel.length > 0 && + contextTokenLimit > 0 && + uiState.sessionStats.lastPromptTokenCount / contextTokenLimit > 0.6; + const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions; + const showShortcutsHint = + settings.merged.ui.showShortcutsHint && + !hideShortcutsHintForSuggestions && + !hideMinimalModeHintWhileBusy && + !hasPendingActionRequired && + (!showUiDetails || !showLoadingIndicator); + const showMinimalModeBleedThrough = + !hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough); + const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator; + const showMinimalBleedThroughRow = + !showUiDetails && + (showMinimalModeBleedThrough || + hasMinimalStatusBleedThrough || + showMinimalContextBleedThrough); + const showMinimalMetaRow = + !showUiDetails && + (showMinimalInlineLoading || + showMinimalBleedThroughRow || + showShortcutsHint); return ( { /> )} - + {showUiDetails && ( + + )} - + {showUiDetails && } { alignItems="center" flexGrow={1} > - {showLoadingIndicator && ( + {showUiDetails && showLoadingIndicator && ( { flexDirection="column" alignItems={isNarrow ? 'flex-start' : 'flex-end'} > - {settings.merged.ui.showShortcutsHint && - !hasPendingActionRequired && } + {showUiDetails && showShortcutsHint && } - {uiState.shortcutsHelpVisible && } - - + {showMinimalMetaRow && ( - {hasToast ? ( - - ) : ( - !showLoadingIndicator && ( + + {showMinimalInlineLoading && ( + + )} + {showMinimalModeBleedThrough && minimalModeBleedThrough && ( + + ● {minimalModeBleedThrough.text} + + )} + {hasMinimalStatusBleedThrough && ( - {showApprovalIndicator && ( - - )} - {uiState.shellModeActive && ( - - - - )} - {showRawMarkdownIndicator && ( - - - - )} + - ) + )} + + {(showMinimalContextBleedThrough || showShortcutsHint) && ( + + {showMinimalContextBleedThrough && ( + + )} + {showShortcutsHint && ( + + + + )} + )} - + )} + {showShortcutsHelp && } + {showUiDetails && } + {showUiDetails && ( - {!showLoadingIndicator && ( - - )} + + {hasToast ? ( + + ) : ( + !showLoadingIndicator && ( + + {showApprovalIndicator && ( + + )} + {uiState.shellModeActive && ( + + + + )} + {showRawMarkdownIndicator && ( + + + + )} + + ) + )} + + + + {!showLoadingIndicator && ( + + )} + - + )} - {uiState.showErrorDetails && ( + {showUiDetails && uiState.showErrorDetails && ( { /> )} - {!settings.merged.ui.hideFooter && !isScreenReaderEnabled &&