diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c71fbe2e22..c6c619219c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -352,21 +352,6 @@ npm run lint
- **Imports:** Pay special attention to import paths. The project uses ESLint to
enforce restrictions on relative imports between packages.
-### Project structure
-
-- `packages/`: Contains the individual sub-packages of the project.
- - `a2a-server`: A2A server implementation for the Gemini CLI. (Experimental)
- - `cli/`: The command-line interface.
- - `core/`: The core backend logic for the Gemini CLI.
- - `test-utils` Utilities for creating and cleaning temporary file systems for
- testing.
- - `vscode-ide-companion/`: The Gemini CLI Companion extension pairs with
- Gemini CLI.
-- `docs/`: Contains all project documentation.
-- `scripts/`: Utility scripts for building, testing, and development tasks.
-
-For more detailed architecture, see `docs/architecture.md`.
-
### Debugging
#### VS Code
diff --git a/GEMINI.md b/GEMINI.md
index f7017eab40..c08e486b22 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -22,9 +22,10 @@ powerful tool for developers.
rendering.
- `packages/core`: Backend logic, Gemini API orchestration, prompt
construction, and tool execution.
- - `packages/core/src/tools/`: Built-in tools for file system, shell, and web
- operations.
- `packages/a2a-server`: Experimental Agent-to-Agent server.
+ - `packages/sdk`: Programmatic SDK for embedding Gemini CLI capabilities.
+ - `packages/devtools`: Integrated developer tools (Network/Console inspector).
+ - `packages/test-utils`: Shared test utilities and test rig.
- `packages/vscode-ide-companion`: VS Code extension pairing with the CLI.
## Building and Running
@@ -58,10 +59,6 @@ powerful tool for developers.
## Development Conventions
-- **Legacy Snippets:** `packages/core/src/prompts/snippets.legacy.ts` is a
- snapshot of an older system prompt. Avoid changing the prompting verbiage to
- preserve its historical behavior; however, structural changes to ensure
- compilation or simplify the code are permitted.
- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires
signing the Google CLA.
- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue.
@@ -69,8 +66,6 @@ powerful tool for developers.
`gh` CLI.
- **Commit Messages:** Follow the
[Conventional Commits](https://www.conventionalcommits.org/) standard.
-- **Coding Style:** Adhere to existing patterns in `packages/cli` (React/Ink)
- and `packages/core` (Backend logic).
- **Imports:** Use specific imports and avoid restricted relative imports
between packages (enforced by ESLint).
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
diff --git a/README.md b/README.md
index 93485498ed..03a7be1296 100644
--- a/README.md
+++ b/README.md
@@ -314,7 +314,6 @@ gemini
- [**Headless Mode (Scripting)**](./docs/cli/headless.md) - Use Gemini CLI in
automated workflows.
-- [**Architecture Overview**](./docs/architecture.md) - How Gemini CLI works.
- [**IDE Integration**](./docs/ide-integration/index.md) - VS Code companion.
- [**Sandboxing & Security**](./docs/cli/sandbox.md) - Safe execution
environments.
diff --git a/docs/changelogs/index.md b/docs/changelogs/index.md
index 84b499c7a6..d79bd910d1 100644
--- a/docs/changelogs/index.md
+++ b/docs/changelogs/index.md
@@ -18,6 +18,17 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
+## Announcements: v0.34.0 - 2026-03-17
+
+- **Plan Mode Enabled by Default:** Plan Mode is now enabled by default to help
+ you break down complex tasks and execute them systematically
+ ([#21713](https://github.com/google-gemini/gemini-cli/pull/21713) by @jerop).
+- **Sandboxing Enhancements:** We've added native gVisor (runsc) and
+ experimental LXC container sandboxing support for safer execution environments
+ ([#21062](https://github.com/google-gemini/gemini-cli/pull/21062) by
+ @Zheyuan-Lin, [#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
+ by @h30s).
+
## Announcements: v0.33.0 - 2026-03-11
- **Agent Architecture Enhancements:** Introduced HTTP authentication for A2A
diff --git a/docs/changelogs/latest.md b/docs/changelogs/latest.md
index 5bac5b95e1..e49ef1c652 100644
--- a/docs/changelogs/latest.md
+++ b/docs/changelogs/latest.md
@@ -1,6 +1,6 @@
-# Latest stable release: v0.33.1
+# Latest stable release: v0.34.0
-Released: March 12, 2026
+Released: March 17, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,224 +11,474 @@ npm install -g @google/gemini-cli
## Highlights
-- **Agent Architecture Enhancements:** Introduced HTTP authentication support
- for A2A remote agents, authenticated A2A agent card discovery, and directly
- indicated auth-required states.
-- **Plan Mode Updates:** Expanded Plan Mode capabilities with built-in research
- subagents, annotation support for feedback during iteration, and a new `copy`
- subcommand.
-- **CLI UX Improvements:** Redesigned the header to be compact with an ASCII
- icon, inverted the context window display to show usage, and allowed sub-agent
- confirmation requests in the UI while preventing background flicker.
-- **ACP & MCP Integrations:** Implemented slash command handling in ACP for
- `/memory`, `/init`, `/extensions`, and `/restore`, added an MCPOAuthProvider,
- and introduced a `set models` interface for ACP.
-- **Admin & Core Stability:** Enabled a 30-day default retention for chat
- history, added tool name validation in TOML policy files, and improved tool
- parameter extraction.
+- **Plan Mode Enabled by Default**: The comprehensive planning capability is now
+ enabled by default, allowing for better structured task management and
+ execution.
+- **Enhanced Sandboxing Capabilities**: Added support for native gVisor (runsc)
+ sandboxing as well as experimental LXC container sandboxing to provide more
+ robust and isolated execution environments.
+- **Improved Loop Detection & Recovery**: Implemented iterative loop detection
+ and model feedback mechanisms to prevent the CLI from getting stuck in
+ repetitive actions.
+- **Customizable UI Elements**: You can now configure a custom footer using the
+ new `/footer` command, and enjoy standardized semantic focus colors for better
+ history visibility.
+- **Extensive Subagent Updates**: Refinements across the tracker visualization
+ tools, background process logging, and broader fallback support for models in
+ tool execution scenarios.
## What's Changed
-- fix(patch): cherry-pick 8432bce to release/v0.33.0-pr-22069 to patch version
- v0.33.0 and create version 0.33.1 by @gemini-cli-robot in
- [#22206](https://github.com/google-gemini/gemini-cli/pull/22206)
-- Docs: Update model docs to remove Preview Features. by @jkcinouye in
- [#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
-- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
- [#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
-- docs: add Windows PowerShell equivalents for environments and scripting by
- @scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
-- fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
- [#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
-- chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
- in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
-- fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
- [#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
-- chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
+- feat(cli): add chat resume footer on session quit by @lordshashank in
+ [#20667](https://github.com/google-gemini/gemini-cli/pull/20667)
+- Support bold and other styles in svg snapshots by @jacob314 in
+ [#20937](https://github.com/google-gemini/gemini-cli/pull/20937)
+- fix(core): increase A2A agent timeout to 30 minutes by @adamfweidman in
+ [#21028](https://github.com/google-gemini/gemini-cli/pull/21028)
+- Cleanup old branches. by @jacob314 in
+ [#19354](https://github.com/google-gemini/gemini-cli/pull/19354)
+- chore(release): bump version to 0.34.0-nightly.20260303.34f0c1538 by
@gemini-cli-robot in
- [#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
-- Changelog for v0.31.0 by @gemini-cli-robot in
- [#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
-- fix: use full paths for ACP diff payloads by @JagjeevanAK in
- [#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
-- Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
- [#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
-- fix: acp/zed race condition between MCP initialisation and prompt by
- @kartikangiras in
- [#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
-- fix(cli): reset themeManager between tests to ensure isolation by
- @NTaylorMullen in
- [#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
-- refactor(core): Extract tool parameter names as constants by @SandyTao520 in
- [#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
-- fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
- mismatches by @sehoon38 in
- [#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
-- feat(skills): add github-issue-creator skill by @sehoon38 in
- [#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
-- fix(cli): allow sub-agent confirmation requests in UI while preventing
- background flicker by @abhipatel12 in
- [#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
-- Merge User and Agent Card Descriptions #20849 by @adamfweidman in
- [#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
-- fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
- [#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
-- fix(plan): deflake plan mode integration tests by @Adib234 in
- [#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
-- Add /unassign support by @scidomino in
- [#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
-- feat(core): implement HTTP authentication support for A2A remote agents by
- @SandyTao520 in
- [#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
-- feat(core): centralize read_file limits and update gemini-3 description by
+ [#21034](https://github.com/google-gemini/gemini-cli/pull/21034)
+- feat(ui): standardize semantic focus colors and enhance history visibility by
+ @keithguerin in
+ [#20745](https://github.com/google-gemini/gemini-cli/pull/20745)
+- fix: merge duplicate imports in packages/core (3/4) by @Nixxx19 in
+ [#20928](https://github.com/google-gemini/gemini-cli/pull/20928)
+- Add extra safety checks for proto pollution by @jacob314 in
+ [#20396](https://github.com/google-gemini/gemini-cli/pull/20396)
+- feat(core): Add tracker CRUD tools & visualization by @anj-s in
+ [#19489](https://github.com/google-gemini/gemini-cli/pull/19489)
+- Revert "fix(ui): persist expansion in AskUser dialog when navigating options"
+ by @jacob314 in
+ [#21042](https://github.com/google-gemini/gemini-cli/pull/21042)
+- Changelog for v0.33.0-preview.0 by @gemini-cli-robot in
+ [#21030](https://github.com/google-gemini/gemini-cli/pull/21030)
+- fix: model persistence for all scenarios by @sripasg in
+ [#21051](https://github.com/google-gemini/gemini-cli/pull/21051)
+- chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 by
+ @gemini-cli-robot in
+ [#21054](https://github.com/google-gemini/gemini-cli/pull/21054)
+- Consistently guard restarts against concurrent auto updates by @scidomino in
+ [#21016](https://github.com/google-gemini/gemini-cli/pull/21016)
+- Defensive coding to reduce the risk of Maximum update depth errors by
+ @jacob314 in [#20940](https://github.com/google-gemini/gemini-cli/pull/20940)
+- fix(cli): Polish shell autocomplete rendering to be a little more shell native
+ feeling. by @jacob314 in
+ [#20931](https://github.com/google-gemini/gemini-cli/pull/20931)
+- Docs: Update plan mode docs by @jkcinouye in
+ [#19682](https://github.com/google-gemini/gemini-cli/pull/19682)
+- fix(mcp): Notifications/tools/list_changed support not working by @jacob314 in
+ [#21050](https://github.com/google-gemini/gemini-cli/pull/21050)
+- fix(cli): register extension lifecycle events in DebugProfiler by
+ @fayerman-source in
+ [#20101](https://github.com/google-gemini/gemini-cli/pull/20101)
+- chore(dev): update vscode settings for typescriptreact by @rohit-4321 in
+ [#19907](https://github.com/google-gemini/gemini-cli/pull/19907)
+- fix(cli): enable multi-arch docker builds for sandbox by @ru-aish in
+ [#19821](https://github.com/google-gemini/gemini-cli/pull/19821)
+- Changelog for v0.32.0 by @gemini-cli-robot in
+ [#21033](https://github.com/google-gemini/gemini-cli/pull/21033)
+- Changelog for v0.33.0-preview.1 by @gemini-cli-robot in
+ [#21058](https://github.com/google-gemini/gemini-cli/pull/21058)
+- feat(core): improve @scripts/copy_files.js autocomplete to prioritize
+ filenames by @sehoon38 in
+ [#21064](https://github.com/google-gemini/gemini-cli/pull/21064)
+- feat(sandbox): add experimental LXC container sandbox support by @h30s in
+ [#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
+- feat(evals): add overall pass rate row to eval nightly summary table by
+ @gundermanc in
+ [#20905](https://github.com/google-gemini/gemini-cli/pull/20905)
+- feat(telemetry): include language in telemetry and fix accepted lines
+ computation by @gundermanc in
+ [#21126](https://github.com/google-gemini/gemini-cli/pull/21126)
+- Changelog for v0.32.1 by @gemini-cli-robot in
+ [#21055](https://github.com/google-gemini/gemini-cli/pull/21055)
+- feat(core): add robustness tests, logging, and metrics for CodeAssistServer
+ SSE parsing by @yunaseoul in
+ [#21013](https://github.com/google-gemini/gemini-cli/pull/21013)
+- feat: add issue assignee workflow by @kartikangiras in
+ [#21003](https://github.com/google-gemini/gemini-cli/pull/21003)
+- fix: improve error message when OAuth succeeds but project ID is required by
+ @Nixxx19 in [#21070](https://github.com/google-gemini/gemini-cli/pull/21070)
+- feat(loop-reduction): implement iterative loop detection and model feedback by
@aishaneeshah in
- [#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
-- Do not block CI on evals by @gundermanc in
- [#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
-- document node limitation for shift+tab by @scidomino in
- [#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
-- Add install as an option when extension is selected. by @DavidAPierce in
- [#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
-- Update CODEOWNERS for README.md reviewers by @g-samroberts in
- [#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
-- feat(core): truncate large MCP tool output by @SandyTao520 in
- [#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
-- Subagent activity UX. by @gundermanc in
- [#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
-- style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
- [#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
-- feat: redesign header to be compact with ASCII icon by @keithguerin in
- [#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
-- fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
- [#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
-- feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
- [#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
-- refactor(cli): fully remove React anti patterns, improve type safety and fix
- UX oversights in SettingsDialog.tsx by @psinha40898 in
- [#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
-- Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
- @Nayana-Parameswarappa in
- [#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
-- feat(core): add tool name validation in TOML policy files by @allenhutchison
- in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
-- docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
- [#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
-- refactor(core): replace manual syncPlanModeTools with declarative policy rules
- by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
-- fix(core): increase default headers timeout to 5 minutes by @gundermanc in
- [#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
-- feat(admin): enable 30 day default retention for chat history & remove warning
+ [#20763](https://github.com/google-gemini/gemini-cli/pull/20763)
+- chore(github): require prompt approvers for agent prompt files by @gundermanc
+ in [#20896](https://github.com/google-gemini/gemini-cli/pull/20896)
+- Docs: Create tools reference by @jkcinouye in
+ [#19470](https://github.com/google-gemini/gemini-cli/pull/19470)
+- fix(core, a2a-server): prevent hang during OAuth in non-interactive sessions
+ by @spencer426 in
+ [#21045](https://github.com/google-gemini/gemini-cli/pull/21045)
+- chore(cli): enable deprecated settings removal by default by @yashodipmore in
+ [#20682](https://github.com/google-gemini/gemini-cli/pull/20682)
+- feat(core): Disable fast ack helper for hints. by @joshualitt in
+ [#21011](https://github.com/google-gemini/gemini-cli/pull/21011)
+- fix(ui): suppress redundant failure note when tool error note is shown by
+ @NTaylorMullen in
+ [#21078](https://github.com/google-gemini/gemini-cli/pull/21078)
+- docs: document planning workflows with Conductor example by @jerop in
+ [#21166](https://github.com/google-gemini/gemini-cli/pull/21166)
+- feat(release): ship esbuild bundle in npm package by @genneth in
+ [#19171](https://github.com/google-gemini/gemini-cli/pull/19171)
+- fix(extensions): preserve symlinks in extension source path while enforcing
+ folder trust by @galz10 in
+ [#20867](https://github.com/google-gemini/gemini-cli/pull/20867)
+- fix(cli): defer tool exclusions to policy engine in non-interactive mode by
+ @EricRahm in [#20639](https://github.com/google-gemini/gemini-cli/pull/20639)
+- fix(ui): removed double padding on rendered content by @devr0306 in
+ [#21029](https://github.com/google-gemini/gemini-cli/pull/21029)
+- fix(core): truncate excessively long lines in grep search output by
+ @gundermanc in
+ [#21147](https://github.com/google-gemini/gemini-cli/pull/21147)
+- feat: add custom footer configuration via `/footer` by @jackwotherspoon in
+ [#19001](https://github.com/google-gemini/gemini-cli/pull/19001)
+- perf(core): fix OOM crash in long-running sessions by @WizardsForgeGames in
+ [#19608](https://github.com/google-gemini/gemini-cli/pull/19608)
+- refactor(cli): categorize built-in themes into dark/ and light/ directories by
+ @JayadityaGit in
+ [#18634](https://github.com/google-gemini/gemini-cli/pull/18634)
+- fix(core): explicitly allow codebase_investigator and cli_help in read-only
+ mode by @Adib234 in
+ [#21157](https://github.com/google-gemini/gemini-cli/pull/21157)
+- test: add browser agent integration tests by @kunal-10-cloud in
+ [#21151](https://github.com/google-gemini/gemini-cli/pull/21151)
+- fix(cli): fix enabling kitty codes on Windows Terminal by @scidomino in
+ [#21136](https://github.com/google-gemini/gemini-cli/pull/21136)
+- refactor(core): extract shared OAuth flow primitives from MCPOAuthProvider by
+ @SandyTao520 in
+ [#20895](https://github.com/google-gemini/gemini-cli/pull/20895)
+- fix(ui): add partial output to cancelled shell UI by @devr0306 in
+ [#21178](https://github.com/google-gemini/gemini-cli/pull/21178)
+- fix(cli): replace hardcoded keybinding strings with dynamic formatters by
+ @scidomino in [#21159](https://github.com/google-gemini/gemini-cli/pull/21159)
+- DOCS: Update quota and pricing page by @g-samroberts in
+ [#21194](https://github.com/google-gemini/gemini-cli/pull/21194)
+- feat(telemetry): implement Clearcut logging for startup statistics by
+ @yunaseoul in [#21172](https://github.com/google-gemini/gemini-cli/pull/21172)
+- feat(triage): add area/documentation to issue triage by @g-samroberts in
+ [#21222](https://github.com/google-gemini/gemini-cli/pull/21222)
+- Fix so shell calls are formatted by @jacob314 in
+ [#21237](https://github.com/google-gemini/gemini-cli/pull/21237)
+- feat(cli): add native gVisor (runsc) sandboxing support by @Zheyuan-Lin in
+ [#21062](https://github.com/google-gemini/gemini-cli/pull/21062)
+- docs: use absolute paths for internal links in plan-mode.md by @jerop in
+ [#21299](https://github.com/google-gemini/gemini-cli/pull/21299)
+- fix(core): prevent unhandled AbortError crash during stream loop detection by
+ @7hokerz in [#21123](https://github.com/google-gemini/gemini-cli/pull/21123)
+- fix:reorder env var redaction checks to scan values first by @kartikangiras in
+ [#21059](https://github.com/google-gemini/gemini-cli/pull/21059)
+- fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences
by @skeshive in
- [#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
-- feat(plan): support annotating plans with feedback for iteration by @Adib234
- in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
-- Add some dos and don'ts to behavioral evals README. by @gundermanc in
- [#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
-- fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
- [#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
-- fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
- models by @SandyTao520 in
- [#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
-- ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
- [#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
-- Build binary by @aswinashok44 in
- [#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
-- Code review fixes as a pr by @jacob314 in
- [#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
-- fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
- [#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
-- feat(cli): invert context window display to show usage by @keithguerin in
- [#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
-- fix(plan): clean up session directories and plans on deletion by @jerop in
- [#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
-- fix(core): enforce optionality for API response fields in code_assist by
- @sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
-- feat(extensions): add support for plan directory in extension manifest by
- @mahimashanware in
- [#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
-- feat(plan): enable built-in research subagents in plan mode by @Adib234 in
- [#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
-- feat(agents): directly indicate auth required state by @adamfweidman in
- [#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
-- fix(cli): wait for background auto-update before relaunching by @scidomino in
- [#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
-- fix: pre-load @scripts/copy_files.js references from external editor prompts
- by @kartikangiras in
- [#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
-- feat(evals): add behavioral evals for ask_user tool by @Adib234 in
- [#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
-- refactor common settings logic for skills,agents by @ishaanxgupta in
- [#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
-- Update docs-writer skill with new resource by @g-samroberts in
- [#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
-- fix(cli): pin clipboardy to ~5.2.x by @scidomino in
- [#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
-- feat: Implement slash command handling in ACP for
- `/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
- [#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
-- Docs/add hooks reference by @AadithyaAle in
- [#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
-- feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
- [#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
-- fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
- in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
-- Format the quota/limit style guide. by @g-samroberts in
- [#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
-- fix(core): send shell output to model on cancel by @devr0306 in
- [#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
-- remove hardcoded tiername when missing tier by @sehoon38 in
- [#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
-- feat(acp): add set models interface by @skeshive in
- [#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
-- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
- version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
+ [#21171](https://github.com/google-gemini/gemini-cli/pull/21171)
+- feat(core): fallback to 2.5 models with no access for toolcalls by @sehoon38
+ in [#21283](https://github.com/google-gemini/gemini-cli/pull/21283)
+- test(core): improve testing for API request/response parsing by @sehoon38 in
+ [#21227](https://github.com/google-gemini/gemini-cli/pull/21227)
+- docs(links): update docs-writer skill and fix broken link by @g-samroberts in
+ [#21314](https://github.com/google-gemini/gemini-cli/pull/21314)
+- Fix code colorizer ansi escape bug. by @jacob314 in
+ [#21321](https://github.com/google-gemini/gemini-cli/pull/21321)
+- remove wildcard behavior on keybindings by @scidomino in
+ [#21315](https://github.com/google-gemini/gemini-cli/pull/21315)
+- feat(acp): Add support for AI Gateway auth by @skeshive in
+ [#21305](https://github.com/google-gemini/gemini-cli/pull/21305)
+- fix(theme): improve theme color contrast for macOS Terminal.app by @clocky in
+ [#21175](https://github.com/google-gemini/gemini-cli/pull/21175)
+- feat (core): Implement tracker related SI changes by @anj-s in
+ [#19964](https://github.com/google-gemini/gemini-cli/pull/19964)
+- Changelog for v0.33.0-preview.2 by @gemini-cli-robot in
+ [#21333](https://github.com/google-gemini/gemini-cli/pull/21333)
+- Changelog for v0.33.0-preview.3 by @gemini-cli-robot in
+ [#21347](https://github.com/google-gemini/gemini-cli/pull/21347)
+- docs: format release times as HH:MM UTC by @pavan-sh in
+ [#20726](https://github.com/google-gemini/gemini-cli/pull/20726)
+- fix(cli): implement --all flag for extensions uninstall by @sehoon38 in
+ [#21319](https://github.com/google-gemini/gemini-cli/pull/21319)
+- docs: fix incorrect relative links to command reference by @kanywst in
+ [#20964](https://github.com/google-gemini/gemini-cli/pull/20964)
+- documentiong ensures ripgrep by @Jatin24062005 in
+ [#21298](https://github.com/google-gemini/gemini-cli/pull/21298)
+- fix(core): handle AbortError thrown during processTurn by @MumuTW in
+ [#21296](https://github.com/google-gemini/gemini-cli/pull/21296)
+- docs(cli): clarify ! command output visibility in shell commands tutorial by
+ @MohammedADev in
+ [#21041](https://github.com/google-gemini/gemini-cli/pull/21041)
+- fix: logic for task tracker strategy and remove tracker tools by @anj-s in
+ [#21355](https://github.com/google-gemini/gemini-cli/pull/21355)
+- fix(partUtils): display media type and size for inline data parts by @Aboudjem
+ in [#21358](https://github.com/google-gemini/gemini-cli/pull/21358)
+- Fix(accessibility): add screen reader support to RewindViewer by @Famous077 in
+ [#20750](https://github.com/google-gemini/gemini-cli/pull/20750)
+- fix(hooks): propagate stopHookActive in AfterAgent retry path (#20426) by
+ @Aarchi-07 in [#20439](https://github.com/google-gemini/gemini-cli/pull/20439)
+- fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive
+ filesystems (#19904) by @Nixxx19 in
+ [#19915](https://github.com/google-gemini/gemini-cli/pull/19915)
+- feat(core): add concurrency safety guidance for subagent delegation (#17753)
+ by @abhipatel12 in
+ [#21278](https://github.com/google-gemini/gemini-cli/pull/21278)
+- feat(ui): dynamically generate all keybinding hints by @scidomino in
+ [#21346](https://github.com/google-gemini/gemini-cli/pull/21346)
+- feat(core): implement unified KeychainService and migrate token storage by
+ @ehedlund in [#21344](https://github.com/google-gemini/gemini-cli/pull/21344)
+- fix(cli): gracefully handle --resume when no sessions exist by @SandyTao520 in
+ [#21429](https://github.com/google-gemini/gemini-cli/pull/21429)
+- fix(plan): keep approved plan during chat compression by @ruomengz in
+ [#21284](https://github.com/google-gemini/gemini-cli/pull/21284)
+- feat(core): implement generic CacheService and optimize setupUser by @sehoon38
+ in [#21374](https://github.com/google-gemini/gemini-cli/pull/21374)
+- Update quota and pricing documentation with subscription tiers by @srithreepo
+ in [#21351](https://github.com/google-gemini/gemini-cli/pull/21351)
+- fix(core): append correct OTLP paths for HTTP exporters by
+ @sebastien-prudhomme in
+ [#16836](https://github.com/google-gemini/gemini-cli/pull/16836)
+- Changelog for v0.33.0-preview.4 by @gemini-cli-robot in
+ [#21354](https://github.com/google-gemini/gemini-cli/pull/21354)
+- feat(cli): implement dot-prefixing for slash command conflicts by @ehedlund in
+ [#20979](https://github.com/google-gemini/gemini-cli/pull/20979)
+- refactor(core): standardize MCP tool naming to mcp\_ FQN format by
+ @abhipatel12 in
+ [#21425](https://github.com/google-gemini/gemini-cli/pull/21425)
+- feat(cli): hide gemma settings from display and mark as experimental by
+ @abhipatel12 in
+ [#21471](https://github.com/google-gemini/gemini-cli/pull/21471)
+- feat(skills): refine string-reviewer guidelines and description by @clocky in
+ [#20368](https://github.com/google-gemini/gemini-cli/pull/20368)
+- fix(core): whitelist TERM and COLORTERM in environment sanitization by
+ @deadsmash07 in
+ [#20514](https://github.com/google-gemini/gemini-cli/pull/20514)
+- fix(billing): fix overage strategy lifecycle and settings integration by
+ @gsquared94 in
+ [#21236](https://github.com/google-gemini/gemini-cli/pull/21236)
+- fix: expand paste placeholders in TextInput on submit by @Jefftree in
+ [#19946](https://github.com/google-gemini/gemini-cli/pull/19946)
+- fix(core): add in-memory cache to ChatRecordingService to prevent OOM by
+ @SandyTao520 in
+ [#21502](https://github.com/google-gemini/gemini-cli/pull/21502)
+- feat(cli): overhaul thinking UI by @keithguerin in
+ [#18725](https://github.com/google-gemini/gemini-cli/pull/18725)
+- fix(ui): unify Ctrl+O expansion hint experience across buffer modes by
+ @jwhelangoog in
+ [#21474](https://github.com/google-gemini/gemini-cli/pull/21474)
+- fix(cli): correct shell height reporting by @jacob314 in
+ [#21492](https://github.com/google-gemini/gemini-cli/pull/21492)
+- Make test suite pass when the GEMINI_SYSTEM_MD env variable or
+ GEMINI_WRITE_SYSTEM_MD variable happens to be set locally/ by @jacob314 in
+ [#21480](https://github.com/google-gemini/gemini-cli/pull/21480)
+- Disallow underspecified types by @gundermanc in
+ [#21485](https://github.com/google-gemini/gemini-cli/pull/21485)
+- refactor(cli): standardize on 'reload' verb for all components by @keithguerin
+ in [#20654](https://github.com/google-gemini/gemini-cli/pull/20654)
+- feat(cli): Invert quota language to 'percent used' by @keithguerin in
+ [#20100](https://github.com/google-gemini/gemini-cli/pull/20100)
+- Docs: Add documentation for notifications (experimental)(macOS) by @jkcinouye
+ in [#21163](https://github.com/google-gemini/gemini-cli/pull/21163)
+- Code review comments as a pr by @jacob314 in
+ [#21209](https://github.com/google-gemini/gemini-cli/pull/21209)
+- feat(cli): unify /chat and /resume command UX by @LyalinDotCom in
+ [#20256](https://github.com/google-gemini/gemini-cli/pull/20256)
+- docs: fix typo 'allowslisted' -> 'allowlisted' in mcp-server.md by
+ @Gyanranjan-Priyam in
+ [#21665](https://github.com/google-gemini/gemini-cli/pull/21665)
+- fix(core): display actual graph output in tracker_visualize tool by @anj-s in
+ [#21455](https://github.com/google-gemini/gemini-cli/pull/21455)
+- fix(core): sanitize SSE-corrupted JSON and domain strings in error
+ classification by @gsquared94 in
+ [#21702](https://github.com/google-gemini/gemini-cli/pull/21702)
+- Docs: Make documentation links relative by @diodesign in
+ [#21490](https://github.com/google-gemini/gemini-cli/pull/21490)
+- feat(cli): expose /tools desc as explicit subcommand for discoverability by
+ @aworki in [#21241](https://github.com/google-gemini/gemini-cli/pull/21241)
+- feat(cli): add /compact alias for /compress command by @jackwotherspoon in
+ [#21711](https://github.com/google-gemini/gemini-cli/pull/21711)
+- feat(plan): enable Plan Mode by default by @jerop in
+ [#21713](https://github.com/google-gemini/gemini-cli/pull/21713)
+- feat(core): Introduce `AgentLoopContext`. by @joshualitt in
+ [#21198](https://github.com/google-gemini/gemini-cli/pull/21198)
+- fix(core): resolve symlinks for non-existent paths during validation by
+ @Adib234 in [#21487](https://github.com/google-gemini/gemini-cli/pull/21487)
+- docs: document tool exclusion from memory via deny policy by @Abhijit-2592 in
+ [#21428](https://github.com/google-gemini/gemini-cli/pull/21428)
+- perf(core): cache loadApiKey to reduce redundant keychain access by @sehoon38
+ in [#21520](https://github.com/google-gemini/gemini-cli/pull/21520)
+- feat(cli): implement /upgrade command by @sehoon38 in
+ [#21511](https://github.com/google-gemini/gemini-cli/pull/21511)
+- Feat/browser agent progress emission by @kunal-10-cloud in
+ [#21218](https://github.com/google-gemini/gemini-cli/pull/21218)
+- fix(settings): display objects as JSON instead of [object Object] by
+ @Zheyuan-Lin in
+ [#21458](https://github.com/google-gemini/gemini-cli/pull/21458)
+- Unmarshall update by @DavidAPierce in
+ [#21721](https://github.com/google-gemini/gemini-cli/pull/21721)
+- Update mcp's list function to check for disablement. by @DavidAPierce in
+ [#21148](https://github.com/google-gemini/gemini-cli/pull/21148)
+- robustness(core): static checks to validate history is immutable by @jacob314
+ in [#21228](https://github.com/google-gemini/gemini-cli/pull/21228)
+- refactor(cli): better react patterns for BaseSettingsDialog by @psinha40898 in
+ [#21206](https://github.com/google-gemini/gemini-cli/pull/21206)
+- feat(security): implement robust IP validation and safeFetch foundation by
+ @alisa-alisa in
+ [#21401](https://github.com/google-gemini/gemini-cli/pull/21401)
+- feat(core): improve subagent result display by @joshualitt in
+ [#20378](https://github.com/google-gemini/gemini-cli/pull/20378)
+- docs: fix broken markdown syntax and anchor links in /tools by @campox747 in
+ [#20902](https://github.com/google-gemini/gemini-cli/pull/20902)
+- feat(policy): support subagent-specific policies in TOML by @akh64bit in
+ [#21431](https://github.com/google-gemini/gemini-cli/pull/21431)
+- Add script to speed up reviewing PRs adding a worktree. by @jacob314 in
+ [#21748](https://github.com/google-gemini/gemini-cli/pull/21748)
+- fix(core): prevent infinite recursion in symlink resolution by @Adib234 in
+ [#21750](https://github.com/google-gemini/gemini-cli/pull/21750)
+- fix(docs): fix headless mode docs by @ame2en in
+ [#21287](https://github.com/google-gemini/gemini-cli/pull/21287)
+- feat/redesign header compact by @jacob314 in
+ [#20922](https://github.com/google-gemini/gemini-cli/pull/20922)
+- refactor: migrate to useKeyMatchers hook by @scidomino in
+ [#21753](https://github.com/google-gemini/gemini-cli/pull/21753)
+- perf(cli): cache loadSettings to reduce redundant disk I/O at startup by
+ @sehoon38 in [#21521](https://github.com/google-gemini/gemini-cli/pull/21521)
+- fix(core): resolve Windows line ending and path separation bugs across CLI by
+ @muhammadusman586 in
+ [#21068](https://github.com/google-gemini/gemini-cli/pull/21068)
+- docs: fix heading formatting in commands.md and phrasing in tools-api.md by
+ @campox747 in [#20679](https://github.com/google-gemini/gemini-cli/pull/20679)
+- refactor(ui): unify keybinding infrastructure and support string
+ initialization by @scidomino in
+ [#21776](https://github.com/google-gemini/gemini-cli/pull/21776)
+- Add support for updating extension sources and names by @chrstnb in
+ [#21715](https://github.com/google-gemini/gemini-cli/pull/21715)
+- fix(core): handle GUI editor non-zero exit codes gracefully by @reyyanxahmed
+ in [#20376](https://github.com/google-gemini/gemini-cli/pull/20376)
+- fix(core): destroy PTY on kill() and exception to prevent fd leak by @nbardy
+ in [#21693](https://github.com/google-gemini/gemini-cli/pull/21693)
+- fix(docs): update theme screenshots and add missing themes by @ashmod in
+ [#20689](https://github.com/google-gemini/gemini-cli/pull/20689)
+- refactor(cli): rename 'return' key to 'enter' internally by @scidomino in
+ [#21796](https://github.com/google-gemini/gemini-cli/pull/21796)
+- build(release): restrict npm bundling to non-stable tags by @sehoon38 in
+ [#21821](https://github.com/google-gemini/gemini-cli/pull/21821)
+- fix(core): override toolRegistry property for sub-agent schedulers by
+ @gsquared94 in
+ [#21766](https://github.com/google-gemini/gemini-cli/pull/21766)
+- fix(cli): make footer items equally spaced by @jacob314 in
+ [#21843](https://github.com/google-gemini/gemini-cli/pull/21843)
+- docs: clarify global policy rules application in plan mode by @jerop in
+ [#21864](https://github.com/google-gemini/gemini-cli/pull/21864)
+- fix(core): ensure correct flash model steering in plan mode implementation
+ phase by @jerop in
+ [#21871](https://github.com/google-gemini/gemini-cli/pull/21871)
+- fix(core): update @a2a-js/sdk to 0.3.11 by @adamfweidman in
+ [#21875](https://github.com/google-gemini/gemini-cli/pull/21875)
+- refactor(core): improve API response error logging when retry by @yunaseoul in
+ [#21784](https://github.com/google-gemini/gemini-cli/pull/21784)
+- fix(ui): handle headless execution in credits and upgrade dialogs by
+ @gsquared94 in
+ [#21850](https://github.com/google-gemini/gemini-cli/pull/21850)
+- fix(core): treat retryable errors with >5 min delay as terminal quota errors
+ by @gsquared94 in
+ [#21881](https://github.com/google-gemini/gemini-cli/pull/21881)
+- feat(telemetry): add specific PR, issue, and custom tracking IDs for GitHub
+ Actions by @cocosheng-g in
+ [#21129](https://github.com/google-gemini/gemini-cli/pull/21129)
+- feat(core): add OAuth2 Authorization Code auth provider for A2A agents by
+ @SandyTao520 in
+ [#21496](https://github.com/google-gemini/gemini-cli/pull/21496)
+- feat(cli): give visibility to /tools list command in the TUI and follow the
+ subcommand pattern of other commands by @JayadityaGit in
+ [#21213](https://github.com/google-gemini/gemini-cli/pull/21213)
+- Handle dirty worktrees better and warn about running scripts/review.sh on
+ untrusted code. by @jacob314 in
+ [#21791](https://github.com/google-gemini/gemini-cli/pull/21791)
+- feat(policy): support auto-add to policy by default and scoped persistence by
+ @spencer426 in
+ [#20361](https://github.com/google-gemini/gemini-cli/pull/20361)
+- fix(core): handle AbortError when ESC cancels tool execution by @PrasannaPal21
+ in [#20863](https://github.com/google-gemini/gemini-cli/pull/20863)
+- fix(release): Improve Patch Release Workflow Comments: Clearer Approval
+ Guidance by @jerop in
+ [#21894](https://github.com/google-gemini/gemini-cli/pull/21894)
+- docs: clarify telemetry setup and comprehensive data map by @jerop in
+ [#21879](https://github.com/google-gemini/gemini-cli/pull/21879)
+- feat(core): add per-model token usage to stream-json output by @yongruilin in
+ [#21839](https://github.com/google-gemini/gemini-cli/pull/21839)
+- docs: remove experimental badge from plan mode in sidebar by @jerop in
+ [#21906](https://github.com/google-gemini/gemini-cli/pull/21906)
+- fix(cli): prevent race condition in loop detection retry by @skyvanguard in
+ [#17916](https://github.com/google-gemini/gemini-cli/pull/17916)
+- Add behavioral evals for tracker by @anj-s in
+ [#20069](https://github.com/google-gemini/gemini-cli/pull/20069)
+- fix(auth): update terminology to 'sign in' and 'sign out' by @clocky in
+ [#20892](https://github.com/google-gemini/gemini-cli/pull/20892)
+- docs(mcp): standardize mcp tool fqn documentation by @abhipatel12 in
+ [#21664](https://github.com/google-gemini/gemini-cli/pull/21664)
+- fix(ui): prevent empty tool-group border stubs after filtering by @Aaxhirrr in
+ [#21852](https://github.com/google-gemini/gemini-cli/pull/21852)
+- make command names consistent by @scidomino in
+ [#21907](https://github.com/google-gemini/gemini-cli/pull/21907)
+- refactor: remove agent_card_requires_auth config flag by @adamfweidman in
+ [#21914](https://github.com/google-gemini/gemini-cli/pull/21914)
+- feat(a2a): implement standardized normalization and streaming reassembly by
+ @alisa-alisa in
+ [#21402](https://github.com/google-gemini/gemini-cli/pull/21402)
+- feat(cli): enable skill activation via slash commands by @NTaylorMullen in
+ [#21758](https://github.com/google-gemini/gemini-cli/pull/21758)
+- docs(cli): mention per-model token usage in stream-json result event by
+ @yongruilin in
+ [#21908](https://github.com/google-gemini/gemini-cli/pull/21908)
+- fix(plan): prevent plan truncation in approval dialog by supporting
+ unconstrained heights by @Adib234 in
+ [#21037](https://github.com/google-gemini/gemini-cli/pull/21037)
+- feat(a2a): switch from callback-based to event-driven tool scheduler by
+ @cocosheng-g in
+ [#21467](https://github.com/google-gemini/gemini-cli/pull/21467)
+- feat(voice): implement speech-friendly response formatter by @ayush31010 in
+ [#20989](https://github.com/google-gemini/gemini-cli/pull/20989)
+- feat: add pulsating blue border automation overlay to browser agent by
+ @kunal-10-cloud in
+ [#21173](https://github.com/google-gemini/gemini-cli/pull/21173)
+- Add extensionRegistryURI setting to change where the registry is read from by
+ @kevinjwang1 in
+ [#20463](https://github.com/google-gemini/gemini-cli/pull/20463)
+- fix: patch gaxios v7 Array.toString() stream corruption by @gsquared94 in
+ [#21884](https://github.com/google-gemini/gemini-cli/pull/21884)
+- fix: prevent hangs in non-interactive mode and improve agent guidance by
+ @cocosheng-g in
+ [#20893](https://github.com/google-gemini/gemini-cli/pull/20893)
+- Add ExtensionDetails dialog and support install by @chrstnb in
+ [#20845](https://github.com/google-gemini/gemini-cli/pull/20845)
+- chore/release: bump version to 0.34.0-nightly.20260310.4653b126f by
@gemini-cli-robot in
- [#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
-- fix(patch): cherry-pick 173376b to release/v0.33.0-preview.1-pr-21157 to patch
- version v0.33.0-preview.1 and create version 0.33.0-preview.2 by
- @gemini-cli-robot in
- [#21300](https://github.com/google-gemini/gemini-cli/pull/21300)
-- fix(patch): cherry-pick 0135b03 to release/v0.33.0-preview.2-pr-21171
+ [#21816](https://github.com/google-gemini/gemini-cli/pull/21816)
+- Changelog for v0.33.0-preview.13 by @gemini-cli-robot in
+ [#21927](https://github.com/google-gemini/gemini-cli/pull/21927)
+- fix(cli): stabilize prompt layout to prevent jumping when typing by
+ @NTaylorMullen in
+ [#21081](https://github.com/google-gemini/gemini-cli/pull/21081)
+- fix: preserve prompt text when cancelling streaming by @Nixxx19 in
+ [#21103](https://github.com/google-gemini/gemini-cli/pull/21103)
+- fix: robust UX for remote agent errors by @Shyam-Raghuwanshi in
+ [#20307](https://github.com/google-gemini/gemini-cli/pull/20307)
+- feat: implement background process logging and cleanup by @galz10 in
+ [#21189](https://github.com/google-gemini/gemini-cli/pull/21189)
+- Changelog for v0.33.0-preview.14 by @gemini-cli-robot in
+ [#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
+- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
[CONFLICTS] by @gemini-cli-robot in
- [#21336](https://github.com/google-gemini/gemini-cli/pull/21336)
-- fix(patch): cherry-pick 7ec477d to release/v0.33.0-preview.3-pr-21305 to patch
- version v0.33.0-preview.3 and create version 0.33.0-preview.4 by
+ [#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
+- fix(patch): cherry-pick 8432bce to release/v0.34.0-preview.1-pr-22069 to patch
+ version v0.34.0-preview.1 and create version 0.34.0-preview.2 by
@gemini-cli-robot in
- [#21349](https://github.com/google-gemini/gemini-cli/pull/21349)
-- fix(patch): cherry-pick 931e668 to release/v0.33.0-preview.4-pr-21425
- [CONFLICTS] by @gemini-cli-robot in
- [#21478](https://github.com/google-gemini/gemini-cli/pull/21478)
-- fix(patch): cherry-pick 7837194 to release/v0.33.0-preview.5-pr-21487 to patch
- version v0.33.0-preview.5 and create version 0.33.0-preview.6 by
+ [#22205](https://github.com/google-gemini/gemini-cli/pull/22205)
+- fix(patch): cherry-pick 24adacd to release/v0.34.0-preview.2-pr-22332 to patch
+ version v0.34.0-preview.2 and create version 0.34.0-preview.3 by
@gemini-cli-robot in
- [#21720](https://github.com/google-gemini/gemini-cli/pull/21720)
-- fix(patch): cherry-pick 4f4431e to release/v0.33.0-preview.7-pr-21750 to patch
- version v0.33.0-preview.7 and create version 0.33.0-preview.8 by
+ [#22391](https://github.com/google-gemini/gemini-cli/pull/22391)
+- fix(patch): cherry-pick 48130eb to release/v0.34.0-preview.3-pr-22665 to patch
+ version v0.34.0-preview.3 and create version 0.34.0-preview.4 by
@gemini-cli-robot in
- [#21782](https://github.com/google-gemini/gemini-cli/pull/21782)
-- fix(patch): cherry-pick 9a74271 to release/v0.33.0-preview.8-pr-21236
- [CONFLICTS] by @gemini-cli-robot in
- [#21788](https://github.com/google-gemini/gemini-cli/pull/21788)
-- fix(patch): cherry-pick 936f624 to release/v0.33.0-preview.9-pr-21702 to patch
- version v0.33.0-preview.9 and create version 0.33.0-preview.10 by
- @gemini-cli-robot in
- [#21800](https://github.com/google-gemini/gemini-cli/pull/21800)
-- fix(patch): cherry-pick 35ee2a8 to release/v0.33.0-preview.10-pr-21713 by
- @gemini-cli-robot in
- [#21859](https://github.com/google-gemini/gemini-cli/pull/21859)
-- fix(patch): cherry-pick 5dd2dab to release/v0.33.0-preview.11-pr-21871 by
- @gemini-cli-robot in
- [#21876](https://github.com/google-gemini/gemini-cli/pull/21876)
-- fix(patch): cherry-pick e5615f4 to release/v0.33.0-preview.12-pr-21037 to
- patch version v0.33.0-preview.12 and create version 0.33.0-preview.13 by
- @gemini-cli-robot in
- [#21922](https://github.com/google-gemini/gemini-cli/pull/21922)
-- fix(patch): cherry-pick 1b69637 to release/v0.33.0-preview.13-pr-21467
- [CONFLICTS] by @gemini-cli-robot in
- [#21930](https://github.com/google-gemini/gemini-cli/pull/21930)
-- fix(patch): cherry-pick 3ff68a9 to release/v0.33.0-preview.14-pr-21884
- [CONFLICTS] by @gemini-cli-robot in
- [#21952](https://github.com/google-gemini/gemini-cli/pull/21952)
+ [#22719](https://github.com/google-gemini/gemini-cli/pull/22719)
**Full Changelog**:
-https://github.com/google-gemini/gemini-cli/compare/v0.32.1...v0.33.1
+https://github.com/google-gemini/gemini-cli/compare/v0.33.2...v0.34.0
diff --git a/docs/changelogs/preview.md b/docs/changelogs/preview.md
index 43a02728b3..91d0c09a0b 100644
--- a/docs/changelogs/preview.md
+++ b/docs/changelogs/preview.md
@@ -1,6 +1,6 @@
-# Preview release: v0.34.0-preview.2
+# Preview release: v0.35.0-preview.1
-Released: March 12, 2026
+Released: March 17, 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,463 +13,364 @@ npm install -g @google/gemini-cli@preview
## Highlights
-- **Plan Mode Enabled by Default:** Plan Mode is now enabled out-of-the-box,
- providing a structured planning workflow and keeping approved plans during
- chat compression.
-- **Sandboxing Enhancements:** Added experimental LXC container sandbox support
- and native gVisor (`runsc`) sandboxing for improved security and isolation.
-- **Tracker Visualization and Tools:** Introduced CRUD tools and visualization
- for trackers, along with task tracker strategy improvements.
-- **Browser Agent Improvements:** Enhanced the browser agent with progress
- emission, a new automation overlay, and additional integration tests.
-- **CLI and UI Updates:** Standardized semantic focus colors, polished shell
- autocomplete rendering, unified keybinding infrastructure, and added custom
- footer configuration options.
+- **Subagents & Architecture Enhancements**: Enabled subagents and laid the
+ foundation for subagent tool isolation. Added proxy routing support for remote
+ A2A subagents and integrated `SandboxManager` to sandbox all process-spawning
+ tools.
+- **CLI & UI Improvements**: Introduced customizable keyboard shortcuts and
+ support for literal character keybindings. Added missing vim mode motions and
+ CJK input support. Enabled code splitting and deferred UI loading for improved
+ performance.
+- **Context & Tools Optimization**: JIT context loading is now enabled by
+ default with deduplication for project memory. Introduced a model-driven
+ parallel tool scheduler and allowed safe tools to execute concurrently.
+- **Security & Extensions**: Implemented cryptographic integrity verification
+ for extension updates and added a `disableAlwaysAllow` setting to prevent
+ auto-approvals for enhanced security.
+- **Plan Mode & Web Fetch Updates**: Added an 'All the above' option for
+ multi-select AskUser questions in Plan Mode. Rolled out Stage 1 and Stage 2
+ security and consistency improvements for the `web_fetch` tool.
## What's Changed
-- fix(patch): cherry-pick 8432bce to release/v0.34.0-preview.1-pr-22069 to patch
- version v0.34.0-preview.1 and create version 0.34.0-preview.2 by
+- feat(cli): customizable keyboard shortcuts by @scidomino in
+ [#21945](https://github.com/google-gemini/gemini-cli/pull/21945)
+- feat(core): Thread `AgentLoopContext` through core. by @joshualitt in
+ [#21944](https://github.com/google-gemini/gemini-cli/pull/21944)
+- chore(release): bump version to 0.35.0-nightly.20260311.657f19c1f by
@gemini-cli-robot in
- [#22205](https://github.com/google-gemini/gemini-cli/pull/22205)
-- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
- [CONFLICTS] by @gemini-cli-robot in
- [#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
-- feat(cli): add chat resume footer on session quit by @lordshashank in
- [#20667](https://github.com/google-gemini/gemini-cli/pull/20667)
-- Support bold and other styles in svg snapshots by @jacob314 in
- [#20937](https://github.com/google-gemini/gemini-cli/pull/20937)
-- fix(core): increase A2A agent timeout to 30 minutes by @adamfweidman in
- [#21028](https://github.com/google-gemini/gemini-cli/pull/21028)
-- Cleanup old branches. by @jacob314 in
- [#19354](https://github.com/google-gemini/gemini-cli/pull/19354)
-- chore(release): bump version to 0.34.0-nightly.20260303.34f0c1538 by
- @gemini-cli-robot in
- [#21034](https://github.com/google-gemini/gemini-cli/pull/21034)
-- feat(ui): standardize semantic focus colors and enhance history visibility by
- @keithguerin in
- [#20745](https://github.com/google-gemini/gemini-cli/pull/20745)
-- fix: merge duplicate imports in packages/core (3/4) by @Nixxx19 in
- [#20928](https://github.com/google-gemini/gemini-cli/pull/20928)
-- Add extra safety checks for proto pollution by @jacob314 in
- [#20396](https://github.com/google-gemini/gemini-cli/pull/20396)
-- feat(core): Add tracker CRUD tools & visualization by @anj-s in
- [#19489](https://github.com/google-gemini/gemini-cli/pull/19489)
-- Revert "fix(ui): persist expansion in AskUser dialog when navigating options"
- by @jacob314 in
- [#21042](https://github.com/google-gemini/gemini-cli/pull/21042)
-- Changelog for v0.33.0-preview.0 by @gemini-cli-robot in
- [#21030](https://github.com/google-gemini/gemini-cli/pull/21030)
-- fix: model persistence for all scenarios by @sripasg in
- [#21051](https://github.com/google-gemini/gemini-cli/pull/21051)
-- chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 by
- @gemini-cli-robot in
- [#21054](https://github.com/google-gemini/gemini-cli/pull/21054)
-- Consistently guard restarts against concurrent auto updates by @scidomino in
- [#21016](https://github.com/google-gemini/gemini-cli/pull/21016)
-- Defensive coding to reduce the risk of Maximum update depth errors by
- @jacob314 in [#20940](https://github.com/google-gemini/gemini-cli/pull/20940)
-- fix(cli): Polish shell autocomplete rendering to be a little more shell native
- feeling. by @jacob314 in
- [#20931](https://github.com/google-gemini/gemini-cli/pull/20931)
-- Docs: Update plan mode docs by @jkcinouye in
- [#19682](https://github.com/google-gemini/gemini-cli/pull/19682)
-- fix(mcp): Notifications/tools/list_changed support not working by @jacob314 in
- [#21050](https://github.com/google-gemini/gemini-cli/pull/21050)
-- fix(cli): register extension lifecycle events in DebugProfiler by
- @fayerman-source in
- [#20101](https://github.com/google-gemini/gemini-cli/pull/20101)
-- chore(dev): update vscode settings for typescriptreact by @rohit-4321 in
- [#19907](https://github.com/google-gemini/gemini-cli/pull/19907)
-- fix(cli): enable multi-arch docker builds for sandbox by @ru-aish in
- [#19821](https://github.com/google-gemini/gemini-cli/pull/19821)
-- Changelog for v0.32.0 by @gemini-cli-robot in
- [#21033](https://github.com/google-gemini/gemini-cli/pull/21033)
-- Changelog for v0.33.0-preview.1 by @gemini-cli-robot in
- [#21058](https://github.com/google-gemini/gemini-cli/pull/21058)
-- feat(core): improve @scripts/copy_files.js autocomplete to prioritize
- filenames by @sehoon38 in
- [#21064](https://github.com/google-gemini/gemini-cli/pull/21064)
-- feat(sandbox): add experimental LXC container sandbox support by @h30s in
- [#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
-- feat(evals): add overall pass rate row to eval nightly summary table by
- @gundermanc in
- [#20905](https://github.com/google-gemini/gemini-cli/pull/20905)
-- feat(telemetry): include language in telemetry and fix accepted lines
- computation by @gundermanc in
- [#21126](https://github.com/google-gemini/gemini-cli/pull/21126)
-- Changelog for v0.32.1 by @gemini-cli-robot in
- [#21055](https://github.com/google-gemini/gemini-cli/pull/21055)
-- feat(core): add robustness tests, logging, and metrics for CodeAssistServer
- SSE parsing by @yunaseoul in
- [#21013](https://github.com/google-gemini/gemini-cli/pull/21013)
-- feat: add issue assignee workflow by @kartikangiras in
- [#21003](https://github.com/google-gemini/gemini-cli/pull/21003)
-- fix: improve error message when OAuth succeeds but project ID is required by
- @Nixxx19 in [#21070](https://github.com/google-gemini/gemini-cli/pull/21070)
-- feat(loop-reduction): implement iterative loop detection and model feedback by
- @aishaneeshah in
- [#20763](https://github.com/google-gemini/gemini-cli/pull/20763)
-- chore(github): require prompt approvers for agent prompt files by @gundermanc
- in [#20896](https://github.com/google-gemini/gemini-cli/pull/20896)
-- Docs: Create tools reference by @jkcinouye in
- [#19470](https://github.com/google-gemini/gemini-cli/pull/19470)
-- fix(core, a2a-server): prevent hang during OAuth in non-interactive sessions
- by @spencer426 in
- [#21045](https://github.com/google-gemini/gemini-cli/pull/21045)
-- chore(cli): enable deprecated settings removal by default by @yashodipmore in
- [#20682](https://github.com/google-gemini/gemini-cli/pull/20682)
-- feat(core): Disable fast ack helper for hints. by @joshualitt in
- [#21011](https://github.com/google-gemini/gemini-cli/pull/21011)
-- fix(ui): suppress redundant failure note when tool error note is shown by
- @NTaylorMullen in
- [#21078](https://github.com/google-gemini/gemini-cli/pull/21078)
-- docs: document planning workflows with Conductor example by @jerop in
- [#21166](https://github.com/google-gemini/gemini-cli/pull/21166)
-- feat(release): ship esbuild bundle in npm package by @genneth in
- [#19171](https://github.com/google-gemini/gemini-cli/pull/19171)
-- fix(extensions): preserve symlinks in extension source path while enforcing
- folder trust by @galz10 in
- [#20867](https://github.com/google-gemini/gemini-cli/pull/20867)
-- fix(cli): defer tool exclusions to policy engine in non-interactive mode by
- @EricRahm in [#20639](https://github.com/google-gemini/gemini-cli/pull/20639)
-- fix(ui): removed double padding on rendered content by @devr0306 in
- [#21029](https://github.com/google-gemini/gemini-cli/pull/21029)
-- fix(core): truncate excessively long lines in grep search output by
- @gundermanc in
- [#21147](https://github.com/google-gemini/gemini-cli/pull/21147)
-- feat: add custom footer configuration via `/footer` by @jackwotherspoon in
- [#19001](https://github.com/google-gemini/gemini-cli/pull/19001)
-- perf(core): fix OOM crash in long-running sessions by @WizardsForgeGames in
- [#19608](https://github.com/google-gemini/gemini-cli/pull/19608)
-- refactor(cli): categorize built-in themes into dark/ and light/ directories by
- @JayadityaGit in
- [#18634](https://github.com/google-gemini/gemini-cli/pull/18634)
-- fix(core): explicitly allow codebase_investigator and cli_help in read-only
- mode by @Adib234 in
- [#21157](https://github.com/google-gemini/gemini-cli/pull/21157)
-- test: add browser agent integration tests by @kunal-10-cloud in
- [#21151](https://github.com/google-gemini/gemini-cli/pull/21151)
-- fix(cli): fix enabling kitty codes on Windows Terminal by @scidomino in
- [#21136](https://github.com/google-gemini/gemini-cli/pull/21136)
-- refactor(core): extract shared OAuth flow primitives from MCPOAuthProvider by
- @SandyTao520 in
- [#20895](https://github.com/google-gemini/gemini-cli/pull/20895)
-- fix(ui): add partial output to cancelled shell UI by @devr0306 in
- [#21178](https://github.com/google-gemini/gemini-cli/pull/21178)
-- fix(cli): replace hardcoded keybinding strings with dynamic formatters by
- @scidomino in [#21159](https://github.com/google-gemini/gemini-cli/pull/21159)
-- DOCS: Update quota and pricing page by @g-samroberts in
- [#21194](https://github.com/google-gemini/gemini-cli/pull/21194)
-- feat(telemetry): implement Clearcut logging for startup statistics by
- @yunaseoul in [#21172](https://github.com/google-gemini/gemini-cli/pull/21172)
-- feat(triage): add area/documentation to issue triage by @g-samroberts in
- [#21222](https://github.com/google-gemini/gemini-cli/pull/21222)
-- Fix so shell calls are formatted by @jacob314 in
- [#21237](https://github.com/google-gemini/gemini-cli/pull/21237)
-- feat(cli): add native gVisor (runsc) sandboxing support by @Zheyuan-Lin in
- [#21062](https://github.com/google-gemini/gemini-cli/pull/21062)
-- docs: use absolute paths for internal links in plan-mode.md by @jerop in
- [#21299](https://github.com/google-gemini/gemini-cli/pull/21299)
-- fix(core): prevent unhandled AbortError crash during stream loop detection by
- @7hokerz in [#21123](https://github.com/google-gemini/gemini-cli/pull/21123)
-- fix:reorder env var redaction checks to scan values first by @kartikangiras in
- [#21059](https://github.com/google-gemini/gemini-cli/pull/21059)
-- fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences
- by @skeshive in
- [#21171](https://github.com/google-gemini/gemini-cli/pull/21171)
-- feat(core): fallback to 2.5 models with no access for toolcalls by @sehoon38
- in [#21283](https://github.com/google-gemini/gemini-cli/pull/21283)
-- test(core): improve testing for API request/response parsing by @sehoon38 in
- [#21227](https://github.com/google-gemini/gemini-cli/pull/21227)
-- docs(links): update docs-writer skill and fix broken link by @g-samroberts in
- [#21314](https://github.com/google-gemini/gemini-cli/pull/21314)
-- Fix code colorizer ansi escape bug. by @jacob314 in
- [#21321](https://github.com/google-gemini/gemini-cli/pull/21321)
-- remove wildcard behavior on keybindings by @scidomino in
- [#21315](https://github.com/google-gemini/gemini-cli/pull/21315)
-- feat(acp): Add support for AI Gateway auth by @skeshive in
- [#21305](https://github.com/google-gemini/gemini-cli/pull/21305)
-- fix(theme): improve theme color contrast for macOS Terminal.app by @clocky in
- [#21175](https://github.com/google-gemini/gemini-cli/pull/21175)
-- feat (core): Implement tracker related SI changes by @anj-s in
- [#19964](https://github.com/google-gemini/gemini-cli/pull/19964)
-- Changelog for v0.33.0-preview.2 by @gemini-cli-robot in
- [#21333](https://github.com/google-gemini/gemini-cli/pull/21333)
-- Changelog for v0.33.0-preview.3 by @gemini-cli-robot in
- [#21347](https://github.com/google-gemini/gemini-cli/pull/21347)
-- docs: format release times as HH:MM UTC by @pavan-sh in
- [#20726](https://github.com/google-gemini/gemini-cli/pull/20726)
-- fix(cli): implement --all flag for extensions uninstall by @sehoon38 in
- [#21319](https://github.com/google-gemini/gemini-cli/pull/21319)
-- docs: fix incorrect relative links to command reference by @kanywst in
- [#20964](https://github.com/google-gemini/gemini-cli/pull/20964)
-- documentiong ensures ripgrep by @Jatin24062005 in
- [#21298](https://github.com/google-gemini/gemini-cli/pull/21298)
-- fix(core): handle AbortError thrown during processTurn by @MumuTW in
- [#21296](https://github.com/google-gemini/gemini-cli/pull/21296)
-- docs(cli): clarify ! command output visibility in shell commands tutorial by
- @MohammedADev in
- [#21041](https://github.com/google-gemini/gemini-cli/pull/21041)
-- fix: logic for task tracker strategy and remove tracker tools by @anj-s in
- [#21355](https://github.com/google-gemini/gemini-cli/pull/21355)
-- fix(partUtils): display media type and size for inline data parts by @Aboudjem
- in [#21358](https://github.com/google-gemini/gemini-cli/pull/21358)
-- Fix(accessibility): add screen reader support to RewindViewer by @Famous077 in
- [#20750](https://github.com/google-gemini/gemini-cli/pull/20750)
-- fix(hooks): propagate stopHookActive in AfterAgent retry path (#20426) by
- @Aarchi-07 in [#20439](https://github.com/google-gemini/gemini-cli/pull/20439)
-- fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive
- filesystems (#19904) by @Nixxx19 in
- [#19915](https://github.com/google-gemini/gemini-cli/pull/19915)
-- feat(core): add concurrency safety guidance for subagent delegation (#17753)
- by @abhipatel12 in
- [#21278](https://github.com/google-gemini/gemini-cli/pull/21278)
-- feat(ui): dynamically generate all keybinding hints by @scidomino in
- [#21346](https://github.com/google-gemini/gemini-cli/pull/21346)
-- feat(core): implement unified KeychainService and migrate token storage by
- @ehedlund in [#21344](https://github.com/google-gemini/gemini-cli/pull/21344)
-- fix(cli): gracefully handle --resume when no sessions exist by @SandyTao520 in
- [#21429](https://github.com/google-gemini/gemini-cli/pull/21429)
-- fix(plan): keep approved plan during chat compression by @ruomengz in
- [#21284](https://github.com/google-gemini/gemini-cli/pull/21284)
-- feat(core): implement generic CacheService and optimize setupUser by @sehoon38
- in [#21374](https://github.com/google-gemini/gemini-cli/pull/21374)
-- Update quota and pricing documentation with subscription tiers by @srithreepo
- in [#21351](https://github.com/google-gemini/gemini-cli/pull/21351)
-- fix(core): append correct OTLP paths for HTTP exporters by
- @sebastien-prudhomme in
- [#16836](https://github.com/google-gemini/gemini-cli/pull/16836)
-- Changelog for v0.33.0-preview.4 by @gemini-cli-robot in
- [#21354](https://github.com/google-gemini/gemini-cli/pull/21354)
-- feat(cli): implement dot-prefixing for slash command conflicts by @ehedlund in
- [#20979](https://github.com/google-gemini/gemini-cli/pull/20979)
-- refactor(core): standardize MCP tool naming to mcp\_ FQN format by
- @abhipatel12 in
- [#21425](https://github.com/google-gemini/gemini-cli/pull/21425)
-- feat(cli): hide gemma settings from display and mark as experimental by
- @abhipatel12 in
- [#21471](https://github.com/google-gemini/gemini-cli/pull/21471)
-- feat(skills): refine string-reviewer guidelines and description by @clocky in
- [#20368](https://github.com/google-gemini/gemini-cli/pull/20368)
-- fix(core): whitelist TERM and COLORTERM in environment sanitization by
- @deadsmash07 in
- [#20514](https://github.com/google-gemini/gemini-cli/pull/20514)
-- fix(billing): fix overage strategy lifecycle and settings integration by
- @gsquared94 in
- [#21236](https://github.com/google-gemini/gemini-cli/pull/21236)
-- fix: expand paste placeholders in TextInput on submit by @Jefftree in
- [#19946](https://github.com/google-gemini/gemini-cli/pull/19946)
-- fix(core): add in-memory cache to ChatRecordingService to prevent OOM by
- @SandyTao520 in
- [#21502](https://github.com/google-gemini/gemini-cli/pull/21502)
-- feat(cli): overhaul thinking UI by @keithguerin in
- [#18725](https://github.com/google-gemini/gemini-cli/pull/18725)
-- fix(ui): unify Ctrl+O expansion hint experience across buffer modes by
- @jwhelangoog in
- [#21474](https://github.com/google-gemini/gemini-cli/pull/21474)
-- fix(cli): correct shell height reporting by @jacob314 in
- [#21492](https://github.com/google-gemini/gemini-cli/pull/21492)
-- Make test suite pass when the GEMINI_SYSTEM_MD env variable or
- GEMINI_WRITE_SYSTEM_MD variable happens to be set locally/ by @jacob314 in
- [#21480](https://github.com/google-gemini/gemini-cli/pull/21480)
-- Disallow underspecified types by @gundermanc in
- [#21485](https://github.com/google-gemini/gemini-cli/pull/21485)
-- refactor(cli): standardize on 'reload' verb for all components by @keithguerin
- in [#20654](https://github.com/google-gemini/gemini-cli/pull/20654)
-- feat(cli): Invert quota language to 'percent used' by @keithguerin in
- [#20100](https://github.com/google-gemini/gemini-cli/pull/20100)
-- Docs: Add documentation for notifications (experimental)(macOS) by @jkcinouye
- in [#21163](https://github.com/google-gemini/gemini-cli/pull/21163)
-- Code review comments as a pr by @jacob314 in
- [#21209](https://github.com/google-gemini/gemini-cli/pull/21209)
-- feat(cli): unify /chat and /resume command UX by @LyalinDotCom in
- [#20256](https://github.com/google-gemini/gemini-cli/pull/20256)
-- docs: fix typo 'allowslisted' -> 'allowlisted' in mcp-server.md by
+ [#21966](https://github.com/google-gemini/gemini-cli/pull/21966)
+- refactor(a2a): remove legacy CoreToolScheduler by @adamfweidman in
+ [#21955](https://github.com/google-gemini/gemini-cli/pull/21955)
+- feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)
+ by @aanari in [#21932](https://github.com/google-gemini/gemini-cli/pull/21932)
+- Feat/retry fetch notifications by @aishaneeshah in
+ [#21813](https://github.com/google-gemini/gemini-cli/pull/21813)
+- fix(core): remove OAuth check from handleFallback and clean up stray file by
+ @sehoon38 in [#21962](https://github.com/google-gemini/gemini-cli/pull/21962)
+- feat(cli): support literal character keybindings and extended Kitty protocol
+ keys by @scidomino in
+ [#21972](https://github.com/google-gemini/gemini-cli/pull/21972)
+- fix(ui): clamp cursor to last char after all NORMAL mode deletes by @aanari in
+ [#21973](https://github.com/google-gemini/gemini-cli/pull/21973)
+- test(core): add missing tests for prompts/utils.ts by @krrishverma1805-web in
+ [#19941](https://github.com/google-gemini/gemini-cli/pull/19941)
+- fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) by
+ @nsalerni in [#19933](https://github.com/google-gemini/gemini-cli/pull/19933)
+- docs(cli): add custom keybinding documentation by @scidomino in
+ [#21980](https://github.com/google-gemini/gemini-cli/pull/21980)
+- docs: fix misleading YOLO mode description in defaultApprovalMode by
@Gyanranjan-Priyam in
- [#21665](https://github.com/google-gemini/gemini-cli/pull/21665)
-- fix(core): display actual graph output in tracker_visualize tool by @anj-s in
- [#21455](https://github.com/google-gemini/gemini-cli/pull/21455)
-- fix(core): sanitize SSE-corrupted JSON and domain strings in error
- classification by @gsquared94 in
- [#21702](https://github.com/google-gemini/gemini-cli/pull/21702)
-- Docs: Make documentation links relative by @diodesign in
- [#21490](https://github.com/google-gemini/gemini-cli/pull/21490)
-- feat(cli): expose /tools desc as explicit subcommand for discoverability by
- @aworki in [#21241](https://github.com/google-gemini/gemini-cli/pull/21241)
-- feat(cli): add /compact alias for /compress command by @jackwotherspoon in
- [#21711](https://github.com/google-gemini/gemini-cli/pull/21711)
-- feat(plan): enable Plan Mode by default by @jerop in
- [#21713](https://github.com/google-gemini/gemini-cli/pull/21713)
-- feat(core): Introduce `AgentLoopContext`. by @joshualitt in
- [#21198](https://github.com/google-gemini/gemini-cli/pull/21198)
-- fix(core): resolve symlinks for non-existent paths during validation by
- @Adib234 in [#21487](https://github.com/google-gemini/gemini-cli/pull/21487)
-- docs: document tool exclusion from memory via deny policy by @Abhijit-2592 in
- [#21428](https://github.com/google-gemini/gemini-cli/pull/21428)
-- perf(core): cache loadApiKey to reduce redundant keychain access by @sehoon38
- in [#21520](https://github.com/google-gemini/gemini-cli/pull/21520)
-- feat(cli): implement /upgrade command by @sehoon38 in
- [#21511](https://github.com/google-gemini/gemini-cli/pull/21511)
-- Feat/browser agent progress emission by @kunal-10-cloud in
- [#21218](https://github.com/google-gemini/gemini-cli/pull/21218)
-- fix(settings): display objects as JSON instead of [object Object] by
- @Zheyuan-Lin in
- [#21458](https://github.com/google-gemini/gemini-cli/pull/21458)
-- Unmarshall update by @DavidAPierce in
- [#21721](https://github.com/google-gemini/gemini-cli/pull/21721)
-- Update mcp's list function to check for disablement. by @DavidAPierce in
- [#21148](https://github.com/google-gemini/gemini-cli/pull/21148)
-- robustness(core): static checks to validate history is immutable by @jacob314
- in [#21228](https://github.com/google-gemini/gemini-cli/pull/21228)
-- refactor(cli): better react patterns for BaseSettingsDialog by @psinha40898 in
- [#21206](https://github.com/google-gemini/gemini-cli/pull/21206)
-- feat(security): implement robust IP validation and safeFetch foundation by
- @alisa-alisa in
- [#21401](https://github.com/google-gemini/gemini-cli/pull/21401)
-- feat(core): improve subagent result display by @joshualitt in
- [#20378](https://github.com/google-gemini/gemini-cli/pull/20378)
-- docs: fix broken markdown syntax and anchor links in /tools by @campox747 in
- [#20902](https://github.com/google-gemini/gemini-cli/pull/20902)
-- feat(policy): support subagent-specific policies in TOML by @akh64bit in
- [#21431](https://github.com/google-gemini/gemini-cli/pull/21431)
-- Add script to speed up reviewing PRs adding a worktree. by @jacob314 in
- [#21748](https://github.com/google-gemini/gemini-cli/pull/21748)
-- fix(core): prevent infinite recursion in symlink resolution by @Adib234 in
- [#21750](https://github.com/google-gemini/gemini-cli/pull/21750)
-- fix(docs): fix headless mode docs by @ame2en in
- [#21287](https://github.com/google-gemini/gemini-cli/pull/21287)
-- feat/redesign header compact by @jacob314 in
- [#20922](https://github.com/google-gemini/gemini-cli/pull/20922)
-- refactor: migrate to useKeyMatchers hook by @scidomino in
- [#21753](https://github.com/google-gemini/gemini-cli/pull/21753)
-- perf(cli): cache loadSettings to reduce redundant disk I/O at startup by
- @sehoon38 in [#21521](https://github.com/google-gemini/gemini-cli/pull/21521)
-- fix(core): resolve Windows line ending and path separation bugs across CLI by
- @muhammadusman586 in
- [#21068](https://github.com/google-gemini/gemini-cli/pull/21068)
-- docs: fix heading formatting in commands.md and phrasing in tools-api.md by
- @campox747 in [#20679](https://github.com/google-gemini/gemini-cli/pull/20679)
-- refactor(ui): unify keybinding infrastructure and support string
- initialization by @scidomino in
- [#21776](https://github.com/google-gemini/gemini-cli/pull/21776)
-- Add support for updating extension sources and names by @chrstnb in
- [#21715](https://github.com/google-gemini/gemini-cli/pull/21715)
-- fix(core): handle GUI editor non-zero exit codes gracefully by @reyyanxahmed
- in [#20376](https://github.com/google-gemini/gemini-cli/pull/20376)
-- fix(core): destroy PTY on kill() and exception to prevent fd leak by @nbardy
- in [#21693](https://github.com/google-gemini/gemini-cli/pull/21693)
-- fix(docs): update theme screenshots and add missing themes by @ashmod in
- [#20689](https://github.com/google-gemini/gemini-cli/pull/20689)
-- refactor(cli): rename 'return' key to 'enter' internally by @scidomino in
- [#21796](https://github.com/google-gemini/gemini-cli/pull/21796)
-- build(release): restrict npm bundling to non-stable tags by @sehoon38 in
- [#21821](https://github.com/google-gemini/gemini-cli/pull/21821)
-- fix(core): override toolRegistry property for sub-agent schedulers by
- @gsquared94 in
- [#21766](https://github.com/google-gemini/gemini-cli/pull/21766)
-- fix(cli): make footer items equally spaced by @jacob314 in
- [#21843](https://github.com/google-gemini/gemini-cli/pull/21843)
-- docs: clarify global policy rules application in plan mode by @jerop in
- [#21864](https://github.com/google-gemini/gemini-cli/pull/21864)
-- fix(core): ensure correct flash model steering in plan mode implementation
- phase by @jerop in
- [#21871](https://github.com/google-gemini/gemini-cli/pull/21871)
-- fix(core): update @a2a-js/sdk to 0.3.11 by @adamfweidman in
- [#21875](https://github.com/google-gemini/gemini-cli/pull/21875)
-- refactor(core): improve API response error logging when retry by @yunaseoul in
- [#21784](https://github.com/google-gemini/gemini-cli/pull/21784)
-- fix(ui): handle headless execution in credits and upgrade dialogs by
- @gsquared94 in
- [#21850](https://github.com/google-gemini/gemini-cli/pull/21850)
-- fix(core): treat retryable errors with >5 min delay as terminal quota errors
- by @gsquared94 in
- [#21881](https://github.com/google-gemini/gemini-cli/pull/21881)
-- feat(telemetry): add specific PR, issue, and custom tracking IDs for GitHub
- Actions by @cocosheng-g in
- [#21129](https://github.com/google-gemini/gemini-cli/pull/21129)
-- feat(core): add OAuth2 Authorization Code auth provider for A2A agents by
- @SandyTao520 in
- [#21496](https://github.com/google-gemini/gemini-cli/pull/21496)
-- feat(cli): give visibility to /tools list command in the TUI and follow the
- subcommand pattern of other commands by @JayadityaGit in
- [#21213](https://github.com/google-gemini/gemini-cli/pull/21213)
-- Handle dirty worktrees better and warn about running scripts/review.sh on
- untrusted code. by @jacob314 in
- [#21791](https://github.com/google-gemini/gemini-cli/pull/21791)
-- feat(policy): support auto-add to policy by default and scoped persistence by
+ [#21878](https://github.com/google-gemini/gemini-cli/pull/21878)
+- fix: clean up /clear and /resume by @jackwotherspoon in
+ [#22007](https://github.com/google-gemini/gemini-cli/pull/22007)
+- fix(core)#20941: reap orphaned descendant processes on PTY abort by @manavmax
+ in [#21124](https://github.com/google-gemini/gemini-cli/pull/21124)
+- fix(core): update language detection to use LSP 3.18 identifiers by @yunaseoul
+ in [#21931](https://github.com/google-gemini/gemini-cli/pull/21931)
+- feat(cli): support removing keybindings via '-' prefix by @scidomino in
+ [#22042](https://github.com/google-gemini/gemini-cli/pull/22042)
+- feat(policy): add --admin-policy flag for supplemental admin policies by
+ @galz10 in [#20360](https://github.com/google-gemini/gemini-cli/pull/20360)
+- merge duplicate imports packages/cli/src subtask1 by @Nixxx19 in
+ [#22040](https://github.com/google-gemini/gemini-cli/pull/22040)
+- perf(core): parallelize user quota and experiments fetching in refreshAuth by
+ @sehoon38 in [#21648](https://github.com/google-gemini/gemini-cli/pull/21648)
+- Changelog for v0.34.0-preview.0 by @gemini-cli-robot in
+ [#21965](https://github.com/google-gemini/gemini-cli/pull/21965)
+- Changelog for v0.33.0 by @gemini-cli-robot in
+ [#21967](https://github.com/google-gemini/gemini-cli/pull/21967)
+- fix(core): handle EISDIR in robustRealpath on Windows by @sehoon38 in
+ [#21984](https://github.com/google-gemini/gemini-cli/pull/21984)
+- feat(core): include initiationMethod in conversation interaction telemetry by
+ @yunaseoul in [#22054](https://github.com/google-gemini/gemini-cli/pull/22054)
+- feat(ui): add vim yank/paste (y/p/P) with unnamed register by @aanari in
+ [#22026](https://github.com/google-gemini/gemini-cli/pull/22026)
+- fix(core): enable numerical routing for api key users by @sehoon38 in
+ [#21977](https://github.com/google-gemini/gemini-cli/pull/21977)
+- feat(telemetry): implement retry attempt telemetry for network related retries
+ by @aishaneeshah in
+ [#22027](https://github.com/google-gemini/gemini-cli/pull/22027)
+- fix(policy): remove unnecessary escapeRegex from pattern builders by
@spencer426 in
- [#20361](https://github.com/google-gemini/gemini-cli/pull/20361)
-- fix(core): handle AbortError when ESC cancels tool execution by @PrasannaPal21
- in [#20863](https://github.com/google-gemini/gemini-cli/pull/20863)
-- fix(release): Improve Patch Release Workflow Comments: Clearer Approval
- Guidance by @jerop in
- [#21894](https://github.com/google-gemini/gemini-cli/pull/21894)
-- docs: clarify telemetry setup and comprehensive data map by @jerop in
- [#21879](https://github.com/google-gemini/gemini-cli/pull/21879)
-- feat(core): add per-model token usage to stream-json output by @yongruilin in
- [#21839](https://github.com/google-gemini/gemini-cli/pull/21839)
-- docs: remove experimental badge from plan mode in sidebar by @jerop in
- [#21906](https://github.com/google-gemini/gemini-cli/pull/21906)
-- fix(cli): prevent race condition in loop detection retry by @skyvanguard in
- [#17916](https://github.com/google-gemini/gemini-cli/pull/17916)
-- Add behavioral evals for tracker by @anj-s in
- [#20069](https://github.com/google-gemini/gemini-cli/pull/20069)
-- fix(auth): update terminology to 'sign in' and 'sign out' by @clocky in
- [#20892](https://github.com/google-gemini/gemini-cli/pull/20892)
-- docs(mcp): standardize mcp tool fqn documentation by @abhipatel12 in
- [#21664](https://github.com/google-gemini/gemini-cli/pull/21664)
-- fix(ui): prevent empty tool-group border stubs after filtering by @Aaxhirrr in
- [#21852](https://github.com/google-gemini/gemini-cli/pull/21852)
-- make command names consistent by @scidomino in
- [#21907](https://github.com/google-gemini/gemini-cli/pull/21907)
-- refactor: remove agent_card_requires_auth config flag by @adamfweidman in
- [#21914](https://github.com/google-gemini/gemini-cli/pull/21914)
-- feat(a2a): implement standardized normalization and streaming reassembly by
- @alisa-alisa in
- [#21402](https://github.com/google-gemini/gemini-cli/pull/21402)
-- feat(cli): enable skill activation via slash commands by @NTaylorMullen in
- [#21758](https://github.com/google-gemini/gemini-cli/pull/21758)
-- docs(cli): mention per-model token usage in stream-json result event by
- @yongruilin in
- [#21908](https://github.com/google-gemini/gemini-cli/pull/21908)
-- fix(plan): prevent plan truncation in approval dialog by supporting
- unconstrained heights by @Adib234 in
- [#21037](https://github.com/google-gemini/gemini-cli/pull/21037)
-- feat(a2a): switch from callback-based to event-driven tool scheduler by
- @cocosheng-g in
- [#21467](https://github.com/google-gemini/gemini-cli/pull/21467)
-- feat(voice): implement speech-friendly response formatter by @Solventerritory
- in [#20989](https://github.com/google-gemini/gemini-cli/pull/20989)
-- feat: add pulsating blue border automation overlay to browser agent by
- @kunal-10-cloud in
- [#21173](https://github.com/google-gemini/gemini-cli/pull/21173)
-- Add extensionRegistryURI setting to change where the registry is read from by
- @kevinjwang1 in
- [#20463](https://github.com/google-gemini/gemini-cli/pull/20463)
-- fix: patch gaxios v7 Array.toString() stream corruption by @gsquared94 in
- [#21884](https://github.com/google-gemini/gemini-cli/pull/21884)
-- fix: prevent hangs in non-interactive mode and improve agent guidance by
- @cocosheng-g in
- [#20893](https://github.com/google-gemini/gemini-cli/pull/20893)
-- Add ExtensionDetails dialog and support install by @chrstnb in
- [#20845](https://github.com/google-gemini/gemini-cli/pull/20845)
-- chore/release: bump version to 0.34.0-nightly.20260310.4653b126f by
- @gemini-cli-robot in
- [#21816](https://github.com/google-gemini/gemini-cli/pull/21816)
-- Changelog for v0.33.0-preview.13 by @gemini-cli-robot in
- [#21927](https://github.com/google-gemini/gemini-cli/pull/21927)
-- fix(cli): stabilize prompt layout to prevent jumping when typing by
+ [#21921](https://github.com/google-gemini/gemini-cli/pull/21921)
+- fix(core): preserve dynamic tool descriptions on session resume by @sehoon38
+ in [#18835](https://github.com/google-gemini/gemini-cli/pull/18835)
+- chore: allow 'gemini-3.1' in sensitive keyword linter by @scidomino in
+ [#22065](https://github.com/google-gemini/gemini-cli/pull/22065)
+- feat(core): support custom base URL via env vars by @junaiddshaukat in
+ [#21561](https://github.com/google-gemini/gemini-cli/pull/21561)
+- merge duplicate imports packages/cli/src subtask2 by @Nixxx19 in
+ [#22051](https://github.com/google-gemini/gemini-cli/pull/22051)
+- fix(core): silently retry API errors up to 3 times before halting session by
+ @spencer426 in
+ [#21989](https://github.com/google-gemini/gemini-cli/pull/21989)
+- feat(core): simplify subagent success UI and improve early termination display
+ by @abhipatel12 in
+ [#21917](https://github.com/google-gemini/gemini-cli/pull/21917)
+- merge duplicate imports packages/cli/src subtask3 by @Nixxx19 in
+ [#22056](https://github.com/google-gemini/gemini-cli/pull/22056)
+- fix(hooks): fix BeforeAgent/AfterAgent inconsistencies (#18514) by @krishdef7
+ in [#21383](https://github.com/google-gemini/gemini-cli/pull/21383)
+- feat(core): implement SandboxManager interface and config schema by @galz10 in
+ [#21774](https://github.com/google-gemini/gemini-cli/pull/21774)
+- docs: document npm deprecation warnings as safe to ignore by @h30s in
+ [#20692](https://github.com/google-gemini/gemini-cli/pull/20692)
+- fix: remove status/need-triage from maintainer-only issues by @SandyTao520 in
+ [#22044](https://github.com/google-gemini/gemini-cli/pull/22044)
+- fix(core): propagate subagent context to policy engine by @NTaylorMullen in
+ [#22086](https://github.com/google-gemini/gemini-cli/pull/22086)
+- fix(cli): resolve skill uninstall failure when skill name is updated by
@NTaylorMullen in
- [#21081](https://github.com/google-gemini/gemini-cli/pull/21081)
-- fix: preserve prompt text when cancelling streaming by @Nixxx19 in
- [#21103](https://github.com/google-gemini/gemini-cli/pull/21103)
-- fix: robust UX for remote agent errors by @Shyam-Raghuwanshi in
- [#20307](https://github.com/google-gemini/gemini-cli/pull/20307)
-- feat: implement background process logging and cleanup by @galz10 in
- [#21189](https://github.com/google-gemini/gemini-cli/pull/21189)
-- Changelog for v0.33.0-preview.14 by @gemini-cli-robot in
- [#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
+ [#22085](https://github.com/google-gemini/gemini-cli/pull/22085)
+- docs(plan): clarify interactive plan editing with Ctrl+X by @Adib234 in
+ [#22076](https://github.com/google-gemini/gemini-cli/pull/22076)
+- fix(policy): ensure user policies are loaded when policyPaths is empty by
+ @NTaylorMullen in
+ [#22090](https://github.com/google-gemini/gemini-cli/pull/22090)
+- Docs: Add documentation for model steering (experimental). by @jkcinouye in
+ [#21154](https://github.com/google-gemini/gemini-cli/pull/21154)
+- Add issue for automated changelogs by @g-samroberts in
+ [#21912](https://github.com/google-gemini/gemini-cli/pull/21912)
+- fix(core): secure argsPattern and revert WEB_FETCH_TOOL_NAME escalation by
+ @spencer426 in
+ [#22104](https://github.com/google-gemini/gemini-cli/pull/22104)
+- feat(core): differentiate User-Agent for a2a-server and ACP clients by
+ @bdmorgan in [#22059](https://github.com/google-gemini/gemini-cli/pull/22059)
+- refactor(core): extract ExecutionLifecycleService for tool backgrounding by
+ @adamfweidman in
+ [#21717](https://github.com/google-gemini/gemini-cli/pull/21717)
+- feat: Display pending and confirming tool calls by @sripasg in
+ [#22106](https://github.com/google-gemini/gemini-cli/pull/22106)
+- feat(browser): implement input blocker overlay during automation by
+ @kunal-10-cloud in
+ [#21132](https://github.com/google-gemini/gemini-cli/pull/21132)
+- fix: register themes on extension load not start by @jackwotherspoon in
+ [#22148](https://github.com/google-gemini/gemini-cli/pull/22148)
+- feat(ui): Do not show Ultra users /upgrade hint (#22154) by @sehoon38 in
+ [#22156](https://github.com/google-gemini/gemini-cli/pull/22156)
+- chore: remove unnecessary log for themes by @jackwotherspoon in
+ [#22165](https://github.com/google-gemini/gemini-cli/pull/22165)
+- fix(core): resolve MCP tool FQN validation, schema export, and wildcards in
+ subagents by @abhipatel12 in
+ [#22069](https://github.com/google-gemini/gemini-cli/pull/22069)
+- fix(cli): validate --model argument at startup by @JaisalJain in
+ [#21393](https://github.com/google-gemini/gemini-cli/pull/21393)
+- fix(core): handle policy ALLOW for exit_plan_mode by @backnotprop in
+ [#21802](https://github.com/google-gemini/gemini-cli/pull/21802)
+- feat(telemetry): add Clearcut instrumentation for AI credits billing events by
+ @gsquared94 in
+ [#22153](https://github.com/google-gemini/gemini-cli/pull/22153)
+- feat(core): add google credentials provider for remote agents by @adamfweidman
+ in [#21024](https://github.com/google-gemini/gemini-cli/pull/21024)
+- test(cli): add integration test for node deprecation warnings by @Nixxx19 in
+ [#20215](https://github.com/google-gemini/gemini-cli/pull/20215)
+- feat(cli): allow safe tools to execute concurrently while agent is busy by
+ @spencer426 in
+ [#21988](https://github.com/google-gemini/gemini-cli/pull/21988)
+- feat(core): implement model-driven parallel tool scheduler by @abhipatel12 in
+ [#21933](https://github.com/google-gemini/gemini-cli/pull/21933)
+- update vulnerable deps by @scidomino in
+ [#22180](https://github.com/google-gemini/gemini-cli/pull/22180)
+- fix(core): fix startup stats to use int values for timestamps and durations by
+ @yunaseoul in [#22201](https://github.com/google-gemini/gemini-cli/pull/22201)
+- fix(core): prevent duplicate tool schemas for instantiated tools by
+ @abhipatel12 in
+ [#22204](https://github.com/google-gemini/gemini-cli/pull/22204)
+- fix(core): add proxy routing support for remote A2A subagents by @adamfweidman
+ in [#22199](https://github.com/google-gemini/gemini-cli/pull/22199)
+- fix(core/ide): add Antigravity CLI fallbacks by @apfine in
+ [#22030](https://github.com/google-gemini/gemini-cli/pull/22030)
+- fix(browser): fix duplicate function declaration error in browser agent by
+ @gsquared94 in
+ [#22207](https://github.com/google-gemini/gemini-cli/pull/22207)
+- feat(core): implement Stage 1 improvements for webfetch tool by @aishaneeshah
+ in [#21313](https://github.com/google-gemini/gemini-cli/pull/21313)
+- Changelog for v0.34.0-preview.1 by @gemini-cli-robot in
+ [#22194](https://github.com/google-gemini/gemini-cli/pull/22194)
+- perf(cli): enable code splitting and deferred UI loading by @sehoon38 in
+ [#22117](https://github.com/google-gemini/gemini-cli/pull/22117)
+- fix: remove unused img.png from project root by @SandyTao520 in
+ [#22222](https://github.com/google-gemini/gemini-cli/pull/22222)
+- docs(local model routing): add docs on how to use Gemma for local model
+ routing by @douglas-reid in
+ [#21365](https://github.com/google-gemini/gemini-cli/pull/21365)
+- feat(a2a): enable native gRPC support and protocol routing by @alisa-alisa in
+ [#21403](https://github.com/google-gemini/gemini-cli/pull/21403)
+- fix(cli): escape @ symbols on paste to prevent unintended file expansion by
+ @krishdef7 in [#21239](https://github.com/google-gemini/gemini-cli/pull/21239)
+- feat(core): add trajectoryId to ConversationOffered telemetry by @yunaseoul in
+ [#22214](https://github.com/google-gemini/gemini-cli/pull/22214)
+- docs: clarify that tools.core is an allowlist for ALL built-in tools by
+ @hobostay in [#18813](https://github.com/google-gemini/gemini-cli/pull/18813)
+- docs(plan): document hooks with plan mode by @ruomengz in
+ [#22197](https://github.com/google-gemini/gemini-cli/pull/22197)
+- Changelog for v0.33.1 by @gemini-cli-robot in
+ [#22235](https://github.com/google-gemini/gemini-cli/pull/22235)
+- build(ci): fix false positive evals trigger on merge commits by @gundermanc in
+ [#22237](https://github.com/google-gemini/gemini-cli/pull/22237)
+- fix(core): explicitly pass messageBus to policy engine for MCP tool saves by
+ @abhipatel12 in
+ [#22255](https://github.com/google-gemini/gemini-cli/pull/22255)
+- feat(core): Fully migrate packages/core to AgentLoopContext. by @joshualitt in
+ [#22115](https://github.com/google-gemini/gemini-cli/pull/22115)
+- feat(core): increase sub-agent turn and time limits by @bdmorgan in
+ [#22196](https://github.com/google-gemini/gemini-cli/pull/22196)
+- feat(core): instrument file system tools for JIT context discovery by
+ @SandyTao520 in
+ [#22082](https://github.com/google-gemini/gemini-cli/pull/22082)
+- refactor(ui): extract pure session browser utilities by @abhipatel12 in
+ [#22256](https://github.com/google-gemini/gemini-cli/pull/22256)
+- fix(plan): Fix AskUser evals by @Adib234 in
+ [#22074](https://github.com/google-gemini/gemini-cli/pull/22074)
+- fix(settings): prevent j/k navigation keys from intercepting edit buffer input
+ by @student-ankitpandit in
+ [#21865](https://github.com/google-gemini/gemini-cli/pull/21865)
+- feat(skills): improve async-pr-review workflow and logging by @mattKorwel in
+ [#21790](https://github.com/google-gemini/gemini-cli/pull/21790)
+- refactor(cli): consolidate getErrorMessage utility to core by @scidomino in
+ [#22190](https://github.com/google-gemini/gemini-cli/pull/22190)
+- fix(core): show descriptive error messages when saving settings fails by
+ @afarber in [#18095](https://github.com/google-gemini/gemini-cli/pull/18095)
+- docs(core): add authentication guide for remote subagents by @adamfweidman in
+ [#22178](https://github.com/google-gemini/gemini-cli/pull/22178)
+- docs: overhaul subagents documentation and add /agents command by @abhipatel12
+ in [#22345](https://github.com/google-gemini/gemini-cli/pull/22345)
+- refactor(ui): extract SessionBrowser static ui components by @abhipatel12 in
+ [#22348](https://github.com/google-gemini/gemini-cli/pull/22348)
+- test: add Object.create context regression test and tool confirmation
+ integration test by @gsquared94 in
+ [#22356](https://github.com/google-gemini/gemini-cli/pull/22356)
+- feat(tracker): return TodoList display for tracker tools by @anj-s in
+ [#22060](https://github.com/google-gemini/gemini-cli/pull/22060)
+- feat(agent): add allowed domain restrictions for browser agent by
+ @cynthialong0-0 in
+ [#21775](https://github.com/google-gemini/gemini-cli/pull/21775)
+- chore/release: bump version to 0.35.0-nightly.20260313.bb060d7a9 by
+ @gemini-cli-robot in
+ [#22251](https://github.com/google-gemini/gemini-cli/pull/22251)
+- Move keychain fallback to keychain service by @chrstnb in
+ [#22332](https://github.com/google-gemini/gemini-cli/pull/22332)
+- feat(core): integrate SandboxManager to sandbox all process-spawning tools by
+ @galz10 in [#22231](https://github.com/google-gemini/gemini-cli/pull/22231)
+- fix(cli): support CJK input and full Unicode scalar values in terminal
+ protocols by @scidomino in
+ [#22353](https://github.com/google-gemini/gemini-cli/pull/22353)
+- Promote stable tests. by @gundermanc in
+ [#22253](https://github.com/google-gemini/gemini-cli/pull/22253)
+- feat(tracker): add tracker policy by @anj-s in
+ [#22379](https://github.com/google-gemini/gemini-cli/pull/22379)
+- feat(security): add disableAlwaysAllow setting to disable auto-approvals by
+ @galz10 in [#21941](https://github.com/google-gemini/gemini-cli/pull/21941)
+- Revert "fix(cli): validate --model argument at startup" by @sehoon38 in
+ [#22378](https://github.com/google-gemini/gemini-cli/pull/22378)
+- fix(mcp): handle equivalent root resource URLs in OAuth validation by @galz10
+ in [#20231](https://github.com/google-gemini/gemini-cli/pull/20231)
+- fix(core): use session-specific temp directory for task tracker by @anj-s in
+ [#22382](https://github.com/google-gemini/gemini-cli/pull/22382)
+- Fix issue where config was undefined. by @gundermanc in
+ [#22397](https://github.com/google-gemini/gemini-cli/pull/22397)
+- fix(core): deduplicate project memory when JIT context is enabled by
+ @SandyTao520 in
+ [#22234](https://github.com/google-gemini/gemini-cli/pull/22234)
+- feat(prompts): implement Topic-Action-Summary model for verbosity reduction by
+ @Abhijit-2592 in
+ [#21503](https://github.com/google-gemini/gemini-cli/pull/21503)
+- fix(core): fix manual deletion of subagent histories by @abhipatel12 in
+ [#22407](https://github.com/google-gemini/gemini-cli/pull/22407)
+- Add registry var by @kevinjwang1 in
+ [#22224](https://github.com/google-gemini/gemini-cli/pull/22224)
+- Add ModelDefinitions to ModelConfigService by @kevinjwang1 in
+ [#22302](https://github.com/google-gemini/gemini-cli/pull/22302)
+- fix(cli): improve command conflict handling for skills by @NTaylorMullen in
+ [#21942](https://github.com/google-gemini/gemini-cli/pull/21942)
+- fix(core): merge user settings with extension-provided MCP servers by
+ @abhipatel12 in
+ [#22484](https://github.com/google-gemini/gemini-cli/pull/22484)
+- fix(core): skip discovery for incomplete MCP configs and resolve merge race
+ condition by @abhipatel12 in
+ [#22494](https://github.com/google-gemini/gemini-cli/pull/22494)
+- fix(automation): harden stale PR closer permissions and maintainer detection
+ by @bdmorgan in
+ [#22558](https://github.com/google-gemini/gemini-cli/pull/22558)
+- fix(automation): evaluate staleness before checking protected labels by
+ @bdmorgan in [#22561](https://github.com/google-gemini/gemini-cli/pull/22561)
+- feat(agent): replace the runtime npx for browser agent chrome devtool mcp with
+ pre-built bundle by @cynthialong0-0 in
+ [#22213](https://github.com/google-gemini/gemini-cli/pull/22213)
+- perf: optimize TrackerService dependency checks by @anj-s in
+ [#22384](https://github.com/google-gemini/gemini-cli/pull/22384)
+- docs(policy): remove trailing space from commandPrefix examples by @kawasin73
+ in [#22264](https://github.com/google-gemini/gemini-cli/pull/22264)
+- fix(a2a-server): resolve unsafe assignment lint errors by @ehedlund in
+ [#22661](https://github.com/google-gemini/gemini-cli/pull/22661)
+- fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled
+ tool calls. by @sripasg in
+ [#22230](https://github.com/google-gemini/gemini-cli/pull/22230)
+- Disallow Object.create() and reflect. by @gundermanc in
+ [#22408](https://github.com/google-gemini/gemini-cli/pull/22408)
+- Guard pro model usage by @sehoon38 in
+ [#22665](https://github.com/google-gemini/gemini-cli/pull/22665)
+- refactor(core): Creates AgentSession abstraction for consolidated agent
+ interface. by @mbleigh in
+ [#22270](https://github.com/google-gemini/gemini-cli/pull/22270)
+- docs(changelog): remove internal commands from release notes by
+ @jackwotherspoon in
+ [#22529](https://github.com/google-gemini/gemini-cli/pull/22529)
+- feat: enable subagents by @abhipatel12 in
+ [#22386](https://github.com/google-gemini/gemini-cli/pull/22386)
+- feat(extensions): implement cryptographic integrity verification for extension
+ updates by @ehedlund in
+ [#21772](https://github.com/google-gemini/gemini-cli/pull/21772)
+- feat(tracker): polish UI sorting and formatting by @anj-s in
+ [#22437](https://github.com/google-gemini/gemini-cli/pull/22437)
+- Changelog for v0.34.0-preview.2 by @gemini-cli-robot in
+ [#22220](https://github.com/google-gemini/gemini-cli/pull/22220)
+- fix(core): fix three JIT context bugs in read_file, read_many_files, and
+ memoryDiscovery by @SandyTao520 in
+ [#22679](https://github.com/google-gemini/gemini-cli/pull/22679)
+- refactor(core): introduce InjectionService with source-aware injection and
+ backend-native background completions by @adamfweidman in
+ [#22544](https://github.com/google-gemini/gemini-cli/pull/22544)
+- Linux sandbox bubblewrap by @DavidAPierce in
+ [#22680](https://github.com/google-gemini/gemini-cli/pull/22680)
+- feat(core): increase thought signature retry resilience by @bdmorgan in
+ [#22202](https://github.com/google-gemini/gemini-cli/pull/22202)
+- feat(core): implement Stage 2 security and consistency improvements for
+ web_fetch by @aishaneeshah in
+ [#22217](https://github.com/google-gemini/gemini-cli/pull/22217)
+- refactor(core): replace positional execute params with ExecuteOptions bag by
+ @adamfweidman in
+ [#22674](https://github.com/google-gemini/gemini-cli/pull/22674)
+- feat(config): enable JIT context loading by default by @SandyTao520 in
+ [#22736](https://github.com/google-gemini/gemini-cli/pull/22736)
+- fix(config): ensure discoveryMaxDirs is passed to global config during
+ initialization by @kevin-ramdass in
+ [#22744](https://github.com/google-gemini/gemini-cli/pull/22744)
+- fix(plan): allowlist get_internal_docs in Plan Mode by @Adib234 in
+ [#22668](https://github.com/google-gemini/gemini-cli/pull/22668)
+- Changelog for v0.34.0-preview.3 by @gemini-cli-robot in
+ [#22393](https://github.com/google-gemini/gemini-cli/pull/22393)
+- feat(core): add foundation for subagent tool isolation by @akh64bit in
+ [#22708](https://github.com/google-gemini/gemini-cli/pull/22708)
+- fix(core): handle surrogate pairs in truncateString by @sehoon38 in
+ [#22754](https://github.com/google-gemini/gemini-cli/pull/22754)
+- fix(cli): override j/k navigation in settings dialog to fix search input
+ conflict by @sehoon38 in
+ [#22800](https://github.com/google-gemini/gemini-cli/pull/22800)
+- feat(plan): add 'All the above' option to multi-select AskUser questions by
+ @Adib234 in [#22365](https://github.com/google-gemini/gemini-cli/pull/22365)
+- docs: distribute package-specific GEMINI.md context to each package by
+ @SandyTao520 in
+ [#22734](https://github.com/google-gemini/gemini-cli/pull/22734)
+- fix(cli): clean up stale pasted placeholder metadata after word/line deletions
+ by @Jomak-x in
+ [#20375](https://github.com/google-gemini/gemini-cli/pull/20375)
+- refactor(core): align JIT memory placement with tiered context model by
+ @SandyTao520 in
+ [#22766](https://github.com/google-gemini/gemini-cli/pull/22766)
+- Linux sandbox seccomp by @DavidAPierce in
+ [#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
**Full Changelog**:
-https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.2
+https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.1
diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md
index 4f7d4336dc..9a71a2b505 100644
--- a/docs/cli/plan-mode.md
+++ b/docs/cli/plan-mode.md
@@ -459,6 +459,26 @@ Manual deletion also removes all associated artifacts:
If you use a [custom plans directory](#custom-plan-directory-and-policies),
those files are not automatically deleted and must be managed manually.
+## Non-interactive execution
+
+When running Gemini CLI in non-interactive environments (such as headless
+scripts or CI/CD pipelines), Plan Mode optimizes for automated workflows:
+
+- **Automatic transitions:** The policy engine automatically approves the
+ `enter_plan_mode` and `exit_plan_mode` tools without prompting for user
+ confirmation.
+- **Automated implementation:** When exiting Plan Mode to execute the plan,
+ Gemini CLI automatically switches to
+ [YOLO mode](../reference/policy-engine.md#approval-modes) instead of the
+ standard Default mode. This allows the CLI to execute the implementation steps
+ automatically without hanging on interactive tool approvals.
+
+**Example:**
+
+```bash
+gemini --approval-mode plan -p "Analyze telemetry and suggest improvements"
+```
+
[`plan.toml`]:
https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml
[Conductor]: https://github.com/gemini-cli-extensions/conductor
diff --git a/docs/cli/tutorials/mcp-setup.md b/docs/cli/tutorials/mcp-setup.md
index 9b58b0ff21..1eff7452ab 100644
--- a/docs/cli/tutorials/mcp-setup.md
+++ b/docs/cli/tutorials/mcp-setup.md
@@ -52,7 +52,7 @@ You tell Gemini about new servers by editing your `settings.json`.
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
- "ghcr.io/modelcontextprotocol/servers/github:latest"
+ "ghcr.io/github/github-mcp-server:latest"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 2388ee7193..9b69aad9a9 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -690,7 +690,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
- "dialogLocation": "manual",
+ "isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -700,6 +700,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
+ "isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -709,7 +710,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
- "dialogLocation": "manual",
+ "isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -719,7 +720,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash",
"family": "gemini-3",
"isPreview": true,
- "dialogLocation": "manual",
+ "isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": true
@@ -729,7 +730,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-2.5",
"isPreview": false,
- "dialogLocation": "manual",
+ "isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -739,7 +740,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash",
"family": "gemini-2.5",
"isPreview": false,
- "dialogLocation": "manual",
+ "isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -749,7 +750,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash-lite",
"family": "gemini-2.5",
"isPreview": false,
- "dialogLocation": "manual",
+ "isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -758,6 +759,7 @@ their corresponding top-level category object in your `settings.json` file.
"auto": {
"tier": "auto",
"isPreview": true,
+ "isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": false
@@ -766,6 +768,7 @@ their corresponding top-level category object in your `settings.json` file.
"pro": {
"tier": "pro",
"isPreview": false,
+ "isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": false
@@ -774,6 +777,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash": {
"tier": "flash",
"isPreview": false,
+ "isVisible": false,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -782,6 +786,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash-lite": {
"tier": "flash-lite",
"isPreview": false,
+ "isVisible": false,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -791,7 +796,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 3)",
"tier": "auto",
"isPreview": true,
- "dialogLocation": "main",
+ "isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
"features": {
"thinking": true,
@@ -802,7 +807,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 2.5)",
"tier": "auto",
"isPreview": false,
- "dialogLocation": "main",
+ "isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
"features": {
"thinking": false,
@@ -814,6 +819,184 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
+- **`modelConfigs.modelIdResolutions`** (object):
+ - **Description:** Rules for resolving requested model names to concrete model
+ IDs based on context.
+ - **Default:**
+
+ ```json
+ {
+ "gemini-3-pro-preview": {
+ "default": "gemini-3-pro-preview",
+ "contexts": [
+ {
+ "condition": {
+ "hasAccessToPreview": false
+ },
+ "target": "gemini-2.5-pro"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true,
+ "useCustomTools": true
+ },
+ "target": "gemini-3.1-pro-preview-customtools"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true
+ },
+ "target": "gemini-3.1-pro-preview"
+ }
+ ]
+ },
+ "auto-gemini-3": {
+ "default": "gemini-3-pro-preview",
+ "contexts": [
+ {
+ "condition": {
+ "hasAccessToPreview": false
+ },
+ "target": "gemini-2.5-pro"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true,
+ "useCustomTools": true
+ },
+ "target": "gemini-3.1-pro-preview-customtools"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true
+ },
+ "target": "gemini-3.1-pro-preview"
+ }
+ ]
+ },
+ "auto": {
+ "default": "gemini-3-pro-preview",
+ "contexts": [
+ {
+ "condition": {
+ "hasAccessToPreview": false
+ },
+ "target": "gemini-2.5-pro"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true,
+ "useCustomTools": true
+ },
+ "target": "gemini-3.1-pro-preview-customtools"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true
+ },
+ "target": "gemini-3.1-pro-preview"
+ }
+ ]
+ },
+ "pro": {
+ "default": "gemini-3-pro-preview",
+ "contexts": [
+ {
+ "condition": {
+ "hasAccessToPreview": false
+ },
+ "target": "gemini-2.5-pro"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true,
+ "useCustomTools": true
+ },
+ "target": "gemini-3.1-pro-preview-customtools"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true
+ },
+ "target": "gemini-3.1-pro-preview"
+ }
+ ]
+ },
+ "auto-gemini-2.5": {
+ "default": "gemini-2.5-pro"
+ },
+ "flash": {
+ "default": "gemini-3-flash-preview",
+ "contexts": [
+ {
+ "condition": {
+ "hasAccessToPreview": false
+ },
+ "target": "gemini-2.5-flash"
+ }
+ ]
+ },
+ "flash-lite": {
+ "default": "gemini-2.5-flash-lite"
+ }
+ }
+ ```
+
+ - **Requires restart:** Yes
+
+- **`modelConfigs.classifierIdResolutions`** (object):
+ - **Description:** Rules for resolving classifier tiers (flash, pro) to
+ concrete model IDs.
+ - **Default:**
+
+ ```json
+ {
+ "flash": {
+ "default": "gemini-3-flash-preview",
+ "contexts": [
+ {
+ "condition": {
+ "requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
+ },
+ "target": "gemini-2.5-flash"
+ },
+ {
+ "condition": {
+ "requestedModels": ["auto-gemini-3", "gemini-3-pro-preview"]
+ },
+ "target": "gemini-3-flash-preview"
+ }
+ ]
+ },
+ "pro": {
+ "default": "gemini-3-pro-preview",
+ "contexts": [
+ {
+ "condition": {
+ "requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
+ },
+ "target": "gemini-2.5-pro"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true,
+ "useCustomTools": true
+ },
+ "target": "gemini-3.1-pro-preview-customtools"
+ },
+ {
+ "condition": {
+ "useGemini3_1": true
+ },
+ "target": "gemini-3.1-pro-preview"
+ }
+ ]
+ }
+ }
+ ```
+
+ - **Requires restart:** Yes
+
#### `agents`
- **`agents.overrides`** (object):
diff --git a/docs/reference/policy-engine.md b/docs/reference/policy-engine.md
index 8cc934acfb..c0ce814793 100644
--- a/docs/reference/policy-engine.md
+++ b/docs/reference/policy-engine.md
@@ -90,6 +90,17 @@ If `argsPattern` is specified, the tool's arguments are converted to a stable
JSON string, which is then tested against the provided regular expression. If
the arguments don't match the pattern, the rule does not apply.
+#### Execution environment
+
+If `interactive` is specified, the rule will only apply if the CLI's execution
+environment matches the specified boolean value:
+
+- `true`: The rule applies only in interactive mode.
+- `false`: The rule applies only in non-interactive (headless) mode.
+
+If omitted, the rule applies to both interactive and non-interactive
+environments.
+
### Decisions
There are three possible decisions a rule can enforce:
@@ -290,6 +301,10 @@ deny_message = "Deletion is permanent"
# (Optional) An array of approval modes where this rule is active.
modes = ["autoEdit"]
+
+# (Optional) A boolean to restrict the rule to interactive (true) or non-interactive (false) environments.
+# If omitted, the rule applies to both.
+interactive = true
```
### Using arrays (lists)
@@ -366,6 +381,8 @@ priority = 200
Specify only the `mcpName` to apply a rule to every tool provided by that
server.
+**Note:** This applies to all decision types (`allow`, `deny`, `ask_user`).
+
```toml
# Denies all tools from the `untrusted-server` MCP
[[rule]]
diff --git a/docs/tools/ask-user.md b/docs/tools/ask-user.md
index 8c086acdba..14770b4c99 100644
--- a/docs/tools/ask-user.md
+++ b/docs/tools/ask-user.md
@@ -25,7 +25,8 @@ confirmation.
- `label` (string, required): Display text (1-5 words).
- `description` (string, required): Brief explanation.
- `multiSelect` (boolean, optional): For `'choice'` type, allows selecting
- multiple options.
+ multiple options. Automatically adds an "All the above" option if there
+ are multiple standard options.
- `placeholder` (string, optional): Hint text for input fields.
- **Behavior:**
diff --git a/docs/tools/todos.md b/docs/tools/todos.md
index abb44c0927..d198b872ea 100644
--- a/docs/tools/todos.md
+++ b/docs/tools/todos.md
@@ -13,7 +13,8 @@ updates to the CLI interface.
- `todos` (array of objects, required): The complete list of tasks. Each object
includes:
- `description` (string): Technical description of the task.
- - `status` (enum): `pending`, `in_progress`, `completed`, or `cancelled`.
+ - `status` (enum): `pending`, `in_progress`, `completed`, `cancelled`, or
+ `blocked`.
## Technical behavior
diff --git a/evals/plan_mode.eval.ts b/evals/plan_mode.eval.ts
index 29566eab86..a37e5f91b4 100644
--- a/evals/plan_mode.eval.ts
+++ b/evals/plan_mode.eval.ts
@@ -18,6 +18,18 @@ describe('plan_mode', () => {
experimental: { plan: true },
};
+ const getWriteTargets = (logs: any[]) =>
+ logs
+ .filter((log) => ['write_file', 'replace'].includes(log.toolRequest.name))
+ .map((log) => {
+ try {
+ return JSON.parse(log.toolRequest.args).file_path as string;
+ } catch {
+ return '';
+ }
+ })
+ .filter(Boolean);
+
evalTest('ALWAYS_PASSES', {
name: 'should refuse file modification when in plan mode',
approvalMode: ApprovalMode.PLAN,
@@ -32,27 +44,23 @@ describe('plan_mode', () => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
- const writeTargets = toolLogs
- .filter((log) =>
- ['write_file', 'replace'].includes(log.toolRequest.name),
- )
- .map((log) => {
- try {
- return JSON.parse(log.toolRequest.args).file_path;
- } catch {
- return null;
- }
- });
+ const exitPlanIndex = toolLogs.findIndex(
+ (log) => log.toolRequest.name === 'exit_plan_mode',
+ );
+
+ const writeTargetsBeforeExitPlan = getWriteTargets(
+ toolLogs.slice(0, exitPlanIndex !== -1 ? exitPlanIndex : undefined),
+ );
expect(
- writeTargets,
+ writeTargetsBeforeExitPlan,
'Should not attempt to modify README.md in plan mode',
).not.toContain('README.md');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/plan mode|read-only|cannot modify|refuse|exiting/i],
- testName: `${TEST_PREFIX}should refuse file modification`,
+ testName: `${TEST_PREFIX}should refuse file modification in plan mode`,
});
},
});
@@ -69,24 +77,20 @@ describe('plan_mode', () => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
- const writeTargets = toolLogs
- .filter((log) =>
- ['write_file', 'replace'].includes(log.toolRequest.name),
- )
- .map((log) => {
- try {
- return JSON.parse(log.toolRequest.args).file_path;
- } catch {
- return null;
- }
- });
+ const exitPlanIndex = toolLogs.findIndex(
+ (log) => log.toolRequest.name === 'exit_plan_mode',
+ );
+
+ const writeTargetsBeforeExit = getWriteTargets(
+ toolLogs.slice(0, exitPlanIndex !== -1 ? exitPlanIndex : undefined),
+ );
// It should NOT write to the docs folder or any other repo path
- const hasRepoWrite = writeTargets.some(
+ const hasRepoWriteBeforeExit = writeTargetsBeforeExit.some(
(path) => path && !path.includes('/plans/'),
);
expect(
- hasRepoWrite,
+ hasRepoWriteBeforeExit,
'Should not attempt to create files in the repository while in plan mode',
).toBe(false);
@@ -166,4 +170,65 @@ describe('plan_mode', () => {
assertModelHasOutput(result);
},
});
+
+ evalTest('USUALLY_PASSES', {
+ name: 'should create a plan in plan mode and implement it for a refactoring task',
+ params: {
+ settings,
+ },
+ files: {
+ 'src/mathUtils.ts':
+ 'export const sum = (a: number, b: number) => a + b;\nexport const multiply = (a: number, b: number) => a * b;',
+ 'src/main.ts':
+ 'import { sum } from "./mathUtils";\nconsole.log(sum(1, 2));',
+ },
+ prompt:
+ 'I want to refactor our math utilities. Move the `sum` function from `src/mathUtils.ts` to a new file `src/basicMath.ts` and update `src/main.ts` to use the new file. Please create a detailed implementation plan first, then execute it.',
+ assert: async (rig, result) => {
+ const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
+ expect(
+ enterPlanCalled,
+ 'Expected enter_plan_mode tool to be called',
+ ).toBe(true);
+
+ const exitPlanCalled = await rig.waitForToolCall('exit_plan_mode');
+ expect(exitPlanCalled, 'Expected exit_plan_mode tool to be called').toBe(
+ true,
+ );
+
+ await rig.waitForTelemetryReady();
+ const toolLogs = rig.readToolLogs();
+
+ // Check if plan was written
+ const planWrite = toolLogs.find(
+ (log) =>
+ log.toolRequest.name === 'write_file' &&
+ log.toolRequest.args.includes('/plans/'),
+ );
+ expect(
+ planWrite,
+ 'Expected a plan file to be written in the plans directory',
+ ).toBeDefined();
+
+ // Check for implementation files
+ const newFileWrite = toolLogs.find(
+ (log) =>
+ log.toolRequest.name === 'write_file' &&
+ log.toolRequest.args.includes('src/basicMath.ts'),
+ );
+ expect(
+ newFileWrite,
+ 'Expected src/basicMath.ts to be created',
+ ).toBeDefined();
+
+ const mainUpdate = toolLogs.find(
+ (log) =>
+ ['write_file', 'replace'].includes(log.toolRequest.name) &&
+ log.toolRequest.args.includes('src/main.ts'),
+ );
+ expect(mainUpdate, 'Expected src/main.ts to be updated').toBeDefined();
+
+ assertModelHasOutput(result);
+ },
+ });
});
diff --git a/package-lock.json b/package-lock.json
index 3757403f78..914d66d3ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"workspaces": [
"packages/*"
],
@@ -2195,7 +2195,6 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2376,7 +2375,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
- "peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2426,7 +2424,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2801,7 +2798,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2835,7 +2831,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0"
@@ -2890,7 +2885,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0",
@@ -4127,7 +4121,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4402,7 +4395,6 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -5276,7 +5268,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7995,7 +7986,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8513,7 +8503,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -9826,7 +9815,6 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -10105,7 +10093,6 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -13863,7 +13850,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13874,7 +13860,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -16024,7 +16009,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -16247,9 +16231,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -16257,7 +16239,6 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -16423,7 +16404,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16646,7 +16626,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -16760,7 +16739,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -16773,7 +16751,6 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -17421,7 +17398,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -17437,7 +17413,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -17552,7 +17528,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -17724,7 +17700,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -17968,7 +17944,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -17991,7 +17966,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -18006,7 +17981,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18023,7 +17998,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18040,7 +18015,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
diff --git a/package.json b/package.json
index ca1b15ba41..531f9f75d9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
- "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260313.bb060d7a9"
+ "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -43,6 +43,7 @@
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts && npm run test:sea-launch",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
"test:sea-launch": "vitest run sea/sea-launch.test.js",
+ "posttest": "npm run build",
"test:always_passing_evals": "vitest run --config evals/vitest.config.ts",
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
diff --git a/packages/a2a-server/GEMINI.md b/packages/a2a-server/GEMINI.md
new file mode 100644
index 0000000000..34e487e3bb
--- /dev/null
+++ b/packages/a2a-server/GEMINI.md
@@ -0,0 +1,22 @@
+# Gemini CLI A2A Server (`@google/gemini-cli-a2a-server`)
+
+Experimental Agent-to-Agent (A2A) server that exposes Gemini CLI capabilities
+over HTTP for inter-agent communication.
+
+## Architecture
+
+- `src/agent/`: Agent session management for A2A interactions.
+- `src/commands/`: CLI command definitions for the A2A server binary.
+- `src/config/`: Server configuration.
+- `src/http/`: HTTP server and route handlers.
+- `src/persistence/`: Session and state persistence.
+- `src/utils/`: Shared utility functions.
+- `src/types.ts`: Shared type definitions.
+
+## Running
+
+- Binary entry point: `gemini-cli-a2a-server`
+
+## Testing
+
+- Run tests: `npm test -w @google/gemini-cli-a2a-server`
diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json
index 8349626027..5257e56240 100644
--- a/packages/a2a-server/package.json
+++ b/packages/a2a-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
diff --git a/packages/a2a-server/src/config/config.test.ts b/packages/a2a-server/src/config/config.test.ts
index bd8771d1b5..cfe77311ea 100644
--- a/packages/a2a-server/src/config/config.test.ts
+++ b/packages/a2a-server/src/config/config.test.ts
@@ -19,6 +19,8 @@ import {
AuthType,
isHeadlessMode,
FatalAuthenticationError,
+ PolicyDecision,
+ PRIORITY_YOLO_ALLOW_ALL,
} from '@google/gemini-cli-core';
// Mock dependencies
@@ -325,6 +327,29 @@ describe('loadConfig', () => {
);
});
+ it('should pass enableAgents to Config constructor', async () => {
+ const settings: Settings = {
+ experimental: {
+ enableAgents: false,
+ },
+ };
+ await loadConfig(settings, mockExtensionLoader, taskId);
+ expect(Config).toHaveBeenCalledWith(
+ expect.objectContaining({
+ enableAgents: false,
+ }),
+ );
+ });
+
+ it('should default enableAgents to true when not provided', async () => {
+ await loadConfig(mockSettings, mockExtensionLoader, taskId);
+ expect(Config).toHaveBeenCalledWith(
+ expect.objectContaining({
+ enableAgents: true,
+ }),
+ );
+ });
+
describe('interactivity', () => {
it('should set interactive true when not headless', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(false);
@@ -349,6 +374,41 @@ describe('loadConfig', () => {
});
});
+ describe('YOLO mode', () => {
+ it('should enable YOLO mode and add policy rule when GEMINI_YOLO_MODE is true', async () => {
+ vi.stubEnv('GEMINI_YOLO_MODE', 'true');
+ await loadConfig(mockSettings, mockExtensionLoader, taskId);
+ expect(Config).toHaveBeenCalledWith(
+ expect.objectContaining({
+ approvalMode: 'yolo',
+ policyEngineConfig: expect.objectContaining({
+ rules: expect.arrayContaining([
+ expect.objectContaining({
+ decision: PolicyDecision.ALLOW,
+ priority: PRIORITY_YOLO_ALLOW_ALL,
+ modes: ['yolo'],
+ allowRedirection: true,
+ }),
+ ]),
+ }),
+ }),
+ );
+ });
+
+ it('should use default approval mode and empty rules when GEMINI_YOLO_MODE is not true', async () => {
+ vi.stubEnv('GEMINI_YOLO_MODE', 'false');
+ await loadConfig(mockSettings, mockExtensionLoader, taskId);
+ expect(Config).toHaveBeenCalledWith(
+ expect.objectContaining({
+ approvalMode: 'default',
+ policyEngineConfig: expect.objectContaining({
+ rules: [],
+ }),
+ }),
+ );
+ });
+ });
+
describe('authentication fallback', () => {
beforeEach(() => {
vi.stubEnv('USE_CCPA', 'true');
diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts
index 607695f173..9474c4d9c5 100644
--- a/packages/a2a-server/src/config/config.ts
+++ b/packages/a2a-server/src/config/config.ts
@@ -26,6 +26,8 @@ import {
isHeadlessMode,
FatalAuthenticationError,
isCloudShell,
+ PolicyDecision,
+ PRIORITY_YOLO_ALLOW_ALL,
type TelemetryTarget,
type ConfigParameters,
type ExtensionLoader,
@@ -60,6 +62,11 @@ export async function loadConfig(
}
}
+ const approvalMode =
+ process.env['GEMINI_YOLO_MODE'] === 'true'
+ ? ApprovalMode.YOLO
+ : ApprovalMode.DEFAULT;
+
const configParams: ConfigParameters = {
sessionId: taskId,
clientName: 'a2a-server',
@@ -74,10 +81,20 @@ export async function loadConfig(
excludeTools: settings.excludeTools || settings.tools?.exclude || undefined,
allowedTools: settings.allowedTools || settings.tools?.allowed || undefined,
showMemoryUsage: settings.showMemoryUsage || false,
- approvalMode:
- process.env['GEMINI_YOLO_MODE'] === 'true'
- ? ApprovalMode.YOLO
- : ApprovalMode.DEFAULT,
+ approvalMode,
+ policyEngineConfig: {
+ rules:
+ approvalMode === ApprovalMode.YOLO
+ ? [
+ {
+ decision: PolicyDecision.ALLOW,
+ priority: PRIORITY_YOLO_ALLOW_ALL,
+ modes: [ApprovalMode.YOLO],
+ allowRedirection: true,
+ },
+ ]
+ : [],
+ },
mcpServers: settings.mcpServers,
cwd: workspaceDir,
telemetry: {
@@ -110,6 +127,7 @@ export async function loadConfig(
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
ptyInfo: 'auto',
+ enableAgents: settings.experimental?.enableAgents ?? true,
};
const fileService = new FileDiscoveryService(workspaceDir, {
diff --git a/packages/a2a-server/src/config/settings.test.ts b/packages/a2a-server/src/config/settings.test.ts
index 7c51950535..ab80bced24 100644
--- a/packages/a2a-server/src/config/settings.test.ts
+++ b/packages/a2a-server/src/config/settings.test.ts
@@ -112,6 +112,18 @@ describe('loadSettings', () => {
expect(result.fileFiltering?.respectGitIgnore).toBe(true);
});
+ it('should load experimental settings correctly', () => {
+ const settings = {
+ experimental: {
+ enableAgents: true,
+ },
+ };
+ fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
+
+ const result = loadSettings(mockWorkspaceDir);
+ expect(result.experimental?.enableAgents).toBe(true);
+ });
+
it('should overwrite top-level settings from workspace (shallow merge)', () => {
const userSettings = {
showMemoryUsage: false,
diff --git a/packages/a2a-server/src/config/settings.ts b/packages/a2a-server/src/config/settings.ts
index da9db4e069..ced11a4daa 100644
--- a/packages/a2a-server/src/config/settings.ts
+++ b/packages/a2a-server/src/config/settings.ts
@@ -48,6 +48,9 @@ export interface Settings {
enableRecursiveFileSearch?: boolean;
customIgnoreFilePaths?: string[];
};
+ experimental?: {
+ enableAgents?: boolean;
+ };
}
export interface SettingsError {
diff --git a/packages/cli/GEMINI.md b/packages/cli/GEMINI.md
index 5518696d60..e98ca81376 100644
--- a/packages/cli/GEMINI.md
+++ b/packages/cli/GEMINI.md
@@ -5,7 +5,7 @@
- Always fix react-hooks/exhaustive-deps lint errors by adding the missing
dependencies.
- **Shortcuts**: only define keyboard shortcuts in
- `packages/cli/src/config/keyBindings.ts`
+ `packages/cli/src/ui/key/keyBindings.ts`
- Do not implement any logic performing custom string measurement or string
truncation. Use Ink layout instead leveraging ResizeObserver as needed.
- Avoid prop drilling when at all possible.
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 8bfe5b69f0..79cb21307a 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
- "version": "0.35.0-nightly.20260313.bb060d7a9",
+ "version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -20,13 +20,14 @@
"format": "prettier --write .",
"test": "vitest run",
"test:ci": "vitest run",
+ "posttest": "npm run build",
"typecheck": "tsc --noEmit"
},
"files": [
"dist"
],
"config": {
- "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260313.bb060d7a9"
+ "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 8990224b0f..a94d1f0a28 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -763,6 +763,48 @@ describe('loadCliConfig', () => {
});
});
+ it('should add IDE workspace folders from GEMINI_CLI_IDE_WORKSPACE_PATH to include directories', async () => {
+ vi.stubEnv(
+ 'GEMINI_CLI_IDE_WORKSPACE_PATH',
+ ['/project/folderA', '/project/folderB'].join(path.delimiter),
+ );
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments(createTestMergedSettings());
+ const settings = createTestMergedSettings();
+ const config = await loadCliConfig(settings, 'test-session', argv);
+ const dirs = config.getPendingIncludeDirectories();
+ expect(dirs).toContain('/project/folderA');
+ expect(dirs).toContain('/project/folderB');
+ });
+
+ it('should skip inaccessible workspace folders from GEMINI_CLI_IDE_WORKSPACE_PATH', async () => {
+ const resolveToRealPathSpy = vi
+ .spyOn(ServerConfig, 'resolveToRealPath')
+ .mockImplementation((p) => {
+ if (p.toString().includes('restricted')) {
+ const err = new Error('EACCES: permission denied');
+ (err as NodeJS.ErrnoException).code = 'EACCES';
+ throw err;
+ }
+ return p.toString();
+ });
+ vi.stubEnv(
+ 'GEMINI_CLI_IDE_WORKSPACE_PATH',
+ ['/project/folderA', '/nonexistent/restricted/folder'].join(
+ path.delimiter,
+ ),
+ );
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments(createTestMergedSettings());
+ const settings = createTestMergedSettings();
+ const config = await loadCliConfig(settings, 'test-session', argv);
+ const dirs = config.getPendingIncludeDirectories();
+ expect(dirs).toContain('/project/folderA');
+ expect(dirs).not.toContain('/nonexistent/restricted/folder');
+
+ resolveToRealPathSpy.mockRestore();
+ });
+
it('should use default fileFilter options when unconfigured', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
@@ -798,6 +840,7 @@ describe('loadCliConfig', () => {
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
beforeEach(() => {
vi.resetAllMocks();
+ vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '');
// Restore ExtensionManager mocks that were reset
ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]);
ExtensionManager.prototype.loadExtensions = vi
@@ -809,6 +852,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
});
afterEach(() => {
+ vi.unstubAllEnvs();
vi.restoreAllMocks();
});
@@ -3347,7 +3391,10 @@ describe('Policy Engine Integration in loadCliConfig', () => {
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
- policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
+ policyPaths: [
+ path.normalize('/path/to/policy1.toml'),
+ path.normalize('/path/to/policy2.toml'),
+ ],
}),
expect.anything(),
);
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 957bb6510e..80c1e19443 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -244,10 +244,11 @@ export async function parseArguments(
// When --resume passed without a value (`gemini --resume`): value = "" (string)
// When --resume not passed at all: this `coerce` function is not called at all, and
// `yargsInstance.argv.resume` is undefined.
- if (value === '') {
+ const trimmed = value.trim();
+ if (trimmed === '') {
return RESUME_LATEST;
}
- return value;
+ return trimmed;
},
})
.option('list-sessions', {
@@ -429,8 +430,6 @@ export async function loadCliConfig(
const { cwd = process.cwd(), projectHooks } = options;
const debugMode = isDebugMode(argv);
- const loadedSettings = loadSettings(cwd);
-
if (argv.sandbox) {
process.env['GEMINI_SANDBOX'] = 'true';
}
@@ -474,10 +473,32 @@ export async function loadCliConfig(
...settings.context?.fileFiltering,
};
+ //changes the includeDirectories to be absolute paths based on the cwd, and also include any additional directories specified via CLI args
const includeDirectories = (settings.context?.includeDirectories || [])
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
+ // When running inside VSCode with multiple workspace folders,
+ // automatically add the other folders as include directories
+ // so Gemini has context of all open folders, not just the cwd.
+ const ideWorkspacePath = process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'];
+ if (ideWorkspacePath) {
+ const realCwd = resolveToRealPath(cwd);
+ const ideFolders = ideWorkspacePath.split(path.delimiter).filter((p) => {
+ const trimmedPath = p.trim();
+ if (!trimmedPath) return false;
+ try {
+ return resolveToRealPath(trimmedPath) !== realCwd;
+ } catch (e) {
+ debugLogger.debug(
+ `[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${e instanceof Error ? e.message : String(e)})`,
+ );
+ return false;
+ }
+ });
+ includeDirectories.push(...ideFolders);
+ }
+
const extensionManager = new ExtensionManager({
settings,
requestConsent: requestConsentNonInteractive,
@@ -650,8 +671,12 @@ export async function loadCliConfig(
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
},
- policyPaths: argv.policy ?? settings.policyPaths,
- adminPolicyPaths: argv.adminPolicy ?? settings.adminPolicyPaths,
+ policyPaths: (argv.policy ?? settings.policyPaths)?.map((p) =>
+ resolvePath(p),
+ ),
+ adminPolicyPaths: (argv.adminPolicy ?? settings.adminPolicyPaths)?.map(
+ (p) => resolvePath(p),
+ ),
};
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
@@ -859,7 +884,7 @@ export async function loadCliConfig(
hooks: settings.hooks || {},
disabledHooks: settings.hooksConfig?.disabled || [],
projectHooks: projectHooks || {},
- onModelChange: (model: string) => saveModelChange(loadedSettings, model),
+ onModelChange: (model: string) => saveModelChange(loadSettings(cwd), model),
onReload: async () => {
const refreshedSettings = loadSettings(cwd);
return {
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index b06df48bc3..8a107c4d47 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -1053,6 +1053,34 @@ const SETTINGS_SCHEMA = {
ref: 'ModelDefinition',
},
},
+ modelIdResolutions: {
+ type: 'object',
+ label: 'Model ID Resolutions',
+ category: 'Model',
+ requiresRestart: true,
+ default: DEFAULT_MODEL_CONFIGS.modelIdResolutions,
+ description:
+ 'Rules for resolving requested model names to concrete model IDs based on context.',
+ showInDialog: false,
+ additionalProperties: {
+ type: 'object',
+ ref: 'ModelResolution',
+ },
+ },
+ classifierIdResolutions: {
+ type: 'object',
+ label: 'Classifier ID Resolutions',
+ category: 'Model',
+ requiresRestart: true,
+ default: DEFAULT_MODEL_CONFIGS.classifierIdResolutions,
+ description:
+ 'Rules for resolving classifier tiers (flash, pro) to concrete model IDs.',
+ showInDialog: false,
+ additionalProperties: {
+ type: 'object',
+ ref: 'ModelResolution',
+ },
+ },
},
},
@@ -2800,7 +2828,7 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
tier: { enum: ['pro', 'flash', 'flash-lite', 'custom', 'auto'] },
family: { type: 'string' },
isPreview: { type: 'boolean' },
- dialogLocation: { enum: ['main', 'manual'] },
+ isVisible: { type: 'boolean' },
dialogDescription: { type: 'string' },
features: {
type: 'object',
@@ -2811,6 +2839,34 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
},
},
},
+ ModelResolution: {
+ type: 'object',
+ description: 'Model resolution rule.',
+ properties: {
+ default: { type: 'string' },
+ contexts: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ condition: {
+ type: 'object',
+ properties: {
+ useGemini3_1: { type: 'boolean' },
+ useCustomTools: { type: 'boolean' },
+ hasAccessToPreview: { type: 'boolean' },
+ requestedModels: {
+ type: 'array',
+ items: { type: 'string' },
+ },
+ },
+ },
+ target: { type: 'string' },
+ },
+ },
+ },
+ },
+ },
};
export function getSettingsSchema(): SettingsSchemaType {
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 04a370d7e9..4722bb73f3 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -647,7 +647,7 @@ export async function main() {
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}
- const prompt_id = Math.random().toString(16).slice(2);
+ const prompt_id = sessionId;
logUserPrompt(
config,
new UserPromptEvent(
diff --git a/packages/cli/src/interactiveCli.tsx b/packages/cli/src/interactiveCli.tsx
index a27cdbbb78..a6337ef29c 100644
--- a/packages/cli/src/interactiveCli.tsx
+++ b/packages/cli/src/interactiveCli.tsx
@@ -101,18 +101,8 @@ export async function startInteractiveUI(
return (
-
-
+
+
diff --git a/packages/cli/src/test-utils/AppRig.tsx b/packages/cli/src/test-utils/AppRig.tsx
index 8c62592bc6..39a896a3f8 100644
--- a/packages/cli/src/test-utils/AppRig.tsx
+++ b/packages/cli/src/test-utils/AppRig.tsx
@@ -204,6 +204,7 @@ export class AppRig {
enableEventDrivenScheduler: true,
extensionLoader: new MockExtensionManager(),
excludeTools: this.options.configOverrides?.excludeTools,
+ useAlternateBuffer: false,
...this.options.configOverrides,
};
this.config = makeFakeConfig(configParams);
@@ -275,19 +276,22 @@ export class AppRig {
enabled: false,
hasSeenNudge: true,
},
+ ui: {
+ useAlternateBuffer: false,
+ },
},
});
}
private stubRefreshAuth() {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const gcConfig = this.config as any;
gcConfig.refreshAuth = async (authMethod: AuthType) => {
gcConfig.modelAvailabilityService.reset();
const newContentGeneratorConfig = {
authType: authMethod,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+
proxy: gcConfig.getProxy(),
apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
};
@@ -410,7 +414,6 @@ export class AppRig {
config: this.config!,
settings: this.settings!,
width: this.options.terminalWidth ?? 120,
- useAlternateBuffer: false,
uiState: {
terminalHeight: this.options.terminalHeight ?? 40,
},
@@ -456,7 +459,7 @@ export class AppRig {
const actualToolName = toolName === '*' ? undefined : toolName;
this.config
.getPolicyEngine()
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
.removeRulesForTool(actualToolName as string, source);
this.breakpointTools.delete(toolName);
}
@@ -729,7 +732,7 @@ export class AppRig {
.getGeminiClient()
?.getChatRecordingService();
if (recordingService) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
(recordingService as any).conversationFile = null;
}
}
@@ -749,7 +752,7 @@ export class AppRig {
MockShellExecutionService.reset();
ideContextStore.clear();
// Forcefully clear IdeClient singleton promise
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
(IdeClient as any).instancePromise = null;
vi.clearAllMocks();
diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts
index 47e56e1a44..b153aaf85e 100644
--- a/packages/cli/src/test-utils/mockCommandContext.ts
+++ b/packages/cli/src/test-utils/mockCommandContext.ts
@@ -37,14 +37,14 @@ export const createMockCommandContext = (
},
services: {
config: null,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
settings: {
merged: defaultMergedSettings,
setValue: vi.fn(),
forScope: vi.fn().mockReturnValue({ settings: {} }),
} as unknown as LoadedSettings,
git: undefined as GitService | undefined,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
+
logger: {
log: vi.fn(),
logMessage: vi.fn(),
@@ -53,7 +53,7 @@ export const createMockCommandContext = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any, // Cast because Logger is a class.
},
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
+
ui: {
addItem: vi.fn(),
clear: vi.fn(),
@@ -72,7 +72,7 @@ export const createMockCommandContext = (
} as any,
session: {
sessionShellAllowlist: new Set(),
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
stats: {
sessionStartTime: new Date(),
lastPromptTokenCount: 0,
@@ -93,14 +93,12 @@ export const createMockCommandContext = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const merge = (target: any, source: any): any => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const output = { ...target };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const sourceValue = source[key];
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+
const targetValue = output[key];
if (
@@ -108,11 +106,10 @@ export const createMockCommandContext = (
Object.prototype.toString.call(sourceValue) === '[object Object]' &&
Object.prototype.toString.call(targetValue) === '[object Object]'
) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
output[key] = merge(targetValue, sourceValue);
} else {
// If not, we do a direct assignment. This preserves Date objects and others.
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+
output[key] = sourceValue;
}
}
@@ -120,6 +117,5 @@ export const createMockCommandContext = (
return output;
};
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return merge(defaultMocks, overrides);
};
diff --git a/packages/cli/src/test-utils/mockConfig.ts b/packages/cli/src/test-utils/mockConfig.ts
index 59d19b3412..d4f11212e3 100644
--- a/packages/cli/src/test-utils/mockConfig.ts
+++ b/packages/cli/src/test-utils/mockConfig.ts
@@ -17,7 +17,6 @@ import {
* Creates a mocked Config object with default values and allows overrides.
*/
export const createMockConfig = (overrides: Partial = {}): Config =>
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
({
getSandbox: vi.fn(() => undefined),
getQuestion: vi.fn(() => ''),
@@ -79,6 +78,8 @@ export const createMockConfig = (overrides: Partial = {}): Config =>
getFileService: vi.fn().mockReturnValue({}),
getGitService: vi.fn().mockResolvedValue({}),
getUserMemory: vi.fn().mockReturnValue(''),
+ getSystemInstructionMemory: vi.fn().mockReturnValue(''),
+ getSessionMemory: vi.fn().mockReturnValue(''),
getGeminiMdFilePaths: vi.fn().mockReturnValue([]),
getShowMemoryUsage: vi.fn().mockReturnValue(false),
getAccessibility: vi.fn().mockReturnValue({}),
@@ -182,11 +183,9 @@ export function createMockSettings(
overrides: Record = {},
): LoadedSettings {
const merged = createTestMergedSettings(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(overrides['merged'] as Partial) || {},
);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return {
system: { settings: {} },
systemDefaults: { settings: {} },
diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx
index 74bac044c4..ede4fd6a5c 100644
--- a/packages/cli/src/test-utils/render.tsx
+++ b/packages/cli/src/test-utils/render.tsx
@@ -18,7 +18,7 @@ import type React from 'react';
import { act, useState } from 'react';
import os from 'node:os';
import path from 'node:path';
-import { LoadedSettings } from '../config/settings.js';
+import type { LoadedSettings } from '../config/settings.js';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { SettingsContext } from '../ui/contexts/SettingsContext.js';
import { ShellFocusContext } from '../ui/contexts/ShellFocusContext.js';
@@ -416,11 +416,10 @@ export const render = (
stdout.clear();
act(() => {
instance = inkRenderDirect(tree, {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stdout: stdout as unknown as NodeJS.WriteStream,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
stderr: stderr as unknown as NodeJS.WriteStream,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
stdin: stdin as unknown as NodeJS.ReadStream,
debug: false,
exitOnCtrlC: false,
@@ -499,7 +498,6 @@ const getMockConfigInternal = (): Config => {
return mockConfigInternal;
};
-// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const configProxy = new Proxy({} as Config, {
get(_target, prop) {
if (prop === 'getTargetDir') {
@@ -526,21 +524,13 @@ const configProxy = new Proxy({} as Config, {
}
const internal = getMockConfigInternal();
if (prop in internal) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return internal[prop as keyof typeof internal];
}
throw new Error(`mockConfig does not have property ${String(prop)}`);
},
});
-export const mockSettings = new LoadedSettings(
- { path: '', settings: {}, originalSettings: {} },
- { path: '', settings: {}, originalSettings: {} },
- { path: '', settings: {}, originalSettings: {} },
- { path: '', settings: {}, originalSettings: {} },
- true,
- [],
-);
+export const mockSettings = createMockSettings();
// A minimal mock UIState to satisfy the context provider.
// Tests that need specific UIState values should provide their own.
@@ -657,9 +647,8 @@ export const renderWithProviders = (
uiState: providedUiState,
width,
mouseEventsEnabled = false,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
config = configProxy as unknown as Config,
- useAlternateBuffer = true,
uiActions,
persistentState,
appState = mockAppState,
@@ -670,7 +659,6 @@ export const renderWithProviders = (
width?: number;
mouseEventsEnabled?: boolean;
config?: Config;
- useAlternateBuffer?: boolean;
uiActions?: Partial;
persistentState?: {
get?: typeof persistentStateMock.get;
@@ -685,20 +673,17 @@ export const renderWithProviders = (
button?: 0 | 1 | 2,
) => Promise;
} => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const baseState: UIState = new Proxy(
{ ...baseMockUiState, ...providedUiState },
{
get(target, prop) {
if (prop in target) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return target[prop as keyof typeof target];
}
// For properties not in the base mock or provided state,
// we'll check the original proxy to see if it's a defined but
// unprovided property, and if not, throw.
if (prop in baseMockUiState) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return baseMockUiState[prop as keyof typeof baseMockUiState];
}
throw new Error(`mockUiState does not have property ${String(prop)}`);
@@ -716,31 +701,8 @@ export const renderWithProviders = (
persistentStateMock.mockClear();
const terminalWidth = width ?? baseState.terminalWidth;
- let finalSettings = settings;
- if (useAlternateBuffer !== undefined) {
- finalSettings = createMockSettings({
- ...settings.merged,
- ui: {
- ...settings.merged.ui,
- useAlternateBuffer,
- },
- });
- }
-
- // Wrap config in a Proxy so useAlternateBuffer hook (which reads from Config) gets the correct value,
- // without replacing the entire config object and its other values.
- let finalConfig = config;
- if (useAlternateBuffer !== undefined) {
- finalConfig = new Proxy(config, {
- get(target, prop, receiver) {
- if (prop === 'getUseAlternateBuffer') {
- return () => useAlternateBuffer;
- }
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return Reflect.get(target, prop, receiver);
- },
- });
- }
+ const finalSettings = settings;
+ const finalConfig = config;
const mainAreaWidth = terminalWidth;
@@ -768,7 +730,7 @@ export const renderWithProviders = (
capturedOverflowState = undefined;
capturedOverflowActions = undefined;
- const renderResult = render(
+ const wrapWithProviders = (comp: React.ReactElement) => (
@@ -803,7 +765,7 @@ export const renderWithProviders = (
flexGrow={0}
flexDirection="column"
>
- {component}
+ {comp}
@@ -821,12 +783,16 @@ export const renderWithProviders = (
- ,
- terminalWidth,
+
);
+ const renderResult = render(wrapWithProviders(component), terminalWidth);
+
return {
...renderResult,
+ rerender: (newComponent: React.ReactElement) => {
+ renderResult.rerender(wrapWithProviders(newComponent));
+ },
capturedOverflowState,
capturedOverflowActions,
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
@@ -847,9 +813,8 @@ export function renderHook(
waitUntilReady: () => Promise;
generateSvg: () => string;
} {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
let currentProps = options?.initialProps as Props;
function TestComponent({
@@ -884,7 +849,6 @@ export function renderHook(
function rerender(props?: Props) {
if (arguments.length > 0) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
currentProps = props as Props;
}
act(() => {
@@ -911,7 +875,6 @@ export function renderHookWithProviders(
width?: number;
mouseEventsEnabled?: boolean;
config?: Config;
- useAlternateBuffer?: boolean;
} = {},
): {
result: { current: Result };
@@ -920,7 +883,6 @@ export function renderHookWithProviders(
waitUntilReady: () => Promise;
generateSvg: () => string;
} {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
let setPropsFn: ((props: Props) => void) | undefined;
@@ -942,7 +904,7 @@ export function renderHookWithProviders(
act(() => {
renderResult = renderWithProviders(
- {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */}
+ {}
,
options,
@@ -952,7 +914,6 @@ export function renderHookWithProviders(
function rerender(newProps?: Props) {
act(() => {
if (arguments.length > 0 && setPropsFn) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
setPropsFn(newProps as Props);
} else if (forceUpdateFn) {
forceUpdateFn();
diff --git a/packages/cli/src/test-utils/settings.ts b/packages/cli/src/test-utils/settings.ts
index dd498b6625..ab2420849d 100644
--- a/packages/cli/src/test-utils/settings.ts
+++ b/packages/cli/src/test-utils/settings.ts
@@ -46,23 +46,22 @@ export const createMockSettings = (
workspace,
isTrusted,
errors,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+
merged: mergedOverride,
...settingsOverrides
} = overrides;
const loaded = new LoadedSettings(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(system as any) || { path: '', settings: {}, originalSettings: {} },
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
(systemDefaults as any) || { path: '', settings: {}, originalSettings: {} },
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
(user as any) || {
path: '',
settings: settingsOverrides,
originalSettings: settingsOverrides,
},
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+
(workspace as any) || { path: '', settings: {}, originalSettings: {} },
isTrusted ?? true,
errors || [],
@@ -76,7 +75,6 @@ export const createMockSettings = (
// Assign any function overrides (e.g., vi.fn() for methods)
for (const key in overrides) {
if (typeof overrides[key] === 'function') {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
(loaded as any)[key] = overrides[key];
}
}
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index d96bfe3071..4e59ab854e 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -7,6 +7,7 @@
import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest';
import type React from 'react';
import { renderWithProviders } from '../test-utils/render.js';
+import { createMockSettings } from '../test-utils/settings.js';
import { Text, useIsScreenReaderEnabled, type DOMElement } from 'ink';
import { App } from './App.js';
import { type UIState } from './contexts/UIStateContext.js';
@@ -97,7 +98,8 @@ describe('App', () => {
,
{
uiState: mockUIState,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
@@ -118,7 +120,8 @@ describe('App', () => {
,
{
uiState: quittingUIState,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
@@ -139,7 +142,8 @@ describe('App', () => {
,
{
uiState: quittingUIState,
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -159,6 +163,8 @@ describe('App', () => {
,
{
uiState: dialogUIState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -185,6 +191,8 @@ describe('App', () => {
,
{
uiState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -201,6 +209,8 @@ describe('App', () => {
,
{
uiState: mockUIState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -219,6 +229,8 @@ describe('App', () => {
,
{
uiState: mockUIState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -265,7 +277,7 @@ describe('App', () => {
],
} as UIState;
- const configWithExperiment = makeFakeConfig();
+ const configWithExperiment = makeFakeConfig({ useAlternateBuffer: true });
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
@@ -274,6 +286,7 @@ describe('App', () => {
{
uiState: stateWithConfirmingTool,
config: configWithExperiment,
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -293,6 +306,8 @@ describe('App', () => {
,
{
uiState: mockUIState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -306,6 +321,8 @@ describe('App', () => {
,
{
uiState: mockUIState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
@@ -322,6 +339,8 @@ describe('App', () => {
,
{
uiState: dialogUIState,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index 13550d3f42..3e420f141d 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -95,7 +95,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
};
});
import ansiEscapes from 'ansi-escapes';
-import { mergeSettings, type LoadedSettings } from '../config/settings.js';
+import { type LoadedSettings } from '../config/settings.js';
+import { createMockSettings } from '../test-utils/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { StreamingState } from './types.js';
@@ -484,23 +485,18 @@ describe('AppContainer State Management', () => {
);
// Mock LoadedSettings
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- mockSettings = {
- merged: {
- ...defaultMergedSettings,
- hideBanner: false,
- hideFooter: false,
- hideTips: false,
- showMemoryUsage: false,
- theme: 'default',
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: false,
- hideWindowTitle: false,
- useAlternateBuffer: false,
- },
+ mockSettings = createMockSettings({
+ hideBanner: false,
+ hideFooter: false,
+ hideTips: false,
+ showMemoryUsage: false,
+ theme: 'default',
+ ui: {
+ showStatusInTitle: false,
+ hideWindowTitle: false,
+ useAlternateBuffer: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock InitializationResult
mockInitResult = {
@@ -1008,16 +1004,12 @@ describe('AppContainer State Management', () => {
describe('Settings Integration', () => {
it('handles settings with all display options disabled', async () => {
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const settingsAllHidden = {
- merged: {
- ...defaultMergedSettings,
- hideBanner: true,
- hideFooter: true,
- hideTips: true,
- showMemoryUsage: false,
- },
- } as unknown as LoadedSettings;
+ const settingsAllHidden = createMockSettings({
+ hideBanner: true,
+ hideFooter: true,
+ hideTips: true,
+ showMemoryUsage: false,
+ });
let unmount: () => void;
await act(async () => {
@@ -1029,16 +1021,9 @@ describe('AppContainer State Management', () => {
});
it('handles settings with memory usage enabled', async () => {
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const settingsWithMemory = {
- merged: {
- ...defaultMergedSettings,
- hideBanner: false,
- hideFooter: false,
- hideTips: false,
- showMemoryUsage: true,
- },
- } as unknown as LoadedSettings;
+ const settingsWithMemory = createMockSettings({
+ showMemoryUsage: true,
+ });
let unmount: () => void;
await act(async () => {
@@ -1078,9 +1063,7 @@ describe('AppContainer State Management', () => {
});
it('handles undefined settings gracefully', async () => {
- const undefinedSettings = {
- merged: mergeSettings({}, {}, {}, {}, true),
- } as LoadedSettings;
+ const undefinedSettings = createMockSettings();
let unmount: () => void;
await act(async () => {
@@ -1498,18 +1481,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title with Working… when showStatusInTitle is false', () => {
// Arrange: Set up mock settings with showStatusInTitle disabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithShowStatusFalse = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: false,
- hideWindowTitle: false,
- },
+ const mockSettingsWithShowStatusFalse = createMockSettings({
+ ui: {
+ showStatusInTitle: false,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state as Active
mockedUseGeminiStream.mockReturnValue({
@@ -1537,17 +1514,12 @@ describe('AppContainer State Management', () => {
it('should use legacy terminal title when dynamicWindowTitle is false', () => {
// Arrange: Set up mock settings with dynamicWindowTitle disabled
- const mockSettingsWithDynamicTitleFalse = {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: {
- ...mockSettings.merged.ui,
- dynamicWindowTitle: false,
- hideWindowTitle: false,
- },
+ const mockSettingsWithDynamicTitleFalse = createMockSettings({
+ ui: {
+ dynamicWindowTitle: false,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state
mockedUseGeminiStream.mockReturnValue({
@@ -1575,18 +1547,12 @@ describe('AppContainer State Management', () => {
it('should not update terminal title when hideWindowTitle is true', () => {
// Arrange: Set up mock settings with hideWindowTitle enabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithHideTitleTrue = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: true,
- hideWindowTitle: true,
- },
+ const mockSettingsWithHideTitleTrue = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: true,
},
- } as unknown as LoadedSettings;
+ });
// Act: Render the container
const { unmount } = renderAppContainer({
@@ -1604,18 +1570,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title with thought subject when in active state', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state and thought
const thoughtSubject = 'Processing request';
@@ -1644,18 +1604,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title with default text when in Idle state and no thought subject', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state as Idle with no thought
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
@@ -1679,18 +1633,12 @@ describe('AppContainer State Management', () => {
it('should update terminal title when in WaitingForConfirmation state with thought subject', async () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state and thought
const thoughtSubject = 'Confirm tool execution';
@@ -1742,17 +1690,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: {
- ...mockSettings.merged.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock an active shell pty but not focused
mockedUseGeminiStream.mockReturnValue({
@@ -1801,17 +1744,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: {
- ...mockSettings.merged.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock an active shell pty with redirection active
mockedUseGeminiStream.mockReturnValue({
@@ -1871,17 +1809,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: {
- ...mockSettings.merged.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock an active shell pty with NO output since operation started (silent)
mockedUseGeminiStream.mockReturnValue({
@@ -1921,17 +1854,12 @@ describe('AppContainer State Management', () => {
vi.setSystemTime(startTime);
// Arrange: Set up mock settings with showStatusInTitle enabled
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: {
- ...mockSettings.merged.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock an active shell pty but not focused
let lastOutputTime = startTime + 1000;
@@ -2005,18 +1933,12 @@ describe('AppContainer State Management', () => {
it('should pad title to exactly 80 characters', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state and thought with a short subject
const shortTitle = 'Short';
@@ -2046,18 +1968,12 @@ describe('AppContainer State Management', () => {
it('should use correct ANSI escape code format', () => {
// Arrange: Set up mock settings with showStatusInTitle enabled
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const mockSettingsWithTitleEnabled = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- showStatusInTitle: true,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleEnabled = createMockSettings({
+ ui: {
+ showStatusInTitle: true,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock the streaming state and thought
const title = 'Test Title';
@@ -2085,17 +2001,12 @@ describe('AppContainer State Management', () => {
it('should use CLI_TITLE environment variable when set', () => {
// Arrange: Set up mock settings with showStatusInTitle disabled (so it shows suffix)
- const mockSettingsWithTitleDisabled = {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: {
- ...mockSettings.merged.ui,
- showStatusInTitle: false,
- hideWindowTitle: false,
- },
+ const mockSettingsWithTitleDisabled = createMockSettings({
+ ui: {
+ showStatusInTitle: false,
+ hideWindowTitle: false,
},
- } as unknown as LoadedSettings;
+ });
// Mock CLI_TITLE environment variable
vi.stubEnv('CLI_TITLE', 'Custom Gemini Title');
@@ -2664,17 +2575,9 @@ describe('AppContainer State Management', () => {
);
// Update settings for this test run
- const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
- const testSettings = {
- ...mockSettings,
- merged: {
- ...defaultMergedSettings,
- ui: {
- ...defaultMergedSettings.ui,
- useAlternateBuffer: isAlternateMode,
- },
- },
- } as unknown as LoadedSettings;
+ const testSettings = createMockSettings({
+ ui: { useAlternateBuffer: isAlternateMode },
+ });
function TestChild() {
useKeypress(childHandler || (() => {}), {
@@ -3384,13 +3287,7 @@ describe('AppContainer State Management', () => {
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
- settings: {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: { ...mockSettings.merged.ui, useAlternateBuffer: false },
- },
- } as LoadedSettings,
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
}).unmount;
});
@@ -3426,13 +3323,7 @@ describe('AppContainer State Management', () => {
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
- settings: {
- ...mockSettings,
- merged: {
- ...mockSettings.merged,
- ui: { ...mockSettings.merged.ui, useAlternateBuffer: true },
- },
- } as LoadedSettings,
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
}).unmount;
});
@@ -3701,16 +3592,9 @@ describe('AppContainer State Management', () => {
});
it('DOES set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
- const alternateSettings = mergeSettings({}, {}, {}, {}, true);
- const settingsWithAlternateBuffer = {
- merged: {
- ...alternateSettings,
- ui: {
- ...alternateSettings.ui,
- useAlternateBuffer: true,
- },
- },
- } as unknown as LoadedSettings;
+ const settingsWithAlternateBuffer = createMockSettings({
+ ui: { useAlternateBuffer: true },
+ });
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index b0a936a81b..b2402f9fe9 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -1677,11 +1677,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
const handleGlobalKeypress = useCallback(
(key: Key): boolean => {
- // Debug log keystrokes if enabled
- if (settings.merged.general.debugKeystrokeLogging) {
- debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
- }
-
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
setShortcutsHelpVisible(false);
}
@@ -1860,7 +1855,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
activePtyId,
handleSuspend,
embeddedShellFocused,
- settings.merged.general.debugKeystrokeLogging,
refreshStatic,
setCopyModeEnabled,
tabFocusTimeoutRef,
diff --git a/packages/cli/src/ui/IdeIntegrationNudge.test.tsx b/packages/cli/src/ui/IdeIntegrationNudge.test.tsx
index 52d00550ea..1b30e0e0b2 100644
--- a/packages/cli/src/ui/IdeIntegrationNudge.test.tsx
+++ b/packages/cli/src/ui/IdeIntegrationNudge.test.tsx
@@ -5,10 +5,9 @@
*/
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
-import { render } from '../test-utils/render.js';
+import { renderWithProviders } from '../test-utils/render.js';
import { act } from 'react';
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
-import { KeypressProvider } from './contexts/KeypressContext.js';
import { debugLogger } from '@google/gemini-cli-core';
// Mock debugLogger
@@ -54,10 +53,8 @@ describe('IdeIntegrationNudge', () => {
});
it('renders correctly with default options', async () => {
- const { lastFrame, waitUntilReady, unmount } = render(
-
-
- ,
+ const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
+ ,
);
await waitUntilReady();
const frame = lastFrame();
@@ -71,10 +68,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "Yes" selection', async () => {
const onComplete = vi.fn();
- const { stdin, waitUntilReady, unmount } = render(
-
-
- ,
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
);
await waitUntilReady();
@@ -94,10 +89,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "No" selection', async () => {
const onComplete = vi.fn();
- const { stdin, waitUntilReady, unmount } = render(
-
-
- ,
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
);
await waitUntilReady();
@@ -122,10 +115,8 @@ describe('IdeIntegrationNudge', () => {
it('handles "Dismiss" selection', async () => {
const onComplete = vi.fn();
- const { stdin, waitUntilReady, unmount } = render(
-
-
- ,
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
);
await waitUntilReady();
@@ -155,10 +146,8 @@ describe('IdeIntegrationNudge', () => {
it('handles Escape key press', async () => {
const onComplete = vi.fn();
- const { stdin, waitUntilReady, unmount } = render(
-
-
- ,
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
);
await waitUntilReady();
@@ -184,10 +173,8 @@ describe('IdeIntegrationNudge', () => {
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp');
const onComplete = vi.fn();
- const { lastFrame, stdin, waitUntilReady, unmount } = render(
-
-
- ,
+ const { lastFrame, stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx
index 52cda094e0..2e5b6ecdb2 100644
--- a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx
+++ b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx
@@ -4,21 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render } from '../../test-utils/render.js';
+import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { act } from 'react';
import { AgentConfigDialog } from './AgentConfigDialog.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
-import { KeypressProvider } from '../contexts/KeypressContext.js';
import type { AgentDefinition } from '@google/gemini-cli-core';
-vi.mock('../contexts/UIStateContext.js', () => ({
- useUIState: () => ({
- mainAreaWidth: 100,
- }),
-}));
-
enum TerminalKeys {
ENTER = '\u000D',
TAB = '\t',
@@ -122,17 +115,16 @@ describe('AgentConfigDialog', () => {
settings: LoadedSettings,
definition: AgentDefinition = createMockAgentDefinition(),
) => {
- const result = render(
-
-
- ,
+ const result = renderWithProviders(
+ ,
+ { settings, uiState: { mainAreaWidth: 100 } },
);
await result.waitUntilReady();
return result;
@@ -331,18 +323,17 @@ describe('AgentConfigDialog', () => {
const settings = createMockSettings();
// Agent config has about 6 base items + 2 per tool
// Render with very small height (20)
- const { lastFrame, unmount } = render(
-
-
- ,
+ const { lastFrame, unmount } = renderWithProviders(
+ ,
+ { settings, uiState: { mainAreaWidth: 100 } },
);
await waitFor(() =>
expect(lastFrame()).toContain('Configure: Test Agent'),
diff --git a/packages/cli/src/ui/components/AnsiOutput.tsx b/packages/cli/src/ui/components/AnsiOutput.tsx
index cc17b6b6b0..a1b30b0856 100644
--- a/packages/cli/src/ui/components/AnsiOutput.tsx
+++ b/packages/cli/src/ui/components/AnsiOutput.tsx
@@ -35,7 +35,11 @@ export const AnsiOutputText: React.FC = ({
? Math.min(availableHeightLimit, maxLines)
: (availableHeightLimit ?? maxLines ?? DEFAULT_HEIGHT);
- const lastLines = disableTruncation ? data : data.slice(-numLinesRetained);
+ const lastLines = disableTruncation
+ ? data
+ : numLinesRetained === 0
+ ? []
+ : data.slice(-numLinesRetained);
return (
{lastLines.map((line: AnsiLine, lineIndex: number) => (
diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx
index 0857306ea8..67289769be 100644
--- a/packages/cli/src/ui/components/AskUserDialog.test.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx
@@ -7,6 +7,8 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
+import { createMockSettings } from '../../test-utils/settings.js';
+import { makeFakeConfig } from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { AskUserDialog } from './AskUserDialog.js';
import { QuestionType, type Question } from '@google/gemini-cli-core';
@@ -87,6 +89,31 @@ describe('AskUserDialog', () => {
writeKey(stdin, '\r'); // Toggle TS
writeKey(stdin, '\x1b[B'); // Down
writeKey(stdin, '\r'); // Toggle ESLint
+ writeKey(stdin, '\x1b[B'); // Down to All of the above
+ writeKey(stdin, '\x1b[B'); // Down to Other
+ writeKey(stdin, '\x1b[B'); // Down to Done
+ writeKey(stdin, '\r'); // Done
+ },
+ expectedSubmit: { '0': 'TypeScript, ESLint' },
+ },
+ {
+ name: 'All of the above',
+ questions: [
+ {
+ question: 'Which features?',
+ header: 'Features',
+ type: QuestionType.CHOICE,
+ options: [
+ { label: 'TypeScript', description: '' },
+ { label: 'ESLint', description: '' },
+ ],
+ multiSelect: true,
+ },
+ ] as Question[],
+ actions: (stdin: { write: (data: string) => void }) => {
+ writeKey(stdin, '\x1b[B'); // Down to ESLint
+ writeKey(stdin, '\x1b[B'); // Down to All of the above
+ writeKey(stdin, '\r'); // Toggle All of the above
writeKey(stdin, '\x1b[B'); // Down to Other
writeKey(stdin, '\x1b[B'); // Down to Done
writeKey(stdin, '\r'); // Done
@@ -131,6 +158,42 @@ describe('AskUserDialog', () => {
});
});
+ it('verifies "All of the above" visual state with snapshot', async () => {
+ const questions = [
+ {
+ question: 'Which features?',
+ header: 'Features',
+ type: QuestionType.CHOICE,
+ options: [
+ { label: 'TypeScript', description: '' },
+ { label: 'ESLint', description: '' },
+ ],
+ multiSelect: true,
+ },
+ ] as Question[];
+
+ const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
+ ,
+ { width: 120 },
+ );
+
+ // Navigate to "All of the above" and toggle it
+ writeKey(stdin, '\x1b[B'); // Down to ESLint
+ writeKey(stdin, '\x1b[B'); // Down to All of the above
+ writeKey(stdin, '\r'); // Toggle All of the above
+
+ await waitFor(async () => {
+ await waitUntilReady();
+ // Verify visual state (checkmarks on all options)
+ expect(lastFrame()).toMatchSnapshot();
+ });
+ });
+
it('handles custom option in single select with inline typing', async () => {
const onSubmit = vi.fn();
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
@@ -252,7 +315,10 @@ describe('AskUserDialog', () => {
width={80}
availableHeight={10} // Small height to force scrolling
/>,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(async () => {
@@ -1230,7 +1296,10 @@ describe('AskUserDialog', () => {
width={80}
/>
,
- { useAlternateBuffer: false },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
+ },
);
// With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
@@ -1266,7 +1335,10 @@ describe('AskUserDialog', () => {
width={40} // Small width to force wrapping
/>
,
- { useAlternateBuffer: true },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
+ },
);
// Should NOT contain the truncation message
diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx
index eec633b7de..b1d23885e6 100644
--- a/packages/cli/src/ui/components/AskUserDialog.tsx
+++ b/packages/cli/src/ui/components/AskUserDialog.tsx
@@ -395,7 +395,7 @@ interface OptionItem {
key: string;
label: string;
description: string;
- type: 'option' | 'other' | 'done';
+ type: 'option' | 'other' | 'done' | 'all';
index: number;
}
@@ -407,6 +407,7 @@ interface ChoiceQuestionState {
type ChoiceQuestionAction =
| { type: 'TOGGLE_INDEX'; payload: { index: number; multiSelect: boolean } }
+ | { type: 'TOGGLE_ALL'; payload: { totalOptions: number } }
| {
type: 'SET_CUSTOM_SELECTED';
payload: { selected: boolean; multiSelect: boolean };
@@ -419,6 +420,25 @@ function choiceQuestionReducer(
action: ChoiceQuestionAction,
): ChoiceQuestionState {
switch (action.type) {
+ case 'TOGGLE_ALL': {
+ const { totalOptions } = action.payload;
+ const allSelected = state.selectedIndices.size === totalOptions;
+ if (allSelected) {
+ return {
+ ...state,
+ selectedIndices: new Set(),
+ };
+ } else {
+ const newIndices = new Set();
+ for (let i = 0; i < totalOptions; i++) {
+ newIndices.add(i);
+ }
+ return {
+ ...state,
+ selectedIndices: newIndices,
+ };
+ }
+ }
case 'TOGGLE_INDEX': {
const { index, multiSelect } = action.payload;
const newIndices = new Set(multiSelect ? state.selectedIndices : []);
@@ -703,6 +723,18 @@ const ChoiceQuestionView: React.FC = ({
},
);
+ // Add 'All of the above' for multi-select
+ if (question.multiSelect && questionOptions.length > 1) {
+ const allItem: OptionItem = {
+ key: 'all',
+ label: 'All of the above',
+ description: 'Select all options',
+ type: 'all',
+ index: list.length,
+ };
+ list.push({ key: 'all', value: allItem });
+ }
+
// Only add custom option for choice type, not yesno
if (question.type !== 'yesno') {
const otherItem: OptionItem = {
@@ -755,6 +787,11 @@ const ChoiceQuestionView: React.FC = ({
type: 'TOGGLE_CUSTOM_SELECTED',
payload: { multiSelect: true },
});
+ } else if (itemValue.type === 'all') {
+ dispatch({
+ type: 'TOGGLE_ALL',
+ payload: { totalOptions: questionOptions.length },
+ });
} else if (itemValue.type === 'done') {
// Done just triggers navigation, selections already saved via useEffect
onAnswer(
@@ -783,6 +820,7 @@ const ChoiceQuestionView: React.FC = ({
},
[
question.multiSelect,
+ questionOptions.length,
selectedIndices,
isCustomOptionSelected,
customOptionText,
@@ -857,11 +895,16 @@ const ChoiceQuestionView: React.FC = ({
renderItem={(item, context) => {
const optionItem = item.value;
const isChecked =
- selectedIndices.has(optionItem.index) ||
- (optionItem.type === 'other' && isCustomOptionSelected);
+ (optionItem.type === 'option' &&
+ selectedIndices.has(optionItem.index)) ||
+ (optionItem.type === 'other' && isCustomOptionSelected) ||
+ (optionItem.type === 'all' &&
+ selectedIndices.size === questionOptions.length);
const showCheck =
question.multiSelect &&
- (optionItem.type === 'option' || optionItem.type === 'other');
+ (optionItem.type === 'option' ||
+ optionItem.type === 'other' ||
+ optionItem.type === 'all');
// Render inline text input for custom option
if (optionItem.type === 'other') {
diff --git a/packages/cli/src/ui/components/Banner.test.tsx b/packages/cli/src/ui/components/Banner.test.tsx
index 46c47b8a71..00a2bf609f 100644
--- a/packages/cli/src/ui/components/Banner.test.tsx
+++ b/packages/cli/src/ui/components/Banner.test.tsx
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render } from '../../test-utils/render.js';
+import { renderWithProviders } from '../../test-utils/render.js';
import { Banner } from './Banner.js';
import { describe, it, expect } from 'vitest';
@@ -12,22 +12,23 @@ describe('Banner', () => {
it.each([
['warning mode', true, 'Warning Message'],
['info mode', false, 'Info Message'],
+ ['multi-line warning', true, 'Title Line\\nBody Line 1\\nBody Line 2'],
])('renders in %s', async (_, isWarning, text) => {
- const { lastFrame, waitUntilReady, unmount } = render(
+ const renderResult = renderWithProviders(
,
);
- await waitUntilReady();
- expect(lastFrame()).toMatchSnapshot();
- unmount();
+ await renderResult.waitUntilReady();
+ await expect(renderResult).toMatchSvgSnapshot();
+ renderResult.unmount();
});
it('handles newlines in text', async () => {
const text = 'Line 1\\nLine 2';
- const { lastFrame, waitUntilReady, unmount } = render(
+ const renderResult = renderWithProviders(
,
);
- await waitUntilReady();
- expect(lastFrame()).toMatchSnapshot();
- unmount();
+ await renderResult.waitUntilReady();
+ await expect(renderResult).toMatchSvgSnapshot();
+ renderResult.unmount();
});
});
diff --git a/packages/cli/src/ui/components/Banner.tsx b/packages/cli/src/ui/components/Banner.tsx
index 99f573a68e..3f9777aa45 100644
--- a/packages/cli/src/ui/components/Banner.tsx
+++ b/packages/cli/src/ui/components/Banner.tsx
@@ -14,20 +14,21 @@ export function getFormattedBannerContent(
isWarning: boolean,
subsequentLineColor: string,
): ReactNode {
- if (isWarning) {
- return (
- {rawText.replace(/\\n/g, '\n')}
- );
- }
-
const text = rawText.replace(/\\n/g, '\n');
const lines = text.split('\n');
return lines.map((line, index) => {
if (index === 0) {
+ if (isWarning) {
+ return (
+
+ {line}
+
+ );
+ }
return (
- {line}
+ {line}
);
}
diff --git a/packages/cli/src/ui/components/ChecklistItem.test.tsx b/packages/cli/src/ui/components/ChecklistItem.test.tsx
index 0f6c0eb0b0..4176f7914b 100644
--- a/packages/cli/src/ui/components/ChecklistItem.test.tsx
+++ b/packages/cli/src/ui/components/ChecklistItem.test.tsx
@@ -15,6 +15,7 @@ describe('', () => {
{ status: 'in_progress', label: 'Doing this' },
{ status: 'completed', label: 'Done this' },
{ status: 'cancelled', label: 'Skipped this' },
+ { status: 'blocked', label: 'Blocked this' },
] as ChecklistItemData[])('renders %s item correctly', async (item) => {
const { lastFrame, waitUntilReady } = render();
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/ChecklistItem.tsx b/packages/cli/src/ui/components/ChecklistItem.tsx
index 6e08e0af6b..065c79d516 100644
--- a/packages/cli/src/ui/components/ChecklistItem.tsx
+++ b/packages/cli/src/ui/components/ChecklistItem.tsx
@@ -13,7 +13,8 @@ export type ChecklistStatus =
| 'pending'
| 'in_progress'
| 'completed'
- | 'cancelled';
+ | 'cancelled'
+ | 'blocked';
export interface ChecklistItemData {
status: ChecklistStatus;
@@ -48,6 +49,12 @@ const ChecklistStatusDisplay: React.FC<{ status: ChecklistStatus }> = ({
✗
);
+ case 'blocked':
+ return (
+
+ ⛔
+
+ );
default:
checkExhaustive(status);
}
@@ -70,6 +77,7 @@ export const ChecklistItem: React.FC = ({
return theme.text.accent;
case 'completed':
case 'cancelled':
+ case 'blocked':
return theme.text.secondary;
case 'pending':
return theme.text.primary;
diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx
index 84f8d15a06..e0919947fb 100644
--- a/packages/cli/src/ui/components/Composer.test.tsx
+++ b/packages/cli/src/ui/components/Composer.test.tsx
@@ -408,7 +408,7 @@ describe('Composer', () => {
thought: { subject: 'Hidden', description: 'Should not show' },
});
const settings = createMockSettings({
- merged: { ui: { loadingPhrases: 'off' } },
+ ui: { loadingPhrases: 'off' },
});
const { lastFrame } = await renderComposer(uiState, settings);
diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx
index 65d54e50d6..b6fd50b33f 100644
--- a/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx
+++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.test.tsx
@@ -38,9 +38,7 @@ describe('DetailedMessagesDisplay', () => {
hasFocus={false}
/>,
{
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'full' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
},
);
await waitUntilReady();
@@ -64,9 +62,7 @@ describe('DetailedMessagesDisplay', () => {
hasFocus={true}
/>,
{
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'full' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
},
);
await waitUntilReady();
@@ -89,9 +85,7 @@ describe('DetailedMessagesDisplay', () => {
hasFocus={true}
/>,
{
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'low' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
},
);
await waitUntilReady();
@@ -112,9 +106,7 @@ describe('DetailedMessagesDisplay', () => {
hasFocus={true}
/>,
{
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'full' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
},
);
await waitUntilReady();
@@ -135,9 +127,7 @@ describe('DetailedMessagesDisplay', () => {
hasFocus={false}
/>,
{
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'full' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
},
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx
index 6ebe22d982..d3b285c3a4 100644
--- a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx
+++ b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx
@@ -4,11 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render } from '../../test-utils/render.js';
+import { renderWithProviders } from '../../test-utils/render.js';
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SettingScope, type LoadedSettings } from '../../config/settings.js';
-import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
import { waitFor } from '../../test-utils/async.js';
import { debugLogger } from '@google/gemini-cli-core';
@@ -52,8 +51,8 @@ describe('EditorSettingsDialog', () => {
vi.clearAllMocks();
});
- const renderWithProvider = (ui: React.ReactNode) =>
- render({ui});
+ const renderWithProvider = (ui: React.ReactElement) =>
+ renderWithProviders(ui);
it('renders correctly', async () => {
const { lastFrame, waitUntilReady } = renderWithProvider(
diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx
index 33daca1e33..231d5f102f 100644
--- a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx
+++ b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx
@@ -7,6 +7,7 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
+import { createMockSettings } from '../../test-utils/settings.js';
import { waitFor } from '../../test-utils/async.js';
import { ExitPlanModeDialog } from './ExitPlanModeDialog.js';
import { useKeypress } from '../hooks/useKeypress.js';
@@ -138,8 +139,9 @@ Implement a comprehensive authentication system with multiple providers.
vi.restoreAllMocks();
});
- const renderDialog = (options?: { useAlternateBuffer?: boolean }) =>
- renderWithProviders(
+ const renderDialog = (options?: { useAlternateBuffer?: boolean }) => {
+ const useAlternateBuffer = options?.useAlternateBuffer ?? true;
+ return renderWithProviders(
options?.useAlternateBuffer ?? true,
+ getUseAlternateBuffer: () => useAlternateBuffer,
} as unknown as import('@google/gemini-cli-core').Config,
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
},
);
+ };
describe.each([{ useAlternateBuffer: true }, { useAlternateBuffer: false }])(
'useAlternateBuffer: $useAlternateBuffer',
@@ -429,7 +433,6 @@ Implement a comprehensive authentication system with multiple providers.
/>
,
{
- useAlternateBuffer,
config: {
getTargetDir: () => mockTargetDir,
getIdeMode: () => false,
@@ -443,6 +446,9 @@ Implement a comprehensive authentication system with multiple providers.
}),
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
} as unknown as import('@google/gemini-cli-core').Config,
+ settings: createMockSettings({
+ ui: { useAlternateBuffer: useAlternateBuffer ?? true },
+ }),
},
);
diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx
index e68417fc55..9ad4fac02d 100644
--- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx
+++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx
@@ -5,11 +5,12 @@
*/
import { renderWithProviders } from '../../test-utils/render.js';
+import { createMockSettings } from '../../test-utils/settings.js';
+import { makeFakeConfig, ExitCodes } from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { act } from 'react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { FolderTrustDialog } from './FolderTrustDialog.js';
-import { ExitCodes } from '@google/gemini-cli-core';
import * as processUtils from '../../utils/processUtils.js';
vi.mock('../../utils/processUtils.js', () => ({
@@ -78,7 +79,8 @@ describe('FolderTrustDialog', () => {
/>,
{
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true, terminalHeight: 24 },
},
);
@@ -108,7 +110,8 @@ describe('FolderTrustDialog', () => {
/>,
{
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true, terminalHeight: 14 },
},
);
@@ -139,7 +142,8 @@ describe('FolderTrustDialog', () => {
/>,
{
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true, terminalHeight: 10 },
},
);
@@ -168,7 +172,8 @@ describe('FolderTrustDialog', () => {
/>,
{
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
// Initially constrained
uiState: { constrainHeight: true, terminalHeight: 24 },
},
@@ -194,7 +199,8 @@ describe('FolderTrustDialog', () => {
/>,
{
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: false, terminalHeight: 24 },
},
);
@@ -434,7 +440,8 @@ describe('FolderTrustDialog', () => {
/>,
{
width: 80,
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: { constrainHeight: false, terminalHeight: 15 },
},
);
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index ab487a440f..84782b2513 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -673,9 +673,7 @@ describe('', () => {
errorCount: 2,
showErrorDetails: false,
},
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'low' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
},
);
await waitUntilReady();
@@ -694,9 +692,7 @@ describe('', () => {
errorCount: 2,
showErrorDetails: false,
},
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'low' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
},
);
await waitUntilReady();
@@ -715,9 +711,7 @@ describe('', () => {
errorCount: 2,
showErrorDetails: false,
},
- settings: createMockSettings({
- merged: { ui: { errorVerbosity: 'full' } },
- }),
+ settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
},
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
index f049ffe15e..d8a08f1991 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
@@ -16,6 +16,7 @@ import {
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
+import { makeFakeConfig } from '@google/gemini-cli-core';
// Mock child components
vi.mock('./messages/ToolGroupMessage.js', () => ({
@@ -84,7 +85,10 @@ describe('', () => {
};
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -278,9 +282,7 @@ describe('', () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
,
{
- settings: createMockSettings({
- merged: { ui: { inlineThinkingMode: 'full' } },
- }),
+ settings: createMockSettings({ ui: { inlineThinkingMode: 'full' } }),
},
);
await waitUntilReady();
@@ -298,9 +300,7 @@ describe('', () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
,
{
- settings: createMockSettings({
- merged: { ui: { inlineThinkingMode: 'full' } },
- }),
+ settings: createMockSettings({ ui: { inlineThinkingMode: 'full' } }),
},
);
await waitUntilReady();
@@ -318,9 +318,7 @@ describe('', () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
,
{
- settings: createMockSettings({
- merged: { ui: { inlineThinkingMode: 'off' } },
- }),
+ settings: createMockSettings({ ui: { inlineThinkingMode: 'off' } }),
},
);
await waitUntilReady();
@@ -352,7 +350,10 @@ describe('', () => {
terminalWidth={80}
availableTerminalHeight={10}
/>,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitUntilReady();
@@ -374,7 +375,10 @@ describe('', () => {
availableTerminalHeight={10}
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
/>,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitUntilReady();
@@ -395,7 +399,10 @@ describe('', () => {
terminalWidth={80}
availableTerminalHeight={10}
/>,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitUntilReady();
@@ -417,7 +424,10 @@ describe('', () => {
availableTerminalHeight={10}
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
/>,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index c092e600b9..b741506186 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -6,6 +6,7 @@
import { renderWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
+import { makeFakeConfig } from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { act, useState } from 'react';
import {
@@ -3512,7 +3513,8 @@ describe('InputPrompt', () => {
,
{
mouseEventsEnabled: true,
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiActions,
},
);
@@ -3603,7 +3605,8 @@ describe('InputPrompt', () => {
,
{
mouseEventsEnabled: true,
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiActions,
},
);
diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx
index e0880e624c..94cfe9085d 100644
--- a/packages/cli/src/ui/components/MainContent.test.tsx
+++ b/packages/cli/src/ui/components/MainContent.test.tsx
@@ -5,6 +5,8 @@
*/
import { renderWithProviders } from '../../test-utils/render.js';
+import { createMockSettings } from '../../test-utils/settings.js';
+import { makeFakeConfig, CoreToolCallStatus } from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { MainContent } from './MainContent.js';
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
@@ -18,7 +20,6 @@ import {
useUIState,
type UIState,
} from '../contexts/UIStateContext.js';
-import { CoreToolCallStatus } from '@google/gemini-cli-core';
import { type IndividualToolCallDisplay } from '../types.js';
// Mock dependencies
@@ -482,7 +483,8 @@ describe('MainContent', () => {
,
{
uiState: uiState as Partial,
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -509,7 +511,8 @@ describe('MainContent', () => {
,
{
uiState: uiState as unknown as Partial,
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -733,7 +736,10 @@ describe('MainContent', () => {
,
{
uiState: uiState as Partial,
- useAlternateBuffer: isAlternateBuffer,
+ config: makeFakeConfig({ useAlternateBuffer: isAlternateBuffer }),
+ settings: createMockSettings({
+ ui: { useAlternateBuffer: isAlternateBuffer },
+ }),
},
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx
index d7e04bd351..0530e171b8 100644
--- a/packages/cli/src/ui/components/MainContent.tsx
+++ b/packages/cli/src/ui/components/MainContent.tsx
@@ -48,6 +48,7 @@ export const MainContent = () => {
pendingHistoryItems,
mainAreaWidth,
staticAreaMaxItemHeight,
+ availableTerminalHeight,
cleanUiDetailsVisible,
} = uiState;
const showHeaderDetails = cleanUiDetailsVisible;
@@ -141,7 +142,7 @@ export const MainContent = () => {
{
[
pendingHistoryItems,
uiState.constrainHeight,
- staticAreaMaxItemHeight,
+ availableTerminalHeight,
mainAreaWidth,
showConfirmationQueue,
confirmingTool,
diff --git a/packages/cli/src/ui/components/NewAgentsNotification.test.tsx b/packages/cli/src/ui/components/NewAgentsNotification.test.tsx
index b184eebffb..d234b70c4d 100644
--- a/packages/cli/src/ui/components/NewAgentsNotification.test.tsx
+++ b/packages/cli/src/ui/components/NewAgentsNotification.test.tsx
@@ -22,6 +22,25 @@ describe('NewAgentsNotification', () => {
{
name: 'Agent B',
description: 'Description B',
+ kind: 'local' as const,
+ inputConfig: { inputSchema: {} },
+ promptConfig: {},
+ modelConfig: {},
+ runConfig: {},
+ mcpServers: {
+ github: {
+ command: 'npx',
+ args: ['-y', '@modelcontextprotocol/server-github'],
+ },
+ postgres: {
+ command: 'npx',
+ args: ['-y', '@modelcontextprotocol/server-postgres'],
+ },
+ },
+ },
+ {
+ name: 'Agent C',
+ description: 'Description C',
kind: 'remote' as const,
agentCardUrl: '',
inputConfig: { inputSchema: {} },
diff --git a/packages/cli/src/ui/components/NewAgentsNotification.tsx b/packages/cli/src/ui/components/NewAgentsNotification.tsx
index e7aa8be510..53287ec433 100644
--- a/packages/cli/src/ui/components/NewAgentsNotification.tsx
+++ b/packages/cli/src/ui/components/NewAgentsNotification.tsx
@@ -80,16 +80,35 @@ export const NewAgentsNotification = ({
borderStyle="single"
padding={1}
>
- {displayAgents.map((agent) => (
-
-
-
- - {agent.name}:{' '}
-
+ {displayAgents.map((agent) => {
+ const mcpServers =
+ agent.kind === 'local' ? agent.mcpServers : undefined;
+ const hasMcpServers =
+ mcpServers && Object.keys(mcpServers).length > 0;
+ return (
+
+
+
+
+ - {agent.name}:{' '}
+
+
+
+ {' '}
+ {agent.description}
+
+
+ {hasMcpServers && (
+
+
+ (Includes MCP servers:{' '}
+ {Object.keys(mcpServers).join(', ')})
+
+
+ )}
- {agent.description}
-
- ))}
+ );
+ })}
{remaining > 0 && (
... and {remaining} more.
diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx
index 0fc80a1d4e..ac9b2c2b00 100644
--- a/packages/cli/src/ui/components/SessionBrowser.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.tsx
@@ -110,78 +110,17 @@ const SESSIONS_PER_PAGE = 20;
// If the SessionItem layout changes, update this accordingly.
const FIXED_SESSION_COLUMNS_WIDTH = 30;
-const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => (
- <>
- {name}: {shortcut}
- >
-);
-
+import {
+ SearchModeDisplay,
+ NavigationHelpDisplay,
+ NoResultsDisplay,
+} from './SessionBrowser/SessionBrowserNav.js';
+import { SessionListHeader } from './SessionBrowser/SessionListHeader.js';
import { SessionBrowserLoading } from './SessionBrowser/SessionBrowserLoading.js';
import { SessionBrowserError } from './SessionBrowser/SessionBrowserError.js';
import { SessionBrowserEmpty } from './SessionBrowser/SessionBrowserEmpty.js';
-
import { sortSessions, filterSessions } from './SessionBrowser/utils.js';
-/**
- * Search input display component.
- */
-const SearchModeDisplay = ({
- state,
-}: {
- state: SessionBrowserState;
-}): React.JSX.Element => (
-
- Search:
- {state.searchQuery}
- (Esc to cancel)
-
-);
-
-/**
- * Header component showing session count and sort information.
- */
-const SessionListHeader = ({
- state,
-}: {
- state: SessionBrowserState;
-}): React.JSX.Element => (
-
-
- Chat Sessions ({state.totalSessions} total
- {state.searchQuery ? `, filtered` : ''})
-
-
- sorted by {state.sortOrder} {state.sortReverse ? 'asc' : 'desc'}
-
-
-);
-
-/**
- * Navigation help component showing keyboard shortcuts.
- */
-const NavigationHelp = (): React.JSX.Element => (
-
-
-
- {' '}
-
- {' '}
-
- {' '}
-
- {' '}
-
-
-
-
- {' '}
-
- {' '}
-
-
-
-);
-
/**
* Table header component with column labels and scroll indicators.
*/
@@ -219,21 +158,6 @@ const SessionTableHeader = ({
);
-/**
- * No results display component for empty search results.
- */
-const NoResultsDisplay = ({
- state,
-}: {
- state: SessionBrowserState;
-}): React.JSX.Element => (
-
-
- No sessions found matching '{state.searchQuery}'.
-
-
-);
-
/**
* Match snippet display component for search results.
*/
@@ -398,7 +322,7 @@ const SessionList = ({
{/* Table Header */}
- {!state.isSearchMode && }
+ {!state.isSearchMode && }
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.tsx
new file mode 100644
index 0000000000..99d0363ed5
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.tsx
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+import type { SessionBrowserState } from '../SessionBrowser.js';
+
+const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => (
+ <>
+ {name}: {shortcut}
+ >
+);
+
+/**
+ * Navigation help component showing keyboard shortcuts.
+ */
+export const NavigationHelpDisplay = (): React.JSX.Element => (
+
+
+
+ {' '}
+
+ {' '}
+
+ {' '}
+
+ {' '}
+
+
+
+
+ {' '}
+
+ {' '}
+
+
+
+);
+
+/**
+ * Search input display component.
+ */
+export const SearchModeDisplay = ({
+ state,
+}: {
+ state: SessionBrowserState;
+}): React.JSX.Element => (
+
+ Search:
+ {state.searchQuery}
+ (Esc to cancel)
+
+);
+
+/**
+ * No results display component for empty search results.
+ */
+export const NoResultsDisplay = ({
+ state,
+}: {
+ state: SessionBrowserState;
+}): React.JSX.Element => (
+
+
+ No sessions found matching '{state.searchQuery}'.
+
+
+);
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx
new file mode 100644
index 0000000000..af7f1a6906
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from '../../../test-utils/render.js';
+import { describe, it, expect } from 'vitest';
+import {
+ SearchModeDisplay,
+ NavigationHelpDisplay,
+ NoResultsDisplay,
+} from './SessionBrowserNav.js';
+import { SessionListHeader } from './SessionListHeader.js';
+import type { SessionBrowserState } from '../SessionBrowser.js';
+
+describe('SessionBrowser Search and Navigation Components', () => {
+ it('SearchModeDisplay renders correctly with query', async () => {
+ const mockState = { searchQuery: 'test query' } as SessionBrowserState;
+ const { lastFrame, waitUntilReady } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('NavigationHelp renders correctly', async () => {
+ const { lastFrame, waitUntilReady } = render();
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('SessionListHeader renders correctly', async () => {
+ const mockState = {
+ totalSessions: 10,
+ searchQuery: '',
+ sortOrder: 'date',
+ sortReverse: false,
+ } as SessionBrowserState;
+ const { lastFrame, waitUntilReady } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('SessionListHeader renders correctly with filter', async () => {
+ const mockState = {
+ totalSessions: 5,
+ searchQuery: 'test',
+ sortOrder: 'name',
+ sortReverse: true,
+ } as SessionBrowserState;
+ const { lastFrame, waitUntilReady } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('NoResultsDisplay renders correctly', async () => {
+ const mockState = { searchQuery: 'no match' } as SessionBrowserState;
+ const { lastFrame, waitUntilReady } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+});
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx
new file mode 100644
index 0000000000..2b7fb79d40
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+import type { SessionBrowserState } from '../SessionBrowser.js';
+
+/**
+ * Header component showing session count and sort information.
+ */
+export const SessionListHeader = ({
+ state,
+}: {
+ state: SessionBrowserState;
+}): React.JSX.Element => (
+
+
+ Chat Sessions ({state.totalSessions} total
+ {state.searchQuery ? `, filtered` : ''})
+
+
+ sorted by {state.sortOrder} {state.sortReverse ? 'asc' : 'desc'}
+
+
+);
diff --git a/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap b/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap
new file mode 100644
index 0000000000..c5ed5e5454
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap
@@ -0,0 +1,29 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`SessionBrowser Search and Navigation Components > NavigationHelp renders correctly 1`] = `
+"Navigate: ↑/↓ Resume: Enter Search: / Delete: x Quit: q
+Sort: s Reverse: r First/Last: g/G
+"
+`;
+
+exports[`SessionBrowser Search and Navigation Components > NoResultsDisplay renders correctly 1`] = `
+"
+No sessions found matching 'no match'.
+"
+`;
+
+exports[`SessionBrowser Search and Navigation Components > SearchModeDisplay renders correctly with query 1`] = `
+"
+Search: test query (Esc to cancel)
+"
+`;
+
+exports[`SessionBrowser Search and Navigation Components > SessionListHeader renders correctly 1`] = `
+"Chat Sessions (10 total) sorted by date desc
+"
+`;
+
+exports[`SessionBrowser Search and Navigation Components > SessionListHeader renders correctly with filter 1`] = `
+"Chat Sessions (5 total, filtered) sorted by name asc
+"
+`;
diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx
index be99dfcc26..bc9249877c 100644
--- a/packages/cli/src/ui/components/SettingsDialog.test.tsx
+++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx
@@ -20,16 +20,14 @@
*
*/
-import { render } from '../../test-utils/render.js';
+import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SettingsDialog } from './SettingsDialog.js';
import { SettingScope } from '../../config/settings.js';
import { createMockSettings } from '../../test-utils/settings.js';
-import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
import { TEST_ONLY } from '../../utils/settingsUtils.js';
-import { SettingsContext } from '../contexts/SettingsContext.js';
import {
getSettingsSchema,
type SettingDefinition,
@@ -37,12 +35,6 @@ import {
} from '../../config/settingsSchema.js';
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
-vi.mock('../contexts/UIStateContext.js', () => ({
- useUIState: () => ({
- terminalWidth: 100, // Fixed width for consistent snapshots
- }),
-}));
-
enum TerminalKeys {
ENTER = '\u000D',
TAB = '\t',
@@ -52,6 +44,8 @@ enum TerminalKeys {
RIGHT_ARROW = '\u001B[C',
ESCAPE = '\u001B',
BACKSPACE = '\u0008',
+ CTRL_P = '\u0010',
+ CTRL_N = '\u000E',
}
vi.mock('../../config/settingsSchema.js', async (importOriginal) => {
@@ -94,7 +88,25 @@ const ENUM_SETTING: SettingDefinition = {
showInDialog: true,
};
+// Minimal general schema for KeypressProvider
+const MINIMAL_GENERAL_SCHEMA = {
+ general: {
+ showInDialog: false,
+ properties: {
+ debugKeystrokeLogging: {
+ type: 'boolean',
+ label: 'Debug Keystroke Logging',
+ category: 'General',
+ requiresRestart: false,
+ default: false,
+ showInDialog: false,
+ },
+ },
+ },
+};
+
const ENUM_FAKE_SCHEMA: SettingsSchemaType = {
+ ...MINIMAL_GENERAL_SCHEMA,
ui: {
showInDialog: false,
properties: {
@@ -106,6 +118,7 @@ const ENUM_FAKE_SCHEMA: SettingsSchemaType = {
} as unknown as SettingsSchemaType;
const ARRAY_FAKE_SCHEMA: SettingsSchemaType = {
+ ...MINIMAL_GENERAL_SCHEMA,
context: {
type: 'object',
label: 'Context',
@@ -162,6 +175,7 @@ const ARRAY_FAKE_SCHEMA: SettingsSchemaType = {
} as unknown as SettingsSchemaType;
const TOOLS_SHELL_FAKE_SCHEMA: SettingsSchemaType = {
+ ...MINIMAL_GENERAL_SCHEMA,
tools: {
type: 'object',
label: 'Tools',
@@ -222,16 +236,16 @@ const renderDialog = (
availableTerminalHeight?: number;
},
) =>
- render(
-
-
-
-
- ,
+ renderWithProviders(
+ ,
+ {
+ settings,
+ uiState: { terminalBackgroundColor: undefined },
+ },
);
describe('SettingsDialog', () => {
@@ -357,9 +371,9 @@ describe('SettingsDialog', () => {
up: TerminalKeys.UP_ARROW,
},
{
- name: 'vim keys (j/k)',
- down: 'j',
- up: 'k',
+ name: 'emacs keys (Ctrl+P/N)',
+ down: TerminalKeys.CTRL_N,
+ up: TerminalKeys.CTRL_P,
},
])('should navigate with $name', async ({ down, up }) => {
const settings = createMockSettings();
@@ -397,6 +411,31 @@ describe('SettingsDialog', () => {
unmount();
});
+ it('should allow j and k characters to be typed in search without triggering navigation', async () => {
+ const settings = createMockSettings();
+ const onSelect = vi.fn();
+ const { lastFrame, stdin, waitUntilReady, unmount } = renderDialog(
+ settings,
+ onSelect,
+ );
+ await waitUntilReady();
+
+ // Enter 'j' and 'k' in search
+ await act(async () => stdin.write('j'));
+ await waitUntilReady();
+ await act(async () => stdin.write('k'));
+ await waitUntilReady();
+
+ await waitFor(() => {
+ const frame = lastFrame();
+ // The search box should contain 'jk'
+ expect(frame).toContain('jk');
+ // Since 'jk' doesn't match any setting labels, it should say "No matches found."
+ expect(frame).toContain('No matches found.');
+ });
+ unmount();
+ });
+
it('wraps around when at the top of the list', async () => {
const settings = createMockSettings();
const onSelect = vi.fn();
@@ -1317,17 +1356,14 @@ describe('SettingsDialog', () => {
describe('String Settings Editing', () => {
it('should allow editing and committing a string setting', async () => {
- let settings = createMockSettings({
+ const settings = createMockSettings({
'general.sessionCleanup.maxAge': 'initial',
});
const onSelect = vi.fn();
- const { stdin, unmount, rerender, waitUntilReady } = render(
-
-
-
-
- ,
+ const { stdin, unmount, waitUntilReady } = renderWithProviders(
+ ,
+ { settings },
);
await waitUntilReady();
@@ -1357,20 +1393,15 @@ describe('SettingsDialog', () => {
});
await waitUntilReady();
- settings = createMockSettings({
- user: {
- settings: { 'general.sessionCleanup.maxAge': 'new value' },
- originalSettings: { 'general.sessionCleanup.maxAge': 'new value' },
- path: '',
- },
+ // Simulate the settings file being updated on disk
+ await act(async () => {
+ settings.setValue(
+ SettingScope.User,
+ 'general.sessionCleanup.maxAge',
+ 'new value',
+ );
});
- rerender(
-
-
-
-
- ,
- );
+ await waitUntilReady();
// Press Escape to exit
await act(async () => {
diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx
index 82965bda71..994bde6ed3 100644
--- a/packages/cli/src/ui/components/SettingsDialog.tsx
+++ b/packages/cli/src/ui/components/SettingsDialog.tsx
@@ -43,6 +43,8 @@ import {
BaseSettingsDialog,
type SettingsDialogItem,
} from './shared/BaseSettingsDialog.js';
+import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
+import { Command, KeyBinding } from '../key/keyBindings.js';
interface FzfResult {
item: string;
@@ -60,6 +62,11 @@ interface SettingsDialogProps {
const MAX_ITEMS_TO_SHOW = 8;
+const KEY_UP = new KeyBinding('up');
+const KEY_CTRL_P = new KeyBinding('ctrl+p');
+const KEY_DOWN = new KeyBinding('down');
+const KEY_CTRL_N = new KeyBinding('ctrl+n');
+
// Create a snapshot of the initial per-scope state of Restart Required Settings
// This creates a nested map of the form
// restartRequiredSetting -> Map { scopeName -> value }
@@ -336,6 +343,18 @@ export function SettingsDialog({
onSelect(undefined, selectedScope as SettingScope);
}, [onSelect, selectedScope]);
+ const globalKeyMatchers = useKeyMatchers();
+ const settingsKeyMatchers = useMemo(
+ () => ({
+ ...globalKeyMatchers,
+ [Command.DIALOG_NAVIGATION_UP]: (key: Key) =>
+ KEY_UP.matches(key) || KEY_CTRL_P.matches(key),
+ [Command.DIALOG_NAVIGATION_DOWN]: (key: Key) =>
+ KEY_DOWN.matches(key) || KEY_CTRL_N.matches(key),
+ }),
+ [globalKeyMatchers],
+ );
+
// Custom key handler for restart key
const handleKeyPress = useCallback(
(key: Key, _currentItem: SettingsDialogItem | undefined): boolean => {
@@ -371,6 +390,7 @@ export function SettingsDialog({
onItemClear={handleItemClear}
onClose={handleClose}
onKeyPress={handleKeyPress}
+ keyMatchers={settingsKeyMatchers}
footer={
showRestartPrompt
? {
diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx
index 77d072b02e..674263652f 100644
--- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx
+++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx
@@ -9,6 +9,7 @@ import { Box } from 'ink';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
import { StreamingState } from '../types.js';
import { renderWithProviders } from '../../test-utils/render.js';
+import { createMockSettings } from '../../test-utils/settings.js';
import { waitFor } from '../../test-utils/async.js';
import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core';
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
@@ -162,8 +163,11 @@ describe('ToolConfirmationQueue', () => {
/>
,
{
- config: mockConfig,
- useAlternateBuffer: true,
+ config: {
+ ...mockConfig,
+ getUseAlternateBuffer: () => true,
+ } as unknown as Config,
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
terminalWidth: 80,
terminalHeight: 20,
@@ -212,7 +216,7 @@ describe('ToolConfirmationQueue', () => {
/>,
{
config: mockConfig,
- useAlternateBuffer: false,
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: {
terminalWidth: 80,
terminalHeight: 40,
diff --git a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
index 06f509f1f6..30caf0fb40 100644
--- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap
@@ -201,3 +201,19 @@ README → (not answered)
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
"
`;
+
+exports[`AskUserDialog > verifies "All of the above" visual state with snapshot 1`] = `
+"Which features?
+(Select all that apply)
+
+ 1. [x] TypeScript
+ 2. [x] ESLint
+● 3. [x] All of the above
+ Select all options
+ 4. [ ] Enter a custom value
+ Done
+ Finish selection
+
+Enter to select · ↑/↓ to navigate · Esc to cancel
+"
+`;
diff --git a/packages/cli/src/ui/components/__snapshots__/Banner-Banner-handles-newlines-in-text.snap.svg b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-handles-newlines-in-text.snap.svg
new file mode 100644
index 0000000000..a6272e0fa9
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-handles-newlines-in-text.snap.svg
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-info-mode.snap.svg b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-info-mode.snap.svg
new file mode 100644
index 0000000000..89d219005d
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-info-mode.snap.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-multi-line-warning.snap.svg b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-multi-line-warning.snap.svg
new file mode 100644
index 0000000000..6b3250fc6b
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-multi-line-warning.snap.svg
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-warning-mode.snap.svg b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-warning-mode.snap.svg
new file mode 100644
index 0000000000..4f3ee74723
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/Banner-Banner-renders-in-warning-mode.snap.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap
index 7766c808ae..6df246dede 100644
--- a/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/Banner.test.tsx.snap
@@ -4,20 +4,25 @@ exports[`Banner > handles newlines in text 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Line 1 │
│ Line 2 │
-╰──────────────────────────────────────────────────────────────────────────────╯
-"
+╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Banner > renders in info mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Info Message │
-╰──────────────────────────────────────────────────────────────────────────────╯
-"
+╰──────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[`Banner > renders in multi-line warning 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ Title Line │
+│ Body Line 1 │
+│ Body Line 2 │
+╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Banner > renders in warning mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Warning Message │
-╰──────────────────────────────────────────────────────────────────────────────╯
-"
+╰──────────────────────────────────────────────────────────────────────────────╯"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/ChecklistItem.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ChecklistItem.test.tsx.snap
index 9cd5fbb64c..80599ae878 100644
--- a/packages/cli/src/ui/components/__snapshots__/ChecklistItem.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ChecklistItem.test.tsx.snap
@@ -1,5 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[` > renders { status: 'blocked', label: 'Blocked this' } item correctly 1`] = `
+"⛔ Blocked this
+"
+`;
+
exports[` > renders { status: 'cancelled', label: 'Skipped this' } item correctly 1`] = `
"✗ Skipped this
"
diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
index c0043bf6f9..785dc6b6f0 100644
--- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
@@ -6,11 +6,12 @@ AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ Shell Command Running a long command... │
│ │
+│ Line 9 │
│ Line 10 │
│ Line 11 │
│ Line 12 │
│ Line 13 │
-│ Line 14 │
+│ Line 14 █ │
│ Line 15 █ │
│ Line 16 █ │
│ Line 17 █ │
@@ -27,11 +28,12 @@ AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ Shell Command Running a long command... │
│ │
+│ Line 9 │
│ Line 10 │
│ Line 11 │
│ Line 12 │
│ Line 13 │
-│ Line 14 │
+│ Line 14 █ │
│ Line 15 █ │
│ Line 16 █ │
│ Line 17 █ │
@@ -47,7 +49,9 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Con
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ Shell Command Running a long command... │
│ │
-│ ... first 11 lines hidden (Ctrl+O to show) ... │
+│ ... first 9 lines hidden (Ctrl+O to show) ... │
+│ Line 10 │
+│ Line 11 │
│ Line 12 │
│ Line 13 │
│ Line 14 │
diff --git a/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap
index bac1f7af36..74dcb8a914 100644
--- a/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap
@@ -10,6 +10,8 @@ exports[`NewAgentsNotification > renders agent list 1`] = `
│ │ │ │
│ │ - Agent A: Description A │ │
│ │ - Agent B: Description B │ │
+ │ │ (Includes MCP servers: github, postgres) │ │
+ │ │ - Agent C: Description C │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
index 9063606146..63f412cb40 100644
--- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
@@ -6,6 +6,8 @@
import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { renderWithProviders } from '../../../test-utils/render.js';
+import { createMockSettings } from '../../../test-utils/settings.js';
+import { makeFakeConfig } from '@google/gemini-cli-core';
import { waitFor } from '../../../test-utils/async.js';
import { DiffRenderer } from './DiffRenderer.js';
import * as CodeColorizer from '../../utils/CodeColorizer.js';
@@ -42,7 +44,10 @@ index 0000000..e69de29
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() =>
expect(mockColorizeCode).toHaveBeenCalledWith({
@@ -74,7 +79,10 @@ index 0000000..e69de29
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() =>
expect(mockColorizeCode).toHaveBeenCalledWith({
@@ -102,7 +110,10 @@ index 0000000..e69de29
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() =>
expect(mockColorizeCode).toHaveBeenCalledWith({
@@ -135,7 +146,10 @@ index 0000001..0000002 100644
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
// colorizeCode is used internally by the line-by-line rendering, not for the whole block
await waitFor(() => expect(lastFrame()).toContain('new line'));
@@ -166,7 +180,10 @@ index 1234567..1234567 100644
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toBeDefined());
expect(lastFrame()).toMatchSnapshot();
@@ -178,7 +195,10 @@ index 1234567..1234567 100644
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toBeDefined());
expect(lastFrame()).toMatchSnapshot();
@@ -208,7 +228,10 @@ index 123..456 100644
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toContain('added line'));
expect(lastFrame()).toMatchSnapshot();
@@ -242,7 +265,10 @@ index abc..def 100644
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toContain('context line 15'));
expect(lastFrame()).toMatchSnapshot();
@@ -292,7 +318,10 @@ index 123..789 100644
availableTerminalHeight={height}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toContain('anotherNew'));
const output = lastFrame();
@@ -326,7 +355,10 @@ fileDiff Index: file.txt
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toContain('newVar'));
expect(lastFrame()).toMatchSnapshot();
@@ -353,7 +385,10 @@ fileDiff Index: Dockerfile
terminalWidth={80}
/>
,
- { useAlternateBuffer },
+ {
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
+ },
);
await waitFor(() => expect(lastFrame()).toContain('RUN npm run build'));
expect(lastFrame()).toMatchSnapshot();
diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
index b650ee4d9d..7ee726a609 100644
--- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
@@ -16,6 +16,8 @@ import {
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { renderWithProviders } from '../../../test-utils/render.js';
+import { createMockSettings } from '../../../test-utils/settings.js';
+import { makeFakeConfig } from '@google/gemini-cli-core';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SHELL_COMMAND_NAME, ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
@@ -48,14 +50,6 @@ describe('', () => {
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
};
- const renderShell = (
- props: Partial = {},
- options: Parameters[1] = {},
- ) =>
- renderWithProviders(, {
- uiActions,
- ...options,
- });
beforeEach(() => {
vi.clearAllMocks();
});
@@ -65,9 +59,9 @@ describe('', () => {
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
])('clicks inside the shell area sets focus for %s', async (_, name) => {
- const { lastFrame, simulateClick, unmount } = renderShell(
- { name },
- { mouseEventsEnabled: true },
+ const { lastFrame, simulateClick, unmount } = renderWithProviders(
+ ,
+ { uiActions, mouseEventsEnabled: true },
);
await waitFor(() => {
@@ -152,7 +146,8 @@ describe('', () => {
ptyId: 1,
},
{
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
embeddedShellFocused: true,
activePtyId: 1,
@@ -166,7 +161,8 @@ describe('', () => {
ptyId: 1,
},
{
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
embeddedShellFocused: false,
activePtyId: 1,
@@ -174,9 +170,9 @@ describe('', () => {
},
],
])('%s', async (_, props, options) => {
- const { lastFrame, waitUntilReady, unmount } = renderShell(
- props,
- options,
+ const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
+ ,
+ { uiActions, ...options },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -203,7 +199,7 @@ describe('', () => {
[
'uses full availableTerminalHeight when focused in alternate buffer mode',
100,
- 98, // 100 - 2
+ 98,
true,
false,
],
@@ -223,16 +219,19 @@ describe('', () => {
focused,
constrainHeight,
) => {
- const { lastFrame, waitUntilReady, unmount } = renderShell(
+ const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
+ ,
{
- resultDisplay: LONG_OUTPUT,
- renderOutputAsMarkdown: false,
- availableTerminalHeight,
- ptyId: 1,
- status: CoreToolCallStatus.Executing,
- },
- {
- useAlternateBuffer: true,
+ uiActions,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
activePtyId: focused ? 1 : 2,
embeddedShellFocused: focused,
@@ -250,14 +249,19 @@ describe('', () => {
);
it('fully expands in standard mode when availableTerminalHeight is undefined', async () => {
- const { lastFrame, unmount } = renderShell(
+ const { lastFrame, unmount } = renderWithProviders(
+ ,
{
- resultDisplay: LONG_OUTPUT,
- renderOutputAsMarkdown: false,
- availableTerminalHeight: undefined,
- status: CoreToolCallStatus.Executing,
+ uiActions,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
- { useAlternateBuffer: false },
);
await waitFor(() => {
@@ -269,16 +273,19 @@ describe('', () => {
});
it('fully expands in alternate buffer mode when constrainHeight is false and isExpandable is true', async () => {
- const { lastFrame, waitUntilReady, unmount } = renderShell(
+ const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
+ ,
{
- resultDisplay: LONG_OUTPUT,
- renderOutputAsMarkdown: false,
- availableTerminalHeight: undefined,
- status: CoreToolCallStatus.Success,
- isExpandable: true,
- },
- {
- useAlternateBuffer: true,
+ uiActions,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
constrainHeight: false,
},
@@ -296,16 +303,19 @@ describe('', () => {
});
it('stays constrained in alternate buffer mode when isExpandable is false even if constrainHeight is false', async () => {
- const { lastFrame, waitUntilReady, unmount } = renderShell(
+ const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
+ ,
{
- resultDisplay: LONG_OUTPUT,
- renderOutputAsMarkdown: false,
- availableTerminalHeight: undefined,
- status: CoreToolCallStatus.Success,
- isExpandable: false,
- },
- {
- useAlternateBuffer: true,
+ uiActions,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
uiState: {
constrainHeight: false,
},
diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx
index f34aa08bfb..f3694f3490 100644
--- a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx
@@ -42,33 +42,19 @@ export interface ShellToolMessageProps extends ToolMessageProps {
export const ShellToolMessage: React.FC = ({
name,
-
description,
-
resultDisplay,
-
status,
-
availableTerminalHeight,
-
terminalWidth,
-
emphasis = 'medium',
-
renderOutputAsMarkdown = true,
-
ptyId,
-
config,
-
isFirst,
-
borderColor,
-
borderDimColor,
-
isExpandable,
-
originalRequestName,
}) => {
const {
@@ -142,11 +128,9 @@ export const ShellToolMessage: React.FC = ({
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
const headerRef = React.useRef(null);
-
const contentRef = React.useRef(null);
// The shell is focusable if it's the shell command, it's executing, and the interactive shell is enabled.
-
const isThisShellFocusable = checkIsShellFocusable(name, status, config);
const handleFocus = () => {
@@ -156,7 +140,6 @@ export const ShellToolMessage: React.FC = ({
};
useMouseClick(headerRef, handleFocus, { isActive: !!isThisShellFocusable });
-
useMouseClick(contentRef, handleFocus, { isActive: !!isThisShellFocusable });
const { shouldShowFocusHint } = useFocusHint(
diff --git a/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx
new file mode 100644
index 0000000000..5af99541b5
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { waitFor } from '../../../test-utils/async.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
+import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
+import { Kind, CoreToolCallStatus } from '@google/gemini-cli-core';
+import type { IndividualToolCallDisplay } from '../../types.js';
+import { vi } from 'vitest';
+import { Text } from 'ink';
+
+vi.mock('../../utils/MarkdownDisplay.js', () => ({
+ MarkdownDisplay: ({ text }: { text: string }) => {text},
+}));
+
+describe('', () => {
+ const mockToolCalls: IndividualToolCallDisplay[] = [
+ {
+ callId: 'call-1',
+ name: 'agent_1',
+ description: 'Test agent 1',
+ confirmationDetails: undefined,
+ status: CoreToolCallStatus.Executing,
+ kind: Kind.Agent,
+ resultDisplay: {
+ isSubagentProgress: true,
+ agentName: 'api-monitor',
+ state: 'running',
+ recentActivity: [
+ {
+ id: 'act-1',
+ type: 'tool_call',
+ status: 'running',
+ content: '',
+ displayName: 'Action Required',
+ description: 'Verify server is running',
+ },
+ ],
+ },
+ },
+ {
+ callId: 'call-2',
+ name: 'agent_2',
+ description: 'Test agent 2',
+ confirmationDetails: undefined,
+ status: CoreToolCallStatus.Success,
+ kind: Kind.Agent,
+ resultDisplay: {
+ isSubagentProgress: true,
+ agentName: 'db-manager',
+ state: 'completed',
+ result: 'Database schema validated',
+ recentActivity: [
+ {
+ id: 'act-2',
+ type: 'thought',
+ status: 'completed',
+ content: 'Database schema validated',
+ },
+ ],
+ },
+ },
+ ];
+
+ const renderSubagentGroup = (
+ toolCallsToRender: IndividualToolCallDisplay[],
+ height?: number,
+ ) =>
+ renderWithProviders(
+ ,
+ );
+
+ it('renders nothing if there are no agent tool calls', async () => {
+ const { lastFrame } = renderSubagentGroup([], 40);
+ expect(lastFrame({ allowEmpty: true })).toBe('');
+ });
+
+ it('renders collapsed view by default with correct agent counts and states', async () => {
+ const { lastFrame, waitUntilReady } = renderSubagentGroup(
+ mockToolCalls,
+ 40,
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('expands when availableTerminalHeight is undefined', async () => {
+ const { lastFrame, rerender } = renderSubagentGroup(mockToolCalls, 40);
+
+ // Default collapsed view
+ await waitFor(() => {
+ expect(lastFrame()).toContain('(ctrl+o to expand)');
+ });
+
+ // Expand view
+ rerender(
+ ,
+ );
+ await waitFor(() => {
+ expect(lastFrame()).toContain('(ctrl+o to collapse)');
+ });
+
+ // Collapse view
+ rerender(
+ ,
+ );
+ await waitFor(() => {
+ expect(lastFrame()).toContain('(ctrl+o to expand)');
+ });
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx
new file mode 100644
index 0000000000..2d3f8a44c8
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx
@@ -0,0 +1,269 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { useEffect, useId } from 'react';
+import { Box, Text } from 'ink';
+import { theme } from '../../semantic-colors.js';
+import type { IndividualToolCallDisplay } from '../../types.js';
+import {
+ isSubagentProgress,
+ checkExhaustive,
+ type SubagentActivityItem,
+} from '@google/gemini-cli-core';
+import {
+ SubagentProgressDisplay,
+ formatToolArgs,
+} from './SubagentProgressDisplay.js';
+import { useOverflowActions } from '../../contexts/OverflowContext.js';
+
+export interface SubagentGroupDisplayProps {
+ toolCalls: IndividualToolCallDisplay[];
+ availableTerminalHeight?: number;
+ terminalWidth: number;
+ borderColor?: string;
+ borderDimColor?: boolean;
+ isFirst?: boolean;
+ isExpandable?: boolean;
+}
+
+export const SubagentGroupDisplay: React.FC = ({
+ toolCalls,
+ availableTerminalHeight,
+ terminalWidth,
+ borderColor,
+ borderDimColor,
+ isFirst,
+ isExpandable = true,
+}) => {
+ const isExpanded = availableTerminalHeight === undefined;
+ const overflowActions = useOverflowActions();
+ const uniqueId = useId();
+ const overflowId = `subagent-${uniqueId}`;
+
+ useEffect(() => {
+ if (isExpandable && overflowActions) {
+ // Register with the global overflow system so "ctrl+o to expand" shows in the sticky footer
+ // and AppContainer passes the shortcut through.
+ overflowActions.addOverflowingId(overflowId);
+ }
+ return () => {
+ if (overflowActions) {
+ overflowActions.removeOverflowingId(overflowId);
+ }
+ };
+ }, [isExpandable, overflowActions, overflowId]);
+
+ if (toolCalls.length === 0) {
+ return null;
+ }
+
+ let headerText = '';
+ if (toolCalls.length === 1) {
+ const singleAgent = toolCalls[0].resultDisplay;
+ if (isSubagentProgress(singleAgent)) {
+ switch (singleAgent.state) {
+ case 'completed':
+ headerText = 'Agent Completed';
+ break;
+ case 'cancelled':
+ headerText = 'Agent Cancelled';
+ break;
+ case 'error':
+ headerText = 'Agent Error';
+ break;
+ default:
+ headerText = 'Running Agent...';
+ break;
+ }
+ } else {
+ headerText = 'Running Agent...';
+ }
+ } else {
+ let completedCount = 0;
+ let runningCount = 0;
+ for (const tc of toolCalls) {
+ const progress = tc.resultDisplay;
+ if (isSubagentProgress(progress)) {
+ if (progress.state === 'completed') completedCount++;
+ else if (progress.state === 'running') runningCount++;
+ } else {
+ // It hasn't emitted progress yet, but it is "running"
+ runningCount++;
+ }
+ }
+
+ if (completedCount === toolCalls.length) {
+ headerText = `${toolCalls.length} Agents Completed`;
+ } else if (completedCount > 0) {
+ headerText = `${toolCalls.length} Agents (${runningCount} running, ${completedCount} completed)...`;
+ } else {
+ headerText = `Running ${toolCalls.length} Agents...`;
+ }
+ }
+ const toggleText = `(ctrl+o to ${isExpanded ? 'collapse' : 'expand'})`;
+
+ const renderCollapsedRow = (
+ key: string,
+ agentName: string,
+ icon: React.ReactNode,
+ content: string,
+ displayArgs?: string,
+ ) => (
+
+
+ {icon}
+
+
+
+ {agentName}
+
+
+
+ ·
+
+
+
+ {content}
+ {displayArgs && ` ${displayArgs}`}
+
+
+
+ );
+
+ return (
+
+
+ ≡
+
+ {headerText}
+
+ {isExpandable && {toggleText}}
+
+
+ {toolCalls.map((toolCall) => {
+ const progress = toolCall.resultDisplay;
+
+ if (!isSubagentProgress(progress)) {
+ const agentName = toolCall.name || 'agent';
+ if (!isExpanded) {
+ return renderCollapsedRow(
+ toolCall.callId,
+ agentName,
+ !,
+ 'Starting...',
+ );
+ } else {
+ return (
+
+
+ !
+
+ {agentName}
+
+
+
+ Starting...
+
+
+ );
+ }
+ }
+
+ const lastActivity: SubagentActivityItem | undefined =
+ progress.recentActivity[progress.recentActivity.length - 1];
+
+ // Collapsed View: Show single compact line per agent
+ if (!isExpanded) {
+ let content = 'Starting...';
+ let formattedArgs: string | undefined;
+
+ if (progress.state === 'completed') {
+ if (
+ progress.terminateReason &&
+ progress.terminateReason !== 'GOAL'
+ ) {
+ content = `Finished Early (${progress.terminateReason})`;
+ } else {
+ content = 'Completed successfully';
+ }
+ } else if (lastActivity) {
+ // Match expanded view logic exactly:
+ // Primary text: displayName || content
+ content = lastActivity.displayName || lastActivity.content;
+
+ // Secondary text: description || formatToolArgs(args)
+ if (lastActivity.description) {
+ formattedArgs = lastActivity.description;
+ } else if (lastActivity.type === 'tool_call' && lastActivity.args) {
+ formattedArgs = formatToolArgs(lastActivity.args);
+ }
+ }
+
+ const displayArgs =
+ progress.state === 'completed' ? '' : formattedArgs;
+
+ const renderStatusIcon = () => {
+ const state = progress.state ?? 'running';
+ switch (state) {
+ case 'running':
+ return !;
+ case 'completed':
+ return ✓;
+ case 'cancelled':
+ return ℹ;
+ case 'error':
+ return ✗;
+ default:
+ return checkExhaustive(state);
+ }
+ };
+
+ return renderCollapsedRow(
+ toolCall.callId,
+ progress.agentName,
+ renderStatusIcon(),
+ lastActivity?.type === 'thought' ? `💭 ${content}` : content,
+ displayArgs,
+ );
+ }
+
+ // Expanded View: Render full history
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx
index e8b67301ad..f2c57f9662 100644
--- a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx
+++ b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx
@@ -36,7 +36,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -60,7 +60,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -82,7 +82,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -104,7 +104,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -128,7 +128,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -149,7 +149,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -164,7 +164,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -185,7 +185,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
diff --git a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx
index b34a904b3e..5d1086c759 100644
--- a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx
+++ b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx
@@ -8,18 +8,21 @@ import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import Spinner from 'ink-spinner';
+import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import type {
SubagentProgress,
SubagentActivityItem,
} from '@google/gemini-cli-core';
import { TOOL_STATUS } from '../../constants.js';
import { STATUS_INDICATOR_WIDTH } from './ToolShared.js';
+import { safeJsonToMarkdown } from '@google/gemini-cli-core';
export interface SubagentProgressDisplayProps {
progress: SubagentProgress;
+ terminalWidth: number;
}
-const formatToolArgs = (args?: string): string => {
+export const formatToolArgs = (args?: string): string => {
if (!args) return '';
try {
const parsed: unknown = JSON.parse(args);
@@ -54,7 +57,7 @@ const formatToolArgs = (args?: string): string => {
export const SubagentProgressDisplay: React.FC<
SubagentProgressDisplayProps
-> = ({ progress }) => {
+> = ({ progress, terminalWidth }) => {
let headerText: string | undefined;
let headerColor = theme.text.secondary;
@@ -67,6 +70,9 @@ export const SubagentProgressDisplay: React.FC<
} else if (progress.state === 'completed') {
headerText = `Subagent ${progress.agentName} completed.`;
headerColor = theme.status.success;
+ } else {
+ headerText = `Running subagent ${progress.agentName}...`;
+ headerColor = theme.text.primary;
}
return (
@@ -146,6 +152,23 @@ export const SubagentProgressDisplay: React.FC<
return null;
})}
+
+ {progress.state === 'completed' && progress.result && (
+
+ {progress.terminateReason && progress.terminateReason !== 'GOAL' && (
+
+
+ Agent Finished Early ({progress.terminateReason})
+
+
+ )}
+
+
+ )}
);
};
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
index 92c8b5743c..24332e83c2 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
@@ -4,16 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { describe, it, expect, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
-import type {
- SerializableConfirmationDetails,
- ToolCallConfirmationDetails,
- Config,
+import {
+ type SerializableConfirmationDetails,
+ type ToolCallConfirmationDetails,
+ type Config,
+ ToolConfirmationOutcome,
} from '@google/gemini-cli-core';
import { renderWithProviders } from '../../../test-utils/render.js';
import { createMockSettings } from '../../../test-utils/settings.js';
import { useToolActions } from '../../contexts/ToolActionsContext.js';
+import { act } from 'react';
vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => {
const actual =
@@ -646,4 +648,63 @@ describe('ToolConfirmationMessage', () => {
expect(output).not.toContain('Invocation Arguments:');
unmount();
});
+
+ describe('ESCAPE key behavior', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.restoreAllMocks();
+ });
+
+ it('should call confirm(Cancel) asynchronously via useEffect when ESC is pressed', async () => {
+ const mockConfirm = vi.fn().mockResolvedValue(undefined);
+
+ vi.mocked(useToolActions).mockReturnValue({
+ confirm: mockConfirm,
+ cancel: vi.fn(),
+ isDiffingEnabled: false,
+ });
+
+ const confirmationDetails: SerializableConfirmationDetails = {
+ type: 'info',
+ title: 'Confirm Web Fetch',
+ prompt: 'https://example.com',
+ urls: ['https://example.com'],
+ };
+
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
+ );
+ await waitUntilReady();
+
+ stdin.write('\x1b');
+
+ // To assert that the confirmation happens asynchronously (via useEffect) rather than
+ // synchronously (directly inside the keystroke handler), we must run our assertion
+ // *inside* the act() block.
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ expect(mockConfirm).not.toHaveBeenCalled();
+ });
+
+ // Now that the act() block has returned, React flushes the useEffect, calling handleConfirm.
+ expect(mockConfirm).toHaveBeenCalledWith(
+ 'test-call-id',
+ ToolConfirmationOutcome.Cancel,
+ undefined,
+ );
+
+ unmount();
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 2e9e133a35..45584a9d46 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -5,7 +5,7 @@
*/
import type React from 'react';
-import { useMemo, useCallback, useState } from 'react';
+import { useEffect, useMemo, useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { DiffRenderer } from './DiffRenderer.js';
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
@@ -79,6 +79,7 @@ export const ToolConfirmationMessage: React.FC<
callId,
expanded: false,
});
+ const [isCancelling, setIsCancelling] = useState(false);
const isMcpToolDetailsExpanded =
mcpDetailsExpansionState.callId === callId
? mcpDetailsExpansionState.expanded
@@ -183,7 +184,7 @@ export const ToolConfirmationMessage: React.FC<
return true;
}
if (keyMatchers[Command.ESCAPE](key)) {
- handleConfirm(ToolConfirmationOutcome.Cancel);
+ setIsCancelling(true);
return true;
}
if (keyMatchers[Command.QUIT](key)) {
@@ -196,6 +197,20 @@ export const ToolConfirmationMessage: React.FC<
{ isActive: isFocused, priority: true },
);
+ // TODO(#23009): Remove this hack once we migrate to the new renderer.
+ // Why useEffect is used here instead of calling handleConfirm directly:
+ // There is a race condition where calling handleConfirm immediately upon
+ // keypress removes the tool UI component while the UI is in an expanded state.
+ // This simultaneously triggers setConstrainHeight, causing render two footers.
+ // By bridging the cancel action through state (isCancelling) and this useEffect,
+ // we delay handleConfirm until the next render cycle, ensuring setConstrainHeight
+ // resolves properly first.
+ useEffect(() => {
+ if (isCancelling) {
+ handleConfirm(ToolConfirmationOutcome.Cancel);
+ }
+ }, [isCancelling, handleConfirm]);
+
const handleSelect = useCallback(
(item: ToolConfirmationOutcome) => handleConfirm(item),
[handleConfirm],
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
index eff418a609..e647c1aec6 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
@@ -65,14 +65,10 @@ describe('', () => {
enableInteractiveShell: true,
});
const fullVerbositySettings = createMockSettings({
- merged: {
- ui: { errorVerbosity: 'full' },
- },
+ ui: { errorVerbosity: 'full' },
});
const lowVerbositySettings = createMockSettings({
- merged: {
- ui: { errorVerbosity: 'low' },
- },
+ ui: { errorVerbosity: 'low' },
});
describe('Golden Snapshots', () => {
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index ee3a98930f..69da3a1029 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -15,12 +15,14 @@ import type {
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
+import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import { isShellTool } from './ToolShared.js';
import {
shouldHideToolCall,
CoreToolCallStatus,
+ Kind,
} from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
@@ -125,12 +127,36 @@ export const ToolGroupMessage: React.FC = ({
let countToolCallsWithResults = 0;
for (const tool of visibleToolCalls) {
- if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
+ if (
+ tool.kind !== Kind.Agent &&
+ tool.resultDisplay !== undefined &&
+ tool.resultDisplay !== ''
+ ) {
countToolCallsWithResults++;
}
}
const countOneLineToolCalls =
- visibleToolCalls.length - countToolCallsWithResults;
+ visibleToolCalls.filter((t) => t.kind !== Kind.Agent).length -
+ countToolCallsWithResults;
+ const groupedTools = useMemo(() => {
+ const groups: Array<
+ IndividualToolCallDisplay | IndividualToolCallDisplay[]
+ > = [];
+ for (const tool of visibleToolCalls) {
+ if (tool.kind === Kind.Agent) {
+ const lastGroup = groups[groups.length - 1];
+ if (Array.isArray(lastGroup)) {
+ lastGroup.push(tool);
+ } else {
+ groups.push([tool]);
+ }
+ } else {
+ groups.push(tool);
+ }
+ }
+ return groups;
+ }, [visibleToolCalls]);
+
const availableTerminalHeightPerToolMessage = availableTerminalHeight
? Math.max(
Math.floor(
@@ -167,8 +193,29 @@ export const ToolGroupMessage: React.FC = ({
width={terminalWidth}
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
>
- {visibleToolCalls.map((tool, index) => {
+ {groupedTools.map((group, index) => {
const isFirst = index === 0;
+ const resolvedIsFirst =
+ borderTopOverride !== undefined
+ ? borderTopOverride && isFirst
+ : isFirst;
+
+ if (Array.isArray(group)) {
+ return (
+
+ );
+ }
+
+ const tool = group;
const isShellToolCall = isShellTool(tool.name);
const commonProps = {
@@ -176,10 +223,7 @@ export const ToolGroupMessage: React.FC = ({
availableTerminalHeight: availableTerminalHeightPerToolMessage,
terminalWidth: contentWidth,
emphasis: 'medium' as const,
- isFirst:
- borderTopOverride !== undefined
- ? borderTopOverride && isFirst
- : isFirst,
+ isFirst: resolvedIsFirst,
borderColor,
borderDimColor,
isExpandable,
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
index e3869b6e1b..b61a825056 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
@@ -13,8 +13,10 @@ import {
type AnsiOutput,
CoreToolCallStatus,
Kind,
+ makeFakeConfig,
} from '@google/gemini-cli-core';
import { renderWithProviders } from '../../../test-utils/render.js';
+import { createMockSettings } from '../../../test-utils/settings.js';
import { tryParseJSON } from '../../../utils/jsonoutput.js';
vi.mock('../GeminiRespondingSpinner.js', () => ({
@@ -462,7 +464,8 @@ describe('', () => {
constrainHeight: true,
},
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
@@ -495,7 +498,8 @@ describe('', () => {
uiActions,
uiState: { streamingState: StreamingState.Idle },
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
@@ -523,7 +527,8 @@ describe('', () => {
uiActions,
uiState: { streamingState: StreamingState.Idle },
width: 80,
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx b/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx
index 2375be7f0e..6057b00d35 100644
--- a/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx
@@ -5,11 +5,12 @@
*/
import { describe, it, expect } from 'vitest';
-import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
+import { type ToolMessageProps, ToolMessage } from './ToolMessage.js';
import { StreamingState } from '../../types.js';
import { StreamingContext } from '../../contexts/StreamingContext.js';
import { renderWithProviders } from '../../../test-utils/render.js';
-import { CoreToolCallStatus } from '@google/gemini-cli-core';
+import { createMockSettings } from '../../../test-utils/settings.js';
+import { CoreToolCallStatus, makeFakeConfig } from '@google/gemini-cli-core';
describe(' - Raw Markdown Display Snapshots', () => {
const baseProps: ToolMessageProps = {
@@ -72,7 +73,8 @@ describe(' - Raw Markdown Display Snapshots', () => {
,
{
uiState: { renderMarkdown, streamingState: StreamingState.Idle },
- useAlternateBuffer,
+ config: makeFakeConfig({ useAlternateBuffer }),
+ settings: createMockSettings({ ui: { useAlternateBuffer } }),
},
);
await waitUntilReady();
diff --git a/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx b/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx
index 20b8d13459..a9fffde8e2 100644
--- a/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx
@@ -7,9 +7,10 @@
import { describe, it, expect } from 'vitest';
import { ToolGroupMessage } from './ToolGroupMessage.js';
import { renderWithProviders } from '../../../test-utils/render.js';
+import { createMockSettings } from '../../../test-utils/settings.js';
import { StreamingState, type IndividualToolCallDisplay } from '../../types.js';
import { waitFor } from '../../../test-utils/async.js';
-import { CoreToolCallStatus } from '@google/gemini-cli-core';
+import { CoreToolCallStatus, makeFakeConfig } from '@google/gemini-cli-core';
import { useOverflowState } from '../../contexts/OverflowContext.js';
describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay synchronization', () => {
@@ -56,7 +57,8 @@ describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay
streamingState: StreamingState.Idle,
constrainHeight: true,
},
- useAlternateBuffer: true,
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -106,7 +108,8 @@ describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay
streamingState: StreamingState.Idle,
constrainHeight: true,
},
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
index 02f466e72f..4f2dfb74f7 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
@@ -5,9 +5,10 @@
*/
import { renderWithProviders } from '../../../test-utils/render.js';
+import { createMockSettings } from '../../../test-utils/settings.js';
import { ToolResultDisplay } from './ToolResultDisplay.js';
import { describe, it, expect, vi } from 'vitest';
-import type { AnsiOutput } from '@google/gemini-cli-core';
+import { makeFakeConfig, type AnsiOutput } from '@google/gemini-cli-core';
describe('ToolResultDisplay', () => {
beforeEach(() => {
@@ -36,7 +37,10 @@ describe('ToolResultDisplay', () => {
terminalWidth={80}
maxLines={10}
/>,
- { useAlternateBuffer: true },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
+ },
);
await waitUntilReady();
const output = lastFrame();
@@ -52,7 +56,10 @@ describe('ToolResultDisplay', () => {
terminalWidth={80}
maxLines={10}
/>,
- { useAlternateBuffer: true },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
+ },
);
await waitUntilReady();
const output = lastFrame();
@@ -69,7 +76,10 @@ describe('ToolResultDisplay', () => {
terminalWidth={80}
hasFocus={true}
/>,
- { useAlternateBuffer: true },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
+ },
);
await waitUntilReady();
@@ -80,7 +90,10 @@ describe('ToolResultDisplay', () => {
it('renders string result as markdown by default', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
,
- { useAlternateBuffer: false },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
+ },
);
await waitUntilReady();
const output = lastFrame();
@@ -98,7 +111,8 @@ describe('ToolResultDisplay', () => {
renderOutputAsMarkdown={false}
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
@@ -118,7 +132,8 @@ describe('ToolResultDisplay', () => {
availableTerminalHeight={20}
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
@@ -140,7 +155,10 @@ describe('ToolResultDisplay', () => {
terminalWidth={80}
availableTerminalHeight={20}
/>,
- { useAlternateBuffer: false },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
+ },
);
await waitUntilReady();
const output = lastFrame();
@@ -170,7 +188,10 @@ describe('ToolResultDisplay', () => {
terminalWidth={80}
availableTerminalHeight={20}
/>,
- { useAlternateBuffer: false },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
+ },
);
await waitUntilReady();
const output = lastFrame();
@@ -189,7 +210,10 @@ describe('ToolResultDisplay', () => {
terminalWidth={80}
availableTerminalHeight={20}
/>,
- { useAlternateBuffer: false },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
+ },
);
await waitUntilReady();
const output = lastFrame({ allowEmpty: true });
@@ -208,7 +232,8 @@ describe('ToolResultDisplay', () => {
renderOutputAsMarkdown={true}
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
@@ -226,7 +251,10 @@ describe('ToolResultDisplay', () => {
availableTerminalHeight={20}
renderOutputAsMarkdown={true}
/>,
- { useAlternateBuffer: true },
+ {
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
+ },
);
await waitUntilReady();
const output = lastFrame();
@@ -306,7 +334,8 @@ describe('ToolResultDisplay', () => {
maxLines={3}
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
@@ -342,7 +371,8 @@ describe('ToolResultDisplay', () => {
availableTerminalHeight={undefined}
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
index 0bbe3446e0..3b7cfaa8da 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
@@ -102,7 +102,12 @@ export const ToolResultDisplay: React.FC = ({
);
} else if (isSubagentProgress(contentData)) {
- content = ;
+ content = (
+
+ );
} else if (typeof contentData === 'string' && renderOutputAsMarkdown) {
content = (
{
it('shows the head of the content when overflowDirection is bottom (string)', async () => {
@@ -20,7 +21,8 @@ describe('ToolResultDisplay Overflow', () => {
overflowDirection="bottom"
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
@@ -46,7 +48,8 @@ describe('ToolResultDisplay Overflow', () => {
overflowDirection="top"
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
@@ -83,7 +86,8 @@ describe('ToolResultDisplay Overflow', () => {
overflowDirection="bottom"
/>,
{
- useAlternateBuffer: false,
+ config: makeFakeConfig({ useAlternateBuffer: false }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
uiState: { constrainHeight: true },
},
);
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/SubagentGroupDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/SubagentGroupDisplay.test.tsx.snap
new file mode 100644
index 0000000000..f672988909
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/__snapshots__/SubagentGroupDisplay.test.tsx.snap
@@ -0,0 +1,9 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[` > renders collapsed view by default with correct agent counts and states 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ ≡ 2 Agents (1 running, 1 completed)... (ctrl+o to expand) │
+│ ! api-monitor · Action Required Verify server is running │
+│ ✓ db-manager · 💭 Completed successfully │
+"
+`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap
index 8a4c5bd4c4..2d31c9c652 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap
@@ -1,7 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[` > renders "Request cancelled." with the info icon 1`] = `
-"ℹ Request cancelled.
+"Running subagent TestAgent...
+
+ℹ Request cancelled.
"
`;
@@ -11,31 +13,43 @@ exports[` > renders cancelled state correctly 1`] = `
`;
exports[` > renders correctly with command fallback 1`] = `
-"⠋ run_shell_command echo hello
+"Running subagent TestAgent...
+
+⠋ run_shell_command echo hello
"
`;
exports[` > renders correctly with description in args 1`] = `
-"⠋ run_shell_command Say hello
+"Running subagent TestAgent...
+
+⠋ run_shell_command Say hello
"
`;
exports[` > renders correctly with displayName and description from item 1`] = `
-"⠋ RunShellCommand Executing echo hello
+"Running subagent TestAgent...
+
+⠋ RunShellCommand Executing echo hello
"
`;
exports[` > renders correctly with file_path 1`] = `
-"✓ write_file /tmp/test.txt
+"Running subagent TestAgent...
+
+✓ write_file /tmp/test.txt
"
`;
exports[` > renders thought bubbles correctly 1`] = `
-"💭 Thinking about life
+"Running subagent TestAgent...
+
+💭 Thinking about life
"
`;
exports[` > truncates long args 1`] = `
-"⠋ run_shell_command This is a very long description that should definitely be tr...
+"Running subagent TestAgent...
+
+⠋ run_shell_command This is a very long description that should definitely be tr...
"
`;
diff --git a/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx b/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx
index 1ac701eff1..ebabe87133 100644
--- a/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx
+++ b/packages/cli/src/ui/components/shared/BaseSettingsDialog.test.tsx
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render } from '../../../test-utils/render.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { act } from 'react';
@@ -14,15 +14,8 @@ import {
type BaseSettingsDialogProps,
type SettingsDialogItem,
} from './BaseSettingsDialog.js';
-import { KeypressProvider } from '../../contexts/KeypressContext.js';
import { SettingScope } from '../../../config/settings.js';
-vi.mock('../../contexts/UIStateContext.js', () => ({
- useUIState: () => ({
- mainAreaWidth: 100,
- }),
-}));
-
enum TerminalKeys {
ENTER = '\u000D',
TAB = '\t',
@@ -115,10 +108,8 @@ describe('BaseSettingsDialog', () => {
...props,
};
- const result = render(
-
-
- ,
+ const result = renderWithProviders(
+ ,
);
await result.waitUntilReady();
return result;
@@ -331,22 +322,18 @@ describe('BaseSettingsDialog', () => {
const filteredItems = [items[0], items[2], items[4]];
await act(async () => {
rerender(
-
-
- ,
+ ,
);
});
- await waitUntilReady();
-
// Verify the dialog hasn't crashed and the items are displayed
await waitFor(() => {
const frame = lastFrame();
@@ -391,22 +378,18 @@ describe('BaseSettingsDialog', () => {
const filteredItems = [items[0], items[1]];
await act(async () => {
rerender(
-
-
- ,
+ ,
);
});
- await waitUntilReady();
-
await waitFor(() => {
const frame = lastFrame();
expect(frame).toContain('Boolean Setting');
diff --git a/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx b/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx
index d96646e8a5..804633fe15 100644
--- a/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx
+++ b/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx
@@ -19,7 +19,7 @@ import { TextInput } from './TextInput.js';
import type { TextBuffer } from './text-buffer.js';
import { cpSlice, cpLen, cpIndexToOffset } from '../../utils/textUtils.js';
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
-import { Command } from '../../key/keyMatchers.js';
+import { Command, type KeyMatchers } from '../../key/keyMatchers.js';
import { useSettingsNavigation } from '../../hooks/useSettingsNavigation.js';
import { useInlineEditBuffer } from '../../hooks/useInlineEditBuffer.js';
import { formatCommand } from '../../key/keybindingUtils.js';
@@ -103,6 +103,9 @@ export interface BaseSettingsDialogProps {
currentItem: SettingsDialogItem | undefined,
) => boolean;
+ /** Optional override for key matchers used for navigation. */
+ keyMatchers?: KeyMatchers;
+
/** Available terminal height for dynamic windowing */
availableHeight?: number;
@@ -134,10 +137,12 @@ export function BaseSettingsDialog({
onItemClear,
onClose,
onKeyPress,
+ keyMatchers: customKeyMatchers,
availableHeight,
footer,
}: BaseSettingsDialogProps): React.JSX.Element {
- const keyMatchers = useKeyMatchers();
+ const globalKeyMatchers = useKeyMatchers();
+ const keyMatchers = customKeyMatchers ?? globalKeyMatchers;
// Calculate effective max items and scope visibility based on terminal height
const { effectiveMaxItemsToShow, finalShowScopeSelector } = useMemo(() => {
const initialShowScope = showScopeSelector;
diff --git a/packages/cli/src/ui/components/shared/ScrollableList.test.tsx b/packages/cli/src/ui/components/shared/ScrollableList.test.tsx
index 1dd72b89a2..2a1182a5f3 100644
--- a/packages/cli/src/ui/components/shared/ScrollableList.test.tsx
+++ b/packages/cli/src/ui/components/shared/ScrollableList.test.tsx
@@ -5,21 +5,12 @@
*/
import { useState, useEffect, useRef, act } from 'react';
-import { render } from '../../../test-utils/render.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
import { Box, Text } from 'ink';
import { ScrollableList, type ScrollableListRef } from './ScrollableList.js';
-import { ScrollProvider } from '../../contexts/ScrollProvider.js';
-import { KeypressProvider } from '../../contexts/KeypressContext.js';
-import { MouseProvider } from '../../contexts/MouseContext.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { waitFor } from '../../../test-utils/async.js';
-vi.mock('../../contexts/UIStateContext.js', () => ({
- useUIState: vi.fn(() => ({
- copyModeEnabled: false,
- })),
-}));
-
// Mock useStdout to provide a fixed size for testing
vi.mock('ink', async (importOriginal) => {
const actual = await importOriginal();
@@ -85,51 +76,45 @@ const TestComponent = ({
}, [onRef]);
return (
-
-
-
-
-
- (
-
+
+
+ (
+
+
+ {item.title}
- {item.title}
-
-
- }
- >
- {item.title}
-
- {getLorem(index)}
+ borderStyle="single"
+ borderTop={true}
+ borderBottom={false}
+ borderLeft={false}
+ borderRight={false}
+ borderColor="gray"
+ />
- )}
- estimatedItemHeight={() => 14}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- initialScrollIndex={Number.MAX_SAFE_INTEGER}
- />
+ }
+ >
+ {item.title}
+
+ {getLorem(index)}
- Count: {items.length}
-
-
-
-
+ )}
+ estimatedItemHeight={() => 14}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ initialScrollIndex={Number.MAX_SAFE_INTEGER}
+ />
+
+ Count: {items.length}
+
);
};
describe('ScrollableList Demo Behavior', () => {
@@ -147,10 +132,10 @@ describe('ScrollableList Demo Behavior', () => {
let lastFrame: (options?: { allowEmpty?: boolean }) => string | undefined;
let waitUntilReady: () => Promise;
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render(
+ result = renderWithProviders(
{
addItem = add;
@@ -230,45 +215,39 @@ describe('ScrollableList Demo Behavior', () => {
}, []);
return (
-
-
-
-
- (
-
- {index === 0 ? (
- [STICKY] {item.title}}
- >
- [Normal] {item.title}
-
- ) : (
- [Normal] {item.title}
- )}
- Content for {item.title}
- More content for {item.title}
-
- )}
- estimatedItemHeight={() => 3}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- />
+
+ (
+
+ {index === 0 ? (
+ [STICKY] {item.title}}
+ >
+ [Normal] {item.title}
+
+ ) : (
+ [Normal] {item.title}
+ )}
+ Content for {item.title}
+ More content for {item.title}
-
-
-
+ )}
+ estimatedItemHeight={() => 3}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ />
+
);
};
let lastFrame: () => string | undefined;
let waitUntilReady: () => Promise;
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render();
+ result = renderWithProviders();
lastFrame = result.lastFrame;
waitUntilReady = result.waitUntilReady;
});
@@ -334,27 +313,21 @@ describe('ScrollableList Demo Behavior', () => {
title: `Item ${i}`,
}));
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render(
-
-
-
-
- {
- listRef = ref;
- }}
- data={items}
- renderItem={({ item }) => {item.title}}
- estimatedItemHeight={() => 1}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- />
-
-
-
- ,
+ result = renderWithProviders(
+
+ {
+ listRef = ref;
+ }}
+ data={items}
+ renderItem={({ item }) => {item.title}}
+ estimatedItemHeight={() => 1}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ />
+ ,
);
lastFrame = result.lastFrame;
stdin = result.stdin;
@@ -444,25 +417,19 @@ describe('ScrollableList Demo Behavior', () => {
let lastFrame: (options?: { allowEmpty?: boolean }) => string | undefined;
let waitUntilReady: () => Promise;
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render(
-
-
-
-
- {item.title}}
- estimatedItemHeight={() => 1}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- width={50}
- />
-
-
-
- ,
+ result = renderWithProviders(
+
+ {item.title}}
+ estimatedItemHeight={() => 1}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ width={50}
+ />
+ ,
);
lastFrame = result.lastFrame;
waitUntilReady = result.waitUntilReady;
@@ -497,31 +464,25 @@ describe('ScrollableList Demo Behavior', () => {
}, []);
return (
-
-
-
-
- {
- listRef = ref;
- }}
- data={items}
- renderItem={({ item }) => {item.title}}
- estimatedItemHeight={() => 1}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- initialScrollIndex={Number.MAX_SAFE_INTEGER}
- />
-
-
-
-
+
+ {
+ listRef = ref;
+ }}
+ data={items}
+ renderItem={({ item }) => {item.title}}
+ estimatedItemHeight={() => 1}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ initialScrollIndex={Number.MAX_SAFE_INTEGER}
+ />
+
);
};
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render();
+ result = renderWithProviders();
});
await result!.waitUntilReady();
@@ -622,33 +583,27 @@ describe('ScrollableList Demo Behavior', () => {
);
return (
-
-
-
-
- {
- listRef = ref;
- }}
- data={items}
- renderItem={({ item, index }) => (
-
- )}
- estimatedItemHeight={() => 1}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- initialScrollIndex={Number.MAX_SAFE_INTEGER}
- />
-
-
-
-
+
+ {
+ listRef = ref;
+ }}
+ data={items}
+ renderItem={({ item, index }) => (
+
+ )}
+ estimatedItemHeight={() => 1}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ initialScrollIndex={Number.MAX_SAFE_INTEGER}
+ />
+
);
};
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render();
+ result = renderWithProviders();
});
await result!.waitUntilReady();
@@ -696,35 +651,29 @@ describe('ScrollableList Demo Behavior', () => {
}, []);
return (
-
-
-
-
- {
- listRef = ref;
- }}
- data={items}
- renderItem={({ item }) => (
-
- {item.title}
-
- )}
- estimatedItemHeight={() => 2}
- keyExtractor={(item) => item.id}
- hasFocus={true}
- initialScrollIndex={Number.MAX_SAFE_INTEGER}
- />
+
+ {
+ listRef = ref;
+ }}
+ data={items}
+ renderItem={({ item }) => (
+
+ {item.title}
-
-
-
+ )}
+ estimatedItemHeight={() => 2}
+ keyExtractor={(item) => item.id}
+ hasFocus={true}
+ initialScrollIndex={Number.MAX_SAFE_INTEGER}
+ />
+
);
};
- let result: ReturnType;
+ let result: ReturnType;
await act(async () => {
- result = render();
+ result = renderWithProviders();
});
await result!.waitUntilReady();
diff --git a/packages/cli/src/ui/components/shared/SearchableList.test.tsx b/packages/cli/src/ui/components/shared/SearchableList.test.tsx
index e156c12695..127a5feef8 100644
--- a/packages/cli/src/ui/components/shared/SearchableList.test.tsx
+++ b/packages/cli/src/ui/components/shared/SearchableList.test.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { render } from '../../../test-utils/render.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
@@ -14,7 +14,6 @@ import {
type SearchListState,
type GenericListItem,
} from './SearchableList.js';
-import { KeypressProvider } from '../../contexts/KeypressContext.js';
import { useTextBuffer } from './text-buffer.js';
const useMockSearch = (props: {
@@ -52,12 +51,6 @@ const useMockSearch = (props: {
};
};
-vi.mock('../../contexts/UIStateContext.js', () => ({
- useUIState: () => ({
- mainAreaWidth: 100,
- }),
-}));
-
const mockItems: GenericListItem[] = [
{
key: 'item-1',
@@ -98,11 +91,7 @@ describe('SearchableList', () => {
...props,
};
- return render(
-
-
- ,
- );
+ return renderWithProviders();
};
it('should render all items initially', async () => {
diff --git a/packages/cli/src/ui/components/shared/SlicingMaxSizedBox.tsx b/packages/cli/src/ui/components/shared/SlicingMaxSizedBox.tsx
index b756c40ee2..f8f851aed3 100644
--- a/packages/cli/src/ui/components/shared/SlicingMaxSizedBox.tsx
+++ b/packages/cli/src/ui/components/shared/SlicingMaxSizedBox.tsx
@@ -46,7 +46,7 @@ export function SlicingMaxSizedBox({
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
}
}
- if (maxLines) {
+ if (maxLines !== undefined) {
const hasTrailingNewline = text.endsWith('\n');
const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
const lines = contentText.split('\n');
@@ -71,7 +71,7 @@ export function SlicingMaxSizedBox({
};
}
- if (Array.isArray(data) && !isAlternateBuffer && maxLines) {
+ if (Array.isArray(data) && !isAlternateBuffer && maxLines !== undefined) {
if (data.length > maxLines) {
// We will have a label from MaxSizedBox. Reserve space for it.
const targetLines = Math.max(1, maxLines - 1);
diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts
index ff4f3495d7..cd2648b81d 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.test.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts
@@ -579,6 +579,47 @@ describe('textBufferReducer', () => {
});
});
+ describe('kill_line_left action', () => {
+ it('should clean up pastedContent when deleting a placeholder line-left', () => {
+ const placeholder = '[Pasted Text: 6 lines]';
+ const stateWithPlaceholder = createStateWithTransformations({
+ lines: [placeholder],
+ cursorRow: 0,
+ cursorCol: cpLen(placeholder),
+ pastedContent: {
+ [placeholder]: 'line1\nline2\nline3\nline4\nline5\nline6',
+ },
+ });
+
+ const state = textBufferReducer(stateWithPlaceholder, {
+ type: 'kill_line_left',
+ });
+
+ expect(state.lines).toEqual(['']);
+ expect(state.cursorCol).toBe(0);
+ expect(Object.keys(state.pastedContent)).toHaveLength(0);
+ });
+ });
+
+ describe('kill_line_right action', () => {
+ it('should reset preferredCol when deleting to end of line', () => {
+ const stateWithText: TextBufferState = {
+ ...initialState,
+ lines: ['hello world'],
+ cursorRow: 0,
+ cursorCol: 5,
+ preferredCol: 9,
+ };
+
+ const state = textBufferReducer(stateWithText, {
+ type: 'kill_line_right',
+ });
+
+ expect(state.lines).toEqual(['hello']);
+ expect(state.preferredCol).toBe(null);
+ });
+ });
+
describe('toggle_paste_expansion action', () => {
const placeholder = '[Pasted Text: 6 lines]';
const content = 'line1\nline2\nline3\nline4\nline5\nline6';
@@ -937,6 +978,107 @@ describe('useTextBuffer', () => {
expect(Object.keys(result.current.pastedContent)).toHaveLength(0);
});
+ it('deleteWordLeft: should clean up pastedContent and avoid #2 suffix on repaste', () => {
+ const { result } = renderHook(() => useTextBuffer({ viewport }));
+ const largeText = '1\n2\n3\n4\n5\n6';
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+
+ act(() => {
+ for (let i = 0; i < 12; i++) {
+ result.current.deleteWordLeft();
+ }
+ });
+ expect(getBufferState(result).text).toBe('');
+ expect(Object.keys(result.current.pastedContent)).toHaveLength(0);
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+ });
+
+ it('deleteWordRight: should clean up pastedContent and avoid #2 suffix on repaste', () => {
+ const { result } = renderHook(() => useTextBuffer({ viewport }));
+ const largeText = '1\n2\n3\n4\n5\n6';
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+
+ act(() => result.current.move('home'));
+ act(() => {
+ for (let i = 0; i < 12; i++) {
+ result.current.deleteWordRight();
+ }
+ });
+ expect(getBufferState(result).text).not.toContain(
+ '[Pasted Text: 6 lines]',
+ );
+ expect(Object.keys(result.current.pastedContent)).toHaveLength(0);
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toContain('[Pasted Text: 6 lines]');
+ expect(getBufferState(result).text).not.toContain('#2');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+ });
+
+ it('killLineLeft: should clean up pastedContent and avoid #2 suffix on repaste', () => {
+ const { result } = renderHook(() => useTextBuffer({ viewport }));
+ const largeText = '1\n2\n3\n4\n5\n6';
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+
+ act(() => result.current.killLineLeft());
+ expect(getBufferState(result).text).toBe('');
+ expect(Object.keys(result.current.pastedContent)).toHaveLength(0);
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+ });
+
+ it('killLineRight: should clean up pastedContent and avoid #2 suffix on repaste', () => {
+ const { result } = renderHook(() => useTextBuffer({ viewport }));
+ const largeText = '1\n2\n3\n4\n5\n6';
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+
+ act(() => {
+ for (let i = 0; i < 40; i++) {
+ result.current.move('left');
+ }
+ });
+ act(() => result.current.killLineRight());
+ expect(getBufferState(result).text).toBe('');
+ expect(Object.keys(result.current.pastedContent)).toHaveLength(0);
+
+ act(() => result.current.insert(largeText, { paste: true }));
+ expect(getBufferState(result).text).toBe('[Pasted Text: 6 lines]');
+ expect(result.current.pastedContent['[Pasted Text: 6 lines]']).toBe(
+ largeText,
+ );
+ });
+
it('newline: should create a new line and move cursor', () => {
const { result } = renderHook(() =>
useTextBuffer({
diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts
index ad04ff91fe..72d842ec98 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.ts
@@ -1609,6 +1609,47 @@ function generatePastedTextId(
return id;
}
+function collectPlaceholderIdsFromLines(lines: string[]): Set {
+ const ids = new Set();
+ const pasteRegex = new RegExp(PASTED_TEXT_PLACEHOLDER_REGEX.source, 'g');
+ for (const line of lines) {
+ if (!line) continue;
+ for (const match of line.matchAll(pasteRegex)) {
+ const placeholderId = match[0];
+ if (placeholderId) {
+ ids.add(placeholderId);
+ }
+ }
+ }
+ return ids;
+}
+
+function pruneOrphanedPastedContent(
+ pastedContent: Record,
+ expandedPasteId: string | null,
+ beforeChangedLines: string[],
+ allLines: string[],
+): Record {
+ if (Object.keys(pastedContent).length === 0) return pastedContent;
+
+ const beforeIds = collectPlaceholderIdsFromLines(beforeChangedLines);
+ if (beforeIds.size === 0) return pastedContent;
+
+ const afterIds = collectPlaceholderIdsFromLines(allLines);
+ const removedIds = [...beforeIds].filter(
+ (id) => !afterIds.has(id) && id !== expandedPasteId,
+ );
+ if (removedIds.length === 0) return pastedContent;
+
+ const pruned = { ...pastedContent };
+ for (const id of removedIds) {
+ if (pruned[id]) {
+ delete pruned[id];
+ }
+ }
+ return pruned;
+}
+
export type TextBufferAction =
| { type: 'insert'; payload: string; isPaste?: boolean }
| {
@@ -2260,9 +2301,11 @@ function textBufferReducerLogic(
const newLines = [...nextState.lines];
let newCursorRow = cursorRow;
let newCursorCol = cursorCol;
+ let beforeChangedLines: string[] = [];
if (newCursorCol > 0) {
const lineContent = currentLine(newCursorRow);
+ beforeChangedLines = [lineContent];
const prevWordStart = findPrevWordStartInLine(
lineContent,
newCursorCol,
@@ -2275,6 +2318,7 @@ function textBufferReducerLogic(
// Act as a backspace
const prevLineContent = currentLine(cursorRow - 1);
const currentLineContentVal = currentLine(cursorRow);
+ beforeChangedLines = [prevLineContent, currentLineContentVal];
const newCol = cpLen(prevLineContent);
newLines[cursorRow - 1] = prevLineContent + currentLineContentVal;
newLines.splice(cursorRow, 1);
@@ -2282,12 +2326,20 @@ function textBufferReducerLogic(
newCursorCol = newCol;
}
+ const newPastedContent = pruneOrphanedPastedContent(
+ nextState.pastedContent,
+ nextState.expandedPaste?.id ?? null,
+ beforeChangedLines,
+ newLines,
+ );
+
return {
...nextState,
lines: newLines,
cursorRow: newCursorRow,
cursorCol: newCursorCol,
preferredCol: null,
+ pastedContent: newPastedContent,
};
}
@@ -2304,23 +2356,34 @@ function textBufferReducerLogic(
const nextState = currentState;
const newLines = [...nextState.lines];
+ let beforeChangedLines: string[] = [];
if (cursorCol >= lineLen) {
// Act as a delete, joining with the next line
const nextLineContent = currentLine(cursorRow + 1);
+ beforeChangedLines = [lineContent, nextLineContent];
newLines[cursorRow] = lineContent + nextLineContent;
newLines.splice(cursorRow + 1, 1);
} else {
+ beforeChangedLines = [lineContent];
const nextWordStart = findNextWordStartInLine(lineContent, cursorCol);
const end = nextWordStart === null ? lineLen : nextWordStart;
newLines[cursorRow] =
cpSlice(lineContent, 0, cursorCol) + cpSlice(lineContent, end);
}
+ const newPastedContent = pruneOrphanedPastedContent(
+ nextState.pastedContent,
+ nextState.expandedPaste?.id ?? null,
+ beforeChangedLines,
+ newLines,
+ );
+
return {
...nextState,
lines: newLines,
preferredCol: null,
+ pastedContent: newPastedContent,
};
}
@@ -2332,22 +2395,39 @@ function textBufferReducerLogic(
if (cursorCol < currentLineLen(cursorRow)) {
const nextState = currentState;
const newLines = [...nextState.lines];
+ const beforeChangedLines = [lineContent];
newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
+ const newPastedContent = pruneOrphanedPastedContent(
+ nextState.pastedContent,
+ nextState.expandedPaste?.id ?? null,
+ beforeChangedLines,
+ newLines,
+ );
return {
...nextState,
lines: newLines,
+ preferredCol: null,
+ pastedContent: newPastedContent,
};
} else if (cursorRow < lines.length - 1) {
// Act as a delete
const nextState = currentState;
const nextLineContent = currentLine(cursorRow + 1);
const newLines = [...nextState.lines];
+ const beforeChangedLines = [lineContent, nextLineContent];
newLines[cursorRow] = lineContent + nextLineContent;
newLines.splice(cursorRow + 1, 1);
+ const newPastedContent = pruneOrphanedPastedContent(
+ nextState.pastedContent,
+ nextState.expandedPaste?.id ?? null,
+ beforeChangedLines,
+ newLines,
+ );
return {
...nextState,
lines: newLines,
preferredCol: null,
+ pastedContent: newPastedContent,
};
}
return currentState;
@@ -2361,12 +2441,20 @@ function textBufferReducerLogic(
const nextState = currentState;
const lineContent = currentLine(cursorRow);
const newLines = [...nextState.lines];
+ const beforeChangedLines = [lineContent];
newLines[cursorRow] = cpSlice(lineContent, cursorCol);
+ const newPastedContent = pruneOrphanedPastedContent(
+ nextState.pastedContent,
+ nextState.expandedPaste?.id ?? null,
+ beforeChangedLines,
+ newLines,
+ );
return {
...nextState,
lines: newLines,
cursorCol: 0,
preferredCol: null,
+ pastedContent: newPastedContent,
};
}
return currentState;
diff --git a/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx b/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx
index d7e4fb8ae4..d8df7012cc 100644
--- a/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx
+++ b/packages/cli/src/ui/components/views/ExtensionDetails.test.tsx
@@ -5,11 +5,10 @@
*/
import React from 'react';
-import { render } from '../../../test-utils/render.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExtensionDetails } from './ExtensionDetails.js';
-import { KeypressProvider } from '../../contexts/KeypressContext.js';
import { type RegistryExtension } from '../../../config/extensionRegistryClient.js';
const mockExtension: RegistryExtension = {
@@ -43,15 +42,13 @@ describe('ExtensionDetails', () => {
});
const renderDetails = (isInstalled = false) =>
- render(
-
-
- ,
+ renderWithProviders(
+ ,
);
it('should render extension details correctly', async () => {
diff --git a/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx b/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx
index b13b202b90..55e307ecfe 100644
--- a/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx
+++ b/packages/cli/src/ui/components/views/ExtensionRegistryView.test.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { render } from '../../../test-utils/render.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExtensionRegistryView } from './ExtensionRegistryView.js';
@@ -14,9 +14,7 @@ import { useExtensionRegistry } from '../../hooks/useExtensionRegistry.js';
import { useExtensionUpdates } from '../../hooks/useExtensionUpdates.js';
import { useRegistrySearch } from '../../hooks/useRegistrySearch.js';
import { type RegistryExtension } from '../../../config/extensionRegistryClient.js';
-import { useUIState } from '../../contexts/UIStateContext.js';
-import { useConfig } from '../../contexts/ConfigContext.js';
-import { KeypressProvider } from '../../contexts/KeypressContext.js';
+import { type UIState } from '../../contexts/UIStateContext.js';
import {
type SearchListState,
type GenericListItem,
@@ -28,8 +26,6 @@ vi.mock('../../hooks/useExtensionRegistry.js');
vi.mock('../../hooks/useExtensionUpdates.js');
vi.mock('../../hooks/useRegistrySearch.js');
vi.mock('../../../config/extension-manager.js');
-vi.mock('../../contexts/UIStateContext.js');
-vi.mock('../../contexts/ConfigContext.js');
const mockExtensions: RegistryExtension[] = [
{
@@ -123,34 +119,27 @@ describe('ExtensionRegistryView', () => {
maxLabelWidth: 10,
}) as unknown as SearchListState,
);
-
- vi.mocked(useUIState).mockReturnValue({
- mainAreaWidth: 100,
- terminalHeight: 40,
- staticExtraHeight: 5,
- } as unknown as ReturnType);
-
- vi.mocked(useConfig).mockReturnValue({
- getEnableExtensionReloading: vi.fn().mockReturnValue(false),
- getExtensionRegistryURI: vi
- .fn()
- .mockReturnValue('https://geminicli.com/extensions.json'),
- } as unknown as ReturnType);
});
const renderView = () =>
- render(
-
-
- ,
+ renderWithProviders(
+ ,
+ {
+ uiState: {
+ staticExtraHeight: 5,
+ terminalHeight: 40,
+ } as Partial,
+ },
);
it('should render extensions', async () => {
- const { lastFrame } = renderView();
+ const { lastFrame, waitUntilReady } = renderView();
+ await waitUntilReady();
+
await waitFor(() => {
expect(lastFrame()).toContain('Test Extension 1');
expect(lastFrame()).toContain('Test Extension 2');
diff --git a/packages/cli/src/ui/constants/tips.ts b/packages/cli/src/ui/constants/tips.ts
index a1ed09de3e..15aa86c118 100644
--- a/packages/cli/src/ui/constants/tips.ts
+++ b/packages/cli/src/ui/constants/tips.ts
@@ -75,7 +75,6 @@ export const INFORMATIVE_TIPS = [
'Set the character threshold for truncating tool outputs (/settings)…',
'Set the number of lines to keep when truncating outputs (/settings)…',
'Enable policy-based tool confirmation via message bus (/settings)…',
- 'Enable write_todos_list tool to generate task lists (/settings)…',
'Enable experimental subagents for task delegation (/settings)…',
'Enable extension management features (settings.json)…',
'Enable extension reloading within the CLI session (settings.json)…',
diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
index 31e43af575..8eb9c7c94f 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx
@@ -5,13 +5,12 @@
*/
import { debugLogger } from '@google/gemini-cli-core';
-import type React from 'react';
import { act } from 'react';
-import { renderHook } from '../../test-utils/render.js';
+import { renderHookWithProviders } from '../../test-utils/render.js';
+import { createMockSettings } from '../../test-utils/settings.js';
import { waitFor } from '../../test-utils/async.js';
import { vi, afterAll, beforeAll, type Mock } from 'vitest';
import {
- KeypressProvider,
useKeypressContext,
ESC_TIMEOUT,
FAST_RETURN_TIMEOUT,
@@ -52,11 +51,8 @@ class MockStdin extends EventEmitter {
// Helper function to setup keypress test with standard configuration
const setupKeypressTest = () => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
return { result, keyHandler };
@@ -66,10 +62,6 @@ describe('KeypressContext', () => {
let stdin: MockStdin;
const mockSetRawMode = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
-
beforeAll(() => vi.useFakeTimers());
afterAll(() => vi.useRealTimers());
@@ -269,10 +261,7 @@ describe('KeypressContext', () => {
it('should handle double Escape', async () => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
act(() => {
@@ -306,10 +295,7 @@ describe('KeypressContext', () => {
it('should handle lone Escape key (keycode 27) with timeout when kitty protocol is enabled', async () => {
// Use real timers for this test to avoid issues with stream/buffer timing
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
// Send just ESC
@@ -432,7 +418,7 @@ describe('KeypressContext', () => {
])('should $name', async ({ pastedText, writeSequence }) => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -452,7 +438,7 @@ describe('KeypressContext', () => {
it('should parse valid OSC 52 response', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -473,7 +459,7 @@ describe('KeypressContext', () => {
it('should handle split OSC 52 response', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -499,7 +485,7 @@ describe('KeypressContext', () => {
it('should handle OSC 52 response terminated by ESC \\', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -520,7 +506,7 @@ describe('KeypressContext', () => {
it('should ignore unknown OSC sequences', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -537,7 +523,7 @@ describe('KeypressContext', () => {
it('should ignore invalid OSC 52 format', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -569,13 +555,11 @@ describe('KeypressContext', () => {
it('should not log keystrokes when debugKeystrokeLogging is false', async () => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext(), {
+ settings: createMockSettings({
+ general: { debugKeystrokeLogging: false },
+ }),
+ });
act(() => result.current.subscribe(keyHandler));
@@ -593,13 +577,11 @@ describe('KeypressContext', () => {
it('should log kitty buffer accumulation when debugKeystrokeLogging is true', async () => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext(), {
+ settings: createMockSettings({
+ general: { debugKeystrokeLogging: true },
+ }),
+ });
act(() => result.current.subscribe(keyHandler));
@@ -614,13 +596,11 @@ describe('KeypressContext', () => {
it('should show char codes when debugKeystrokeLogging is true even without debug mode', async () => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
- );
-
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext(), {
+ settings: createMockSettings({
+ general: { debugKeystrokeLogging: true },
+ }),
+ });
act(() => result.current.subscribe(keyHandler));
@@ -765,7 +745,7 @@ describe('KeypressContext', () => {
'should recognize sequence "$sequence" as $expected.name',
({ sequence, expected }) => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
act(() => stdin.write(sequence));
@@ -1000,12 +980,7 @@ describe('KeypressContext', () => {
'should handle Alt+$key in $terminal',
({ chunk, expected }: { chunk: string; expected: Partial }) => {
const keyHandler = vi.fn();
- const testWrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useKeypressContext(), {
- wrapper: testWrapper,
- });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
act(() => stdin.write(chunk));
@@ -1042,7 +1017,7 @@ describe('KeypressContext', () => {
it('should timeout and flush incomplete kitty sequences after 50ms', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1077,7 +1052,7 @@ describe('KeypressContext', () => {
it('should immediately flush non-kitty CSI sequences', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1099,7 +1074,7 @@ describe('KeypressContext', () => {
it('should parse valid kitty sequences immediately when complete', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1117,7 +1092,7 @@ describe('KeypressContext', () => {
it('should handle batched kitty sequences correctly', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1144,7 +1119,7 @@ describe('KeypressContext', () => {
it('should handle mixed valid and invalid sequences', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1172,7 +1147,7 @@ describe('KeypressContext', () => {
'should handle sequences arriving character by character with %s ms delay',
async (delay) => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1196,7 +1171,7 @@ describe('KeypressContext', () => {
it('should reset timeout when new input arrives', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1231,7 +1206,7 @@ describe('KeypressContext', () => {
describe('SGR Mouse Handling', () => {
it('should ignore SGR mouse sequences', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1249,7 +1224,7 @@ describe('KeypressContext', () => {
it('should handle mixed SGR mouse and key sequences', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1275,7 +1250,7 @@ describe('KeypressContext', () => {
it('should ignore X11 mouse sequences', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1291,7 +1266,7 @@ describe('KeypressContext', () => {
it('should not flush slow SGR mouse sequences as garbage', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1311,7 +1286,7 @@ describe('KeypressContext', () => {
it('should ignore specific SGR mouse sequence sandwiched between keystrokes', async () => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
@@ -1342,12 +1317,7 @@ describe('KeypressContext', () => {
{ name: 'another mouse', sequence: '\u001b[<0;29;19m' },
])('should ignore $name sequence', async ({ sequence }) => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useKeypressContext(), {
- wrapper,
- });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
for (const char of sequence) {
@@ -1372,10 +1342,7 @@ describe('KeypressContext', () => {
it('should handle F12', async () => {
const keyHandler = vi.fn();
- const wrapper = ({ children }: { children: React.ReactNode }) => (
- {children}
- );
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
act(() => {
@@ -1404,7 +1371,7 @@ describe('KeypressContext', () => {
'A你B好C', // Mixed characters
])('should correctly handle string "%s"', async (inputString) => {
const keyHandler = vi.fn();
- const { result } = renderHook(() => useKeypressContext(), { wrapper });
+ const { result } = renderHookWithProviders(() => useKeypressContext());
act(() => result.current.subscribe(keyHandler));
act(() => stdin.write(inputString));
diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx
index cdd6da7feb..3189172792 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.tsx
@@ -13,6 +13,7 @@ import {
useCallback,
useContext,
useEffect,
+ useMemo,
useRef,
} from 'react';
@@ -21,6 +22,7 @@ import { parseMouseEvent } from '../utils/mouse.js';
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
import { appEvents, AppEvent } from '../../utils/events.js';
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
+import { useSettingsStore } from './SettingsContext.js';
export const BACKSLASH_ENTER_TIMEOUT = 5;
export const ESC_TIMEOUT = 50;
@@ -766,12 +768,13 @@ export function useKeypressContext() {
export function KeypressProvider({
children,
config,
- debugKeystrokeLogging,
}: {
children: React.ReactNode;
config?: Config;
- debugKeystrokeLogging?: boolean;
}) {
+ const { settings } = useSettingsStore();
+ const debugKeystrokeLogging = settings.merged.general.debugKeystrokeLogging;
+
const { stdin, setRawMode } = useStdin();
const subscribersToPriority = useRef