Compare commits

...

70 Commits

Author SHA1 Message Date
jacob314 674545cc21 Update ink version to fix terminal worker launching bug. 2026-04-01 14:13:06 -07:00
jacob314 9a03c5ca53 feat: add config and keybindings for new ink terminal buffer mode
test: update test utils and resolve snapshot differences for ink changes

feat: refactor VirtualizedList to support static rendering and terminal buffers

feat: wire up AppContainer mouse mode and recording state for ink buffer

Fix stale ref in ScrollProvider breaking scrolling.

The ScrollProvider was getting stuck with a stale reference because the
`scrollables` Map was being registered and unregistered whenever the entry
identity changed. The underlying `ScrollableList` component dynamically created
a new ID which caused React remount cycles, while the `ScrollProvider` itself
suffered from state lag where the event handler ref was one tick behind the
actual registration.

This commit resolves these issues by:
1. Adding a stable `id` and `key` to `ScrollableList` in `MainContent.tsx`
2. Making `scrollablesRef` synchronously update in `ScrollProvider.tsx`
3. Using a proxy entry in `useScrollable` to avoid constant re-registering.

chore: add useEffect cleanup for ResizeObservers

Checkpoint fixing terminal buffer support.

Termina Serializer Optimization

CHECKPOINT_BS_NOT_FOR_COMMIT

Setting schema stuff missed earlier.

Tool Result display fixes.

Checkpoint no commit.

Checkpoint of compile fixes

feat: Auto-scroll to bottom via Events

feat: Auto-scroll to bottom on exiting mouse mode

Fix snapshots
2026-04-01 14:07:28 -07:00
Sam Roberts 597778e55f Update README.md for links. (#22759) 2026-04-01 19:15:44 +00:00
N. Taylor Mullen 4e21e5b8a3 fix(cli): refresh slash command list after /skills reload (#24454) 2026-04-01 19:15:27 +00:00
AK d9d51ba15b docs(core): add subagent tool isolation draft doc (#23275)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
2026-04-01 18:45:21 +00:00
Gaurav aed85725b6 feat(browser): supersede stale snapshots to reclaim context-window tokens (#24440) 2026-04-01 18:41:39 +00:00
anj-s 43cf63e189 fix: update task tracker storage location in system prompt (#24034) 2026-04-01 18:29:09 +00:00
gemini-cli-robot bda4491616 Changelog for v0.36.0-preview.7 (#24346)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-04-01 18:23:28 +00:00
Sri Pasumarthi 6b303a13eb fix(acp) refactor(core,cli): centralize model discovery logic in ModelConfigService (#24392) 2026-04-01 18:03:30 +00:00
cynthialong0-0 16468a855d feat(core): update browser agent prompt to check open pages first when bringing up (#24431) 2026-04-01 17:52:18 +00:00
joshualitt 377e834e03 refactor(core): Unified context management settings schema (#24391) 2026-04-01 17:24:45 +00:00
anj-s 3a32d9723e feat(core): enhance tracker task schemas for detailed titles and descriptions (#23902) 2026-04-01 17:19:47 +00:00
Gaurav bf3ac20da0 feat(browser): add sandbox-aware browser agent initialization (#24419) 2026-04-01 17:18:17 +00:00
Gaurav a3ef87e6e2 fix(build): wire bundle:browser-mcp into bundle pipeline (#24424) 2026-04-01 17:09:58 +00:00
Dev Randalpura 4c4d8bc411 fix(ui): removed additional vertical padding for tables (#24381) 2026-04-01 16:46:38 +00:00
Sehoon Shon f938a3f51d fix(build): upload full bundle directory archive to GitHub releases (#24403) 2026-04-01 16:39:44 +00:00
cynthialong0-0 c61506bbc1 fix(core): ensure blue border overlay and input blocker to act correctly depending on browser agent activities (#24385) 2026-04-01 16:35:44 +00:00
Emily Hedlund 6a8a0d4faa feat(core): populate sandbox forbidden paths with project ignore file contents (#24038) 2026-04-01 16:27:55 +00:00
Dev Randalpura 066da2a1d1 fix(ui): add accelerated scrolling on alternate buffer mode (#23940)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-04-01 16:23:40 +00:00
PROTHAM 7d1848d578 fix(cli): cap shell output at 10 MB to prevent RangeError crash (#24168) 2026-04-01 16:09:30 +00:00
ruomeng eb95e99b3d feat(plan): conditionally add enter/exit plan mode tools based on current mode (#24378) 2026-04-01 15:56:10 +00:00
Jerop Kipruto ca43f8c291 feat(core): prioritize discussion before formal plan approval (#24423) 2026-04-01 15:55:47 +00:00
Abhi dcf5afafda fix(core): resolve subagent chat recording gaps and directory inheritance (#24368) 2026-04-01 15:29:38 +00:00
Jacob Richman 9f76f34049 ink 6.6.3 (#24372)
Co-authored-by: matt korwel <matt.korwel@gmail.com>
2026-04-01 04:50:12 +00:00
matt korwel 7c5cd693ce Set memoryManager to false in settings.json (#24393) 2026-04-01 03:38:50 +00:00
matt korwel c9ed5e41b1 fix(cli): refactor test config loading and mock debugLogger in test-setup (#24389) 2026-03-31 20:11:02 -07:00
Daniel R. Neal 8ae5b56b5b feat(ui): add Tokyo Night theme (#24054)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2026-04-01 01:08:28 +00:00
Sri Pasumarthi 1b265f343f fix(core): pin AuthType.GATEWAY to use Gemini 3.1 Pro/Flash Lite by default (#24375) 2026-04-01 00:25:59 +00:00
joshualitt fd5c103f99 refactor(core): Centralize context management logic into src/context (#24380) 2026-04-01 00:01:46 +00:00
Jarrod Whelan cdc602edd7 fix(ui): wrap topic and intent fields in TopicMessage (#24386) 2026-04-01 00:00:48 +00:00
Gaurav 782bb4e4bd fix(core): fix browser agent UX issues and improve E2E test reliability (#24312) 2026-03-31 23:00:16 +00:00
David Pierce 94f9480a3a fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions (#24047) 2026-03-31 22:06:50 +00:00
Abhi 9364dd8a49 feat(core): infrastructure for event-driven subagent history (#23914) 2026-03-31 21:54:22 +00:00
Tirth Panchal 6d48a12efe docs: fix broken GEMINI.md link in CONTRIBUTING.md (#24182)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Sam Roberts <158088236+g-samroberts@users.noreply.github.com>
2026-03-31 21:44:00 +00:00
Abhi beab7cc053 docs(policy): remove unsupported mcpName wildcard edge case (#24133) 2026-03-31 21:11:29 +00:00
Gal Zahavi 6cade3eaec test(sandbox): add integration test for dynamic permission expansion (#24359) 2026-03-31 20:59:44 +00:00
Sandy Tao c8d18eb2ac fix(config): disable JIT context loading by default (#24364) 2026-03-31 20:31:22 +00:00
Tommaso Sciortino 91400c5b0b don't try to remove tags in dry run (#24356) 2026-03-31 12:54:53 -07:00
Gal Zahavi 554a5a36a3 fix(core): implement __read and __write commands in sandbox managers (#24283) 2026-03-31 19:39:51 +00:00
Abhi 7e117ac0ac docs: update subagents docs to not be experimental (#24343) 2026-03-31 18:58:41 +00:00
Jerop Kipruto 6f053dd8b9 Revert "Add security settings for tool sandboxing" (#24357) 2026-03-31 18:39:28 +00:00
Sehoon Shon 25af91e4df chore(release): allow bundling for all builds, including stable (#24305) 2026-03-31 18:37:14 +00:00
Emily Hedlund 1edc542b55 ci: install bubblewrap on Linux for release workflows (#24347) 2026-03-31 18:19:48 +00:00
Sehoon Shon 119dff3b73 perf(build): optimize build scripts for parallel execution and remove redundant checks (#24307) 2026-03-31 17:46:36 +00:00
cynthialong0-0 7c4b497a84 fix(core): fix race condition between browser agent and main closing process (#24340) 2026-03-31 17:43:37 +00:00
Emily Hedlund 65ee6171e7 fix(core): batch macOS seatbelt rules into a profile file to prevent ARG_MAX errors (#24255) 2026-03-31 17:35:13 +00:00
Yifan Ruan 9cb09ccf8e docs: Add 'plan' option to approval mode in CLI reference (#24134) 2026-03-31 17:34:59 +00:00
cynthialong0-0 a79f2f81ae Fix(browser): terminate immediately for "browser is already running" error (#24233)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2026-03-31 17:07:58 +00:00
ruomeng 140c2b9914 feat(cli): add UI to update extensions (#23682) 2026-03-31 17:05:08 +00:00
Gaurav 6805e818f7 fix(browser): terminate subagent immediately on domain restriction violations (#24313) 2026-03-31 16:51:27 +00:00
ruomeng 07e2053e12 feat(plan): promote planning feature to stable (#24282) 2026-03-31 16:10:13 +00:00
Sehoon Shon f9a93a1337 feat(cli): add auth info to footer (#24042) 2026-03-31 16:10:13 +00:00
cynthialong0-0 3982a252bb fix(browser): reset action counter for each agent session and let it ignore internal actions (#24228) 2026-03-31 15:23:19 +00:00
gemini-cli-robot e293424bb4 Changelog for v0.35.3 (#24083)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-31 06:35:30 +00:00
gemini-cli-robot 21ad42f677 Changelog for v0.36.0-preview.6 (#24082)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-31 06:35:26 +00:00
Sehoon Shon 561418c554 fix(core): enable mid-stream retries for all models and re-enable compression test (#24302) 2026-03-31 06:25:21 +00:00
Adib234 d0d3639e16 feat(plan): support Plan mode in untrusted folders (#17586) 2026-03-31 03:33:55 +00:00
Sehoon Shon 2f7f967189 test(integration): fix plan mode write denial test false positive (#24299) 2026-03-31 03:17:36 +00:00
Sandy Tao 46d6b119b6 feat(core): add project-level memory scope to save_memory tool (#24161) 2026-03-31 01:32:15 +00:00
Abhijit Balaji 80929c48c5 feat(core): enable topic update narration for legacy models (#24241)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-31 00:57:46 +00:00
Sehoon Shon 35efdfc409 chore(test-utils): switch integration tests to use PREVIEW_GEMINI_MODEL (#24276)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-03-31 00:31:04 +00:00
Gal Zahavi bea57a2f3d Add security settings for tool sandboxing (#23923) 2026-03-30 23:49:53 +00:00
Jarrod Whelan 1df5c98b33 feat(cli): implement compact tool output (#20974) 2026-03-30 23:43:29 +00:00
Sehoon Shon 3e95b8ec59 feat(cli): show Flash Lite Preview model regardless of user tier (#23904) 2026-03-30 23:30:33 +00:00
Abhi 5b5f87abc7 fix(core): ensure default agents provide tools and use model-specific schemas (#24268) 2026-03-30 23:10:57 +00:00
Christian Gunderman 991dca4c40 Default enable narration for the team. (#24224) 2026-03-30 22:30:55 +00:00
joshualitt dfba0e91e2 feat(core): Unified Context Management and Tool Distillation. (#24157) 2026-03-30 22:29:59 +00:00
Christian Gunderman 117a2d3844 fix(evals): add update_topic behavioral eval (#24223) 2026-03-30 22:02:53 +00:00
Tommaso Sciortino 4b20d93e1d fix broken tests (#24279) 2026-03-30 15:25:56 -07:00
Tommaso Sciortino 44cdb3e376 fix(cli): resolve missing F12 logs via global console store (#24235) 2026-03-30 13:15:10 -07:00
326 changed files with 12920 additions and 4170 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
{
"experimental": {
"plan": true,
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true
"memoryManager": false,
"topicUpdateNarration": true
},
"general": {
"devtools": true
+25 -4
View File
@@ -175,7 +175,9 @@ runs:
--dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
--no-tag
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
fi
- name: '🔗 Install latest core package'
working-directory: '${{ inputs.working-directory }}'
@@ -193,7 +195,7 @@ runs:
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
- name: '📦 Prepare bundled CLI for npm release'
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'"
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
@@ -248,7 +250,9 @@ runs:
--dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
--no-tag
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
fi
- name: '🔬 Verify NPM release by version'
uses: './.github/actions/verify-release'
@@ -287,8 +291,25 @@ runs:
INPUTS_PREVIOUS_TAG: '${{ inputs.previous-tag }}'
shell: 'bash'
run: |
rm -f gemini-cli-bundle.zip
(cd bundle && chmod +x gemini.js && zip -r ../gemini-cli-bundle.zip .)
echo "Testing the generated bundle archive..."
rm -rf test-bundle
mkdir -p test-bundle
unzip -q gemini-cli-bundle.zip -d test-bundle
# Verify it runs and outputs a version
BUNDLE_VERSION=$(node test-bundle/gemini.js --version | xargs)
echo "Bundle version output: ${BUNDLE_VERSION}"
if [[ -z "${BUNDLE_VERSION}" ]]; then
echo "Error: Bundle failed to execute or return version."
exit 1
fi
rm -rf test-bundle
gh release create "${INPUTS_RELEASE_TAG}" \
bundle/gemini.js \
gemini-cli-bundle.zip \
--target "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" \
--title "Release ${INPUTS_RELEASE_TAG}" \
--notes-start-tag "${INPUTS_PREVIOUS_TAG}" \
+7
View File
@@ -18,6 +18,13 @@ runs:
env:
JSON_INPUTS: '${{ toJSON(inputs) }}'
run: 'echo "$JSON_INPUTS"'
- name: 'Install system dependencies'
if: "runner.os == 'Linux'"
run: |
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
shell: 'bash'
- name: 'Run Tests'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
+8 -5
View File
@@ -346,9 +346,11 @@ npm run lint
- Please adhere to the coding style, patterns, and conventions used throughout
the existing codebase.
- Consult [GEMINI.md](../GEMINI.md) (typically found in the project root) for
specific instructions related to AI-assisted development, including
conventions for React, comments, and Git usage.
- Consult
[GEMINI.md](https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md)
(typically found in the project root) for specific instructions related to
AI-assisted development, including conventions for React, comments, and Git
usage.
- **Imports:** Pay special attention to import paths. The project uses ESLint to
enforce restrictions on relative imports between packages.
@@ -505,8 +507,9 @@ code.
### Documentation structure
Our documentation is organized using [sidebar.json](/docs/sidebar.json) as the
table of contents. When adding new documentation:
Our documentation is organized using
[sidebar.json](https://github.com/google-gemini/gemini-cli/blob/main/docs/sidebar.json)
as the table of contents. When adding new documentation:
1. Create your markdown file **in the appropriate directory** under `/docs`.
2. Add an entry to `sidebar.json` in the relevant section.
+58 -50
View File
@@ -30,7 +30,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
## 📦 Installation
See
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
[Gemini CLI installation, execution, and releases](https://www.geminicli.com/docs/get-started/installation)
for recommended system specifications and a detailed installation guide.
### Quick Install
@@ -71,9 +71,9 @@ conda activate gemini_env
npm install -g @google/gemini-cli
```
## Release Cadence and Tags
## Release Channels
See [Releases](./docs/releases.md) for more details.
See [Releases](https://www.geminicli.com/docs/changelogs) for more details.
### Preview
@@ -209,7 +209,7 @@ gemini
```
For Google Workspace accounts and other authentication methods, see the
[authentication guide](./docs/get-started/authentication.md).
[authentication guide](https://www.geminicli.com/docs/get-started/authentication).
## 🚀 Getting Started
@@ -278,59 +278,64 @@ gemini
### Getting Started
- [**Quickstart Guide**](./docs/get-started/index.md) - Get up and running
quickly.
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
auth configuration.
- [**Configuration Guide**](./docs/reference/configuration.md) - Settings and
customization.
- [**Keyboard Shortcuts**](./docs/reference/keyboard-shortcuts.md) -
- [**Quickstart Guide**](https://www.geminicli.com/docs/get-started) - Get up
and running quickly.
- [**Authentication Setup**](https://www.geminicli.com/docs/get-started/authentication) -
Detailed auth configuration.
- [**Configuration Guide**](https://www.geminicli.com/docs/reference/configuration) -
Settings and customization.
- [**Keyboard Shortcuts**](https://www.geminicli.com/docs/reference/keyboard-shortcuts) -
Productivity tips.
### Core Features
- [**Commands Reference**](./docs/reference/commands.md) - All slash commands
(`/help`, `/chat`, etc).
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
reusable commands.
- [**Context Files (GEMINI.md)**](./docs/cli/gemini-md.md) - Provide persistent
context to Gemini CLI.
- [**Checkpointing**](./docs/cli/checkpointing.md) - Save and resume
conversations.
- [**Token Caching**](./docs/cli/token-caching.md) - Optimize token usage.
- [**Commands Reference**](https://www.geminicli.com/docs/reference/commands) -
All slash commands (`/help`, `/chat`, etc).
- [**Custom Commands**](https://www.geminicli.com/docs/cli/custom-commands) -
Create your own reusable commands.
- [**Context Files (GEMINI.md)**](https://www.geminicli.com/docs/cli/gemini-md) -
Provide persistent context to Gemini CLI.
- [**Checkpointing**](https://www.geminicli.com/docs/cli/checkpointing) - Save
and resume conversations.
- [**Token Caching**](https://www.geminicli.com/docs/cli/token-caching) -
Optimize token usage.
### Tools & Extensions
- [**Built-in Tools Overview**](./docs/reference/tools.md)
- [File System Operations](./docs/tools/file-system.md)
- [Shell Commands](./docs/tools/shell.md)
- [Web Fetch & Search](./docs/tools/web-fetch.md)
- [**MCP Server Integration**](./docs/tools/mcp-server.md) - Extend with custom
tools.
- [**Custom Extensions**](./docs/extensions/index.md) - Build and share your own
commands.
- [**Built-in Tools Overview**](https://www.geminicli.com/docs/reference/tools)
- [File System Operations](https://www.geminicli.com/docs/tools/file-system)
- [Shell Commands](https://www.geminicli.com/docs/tools/shell)
- [Web Fetch & Search](https://www.geminicli.com/docs/tools/web-fetch)
- [**MCP Server Integration**](https://www.geminicli.com/docs/tools/mcp-server) -
Extend with custom tools.
- [**Custom Extensions**](https://geminicli.com/docs/extensions/writing-extensions) -
Build and share your own commands.
### Advanced Topics
- [**Headless Mode (Scripting)**](./docs/cli/headless.md) - Use Gemini CLI in
automated workflows.
- [**IDE Integration**](./docs/ide-integration/index.md) - VS Code companion.
- [**Sandboxing & Security**](./docs/cli/sandbox.md) - Safe execution
environments.
- [**Trusted Folders**](./docs/cli/trusted-folders.md) - Control execution
policies by folder.
- [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a
corporate environment.
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
- [**Tools reference**](./docs/reference/tools.md) - Built-in tools overview.
- [**Local development**](./docs/local-development.md) - Local development
tooling.
- [**Headless Mode (Scripting)**](https://www.geminicli.com/docs/cli/headless) -
Use Gemini CLI in automated workflows.
- [**IDE Integration**](https://www.geminicli.com/docs/ide-integration) - VS
Code companion.
- [**Sandboxing & Security**](https://www.geminicli.com/docs/cli/sandbox) - Safe
execution environments.
- [**Trusted Folders**](https://www.geminicli.com/docs/cli/trusted-folders) -
Control execution policies by folder.
- [**Enterprise Guide**](https://www.geminicli.com/docs/cli/enterprise) - Deploy
and manage in a corporate environment.
- [**Telemetry & Monitoring**](https://www.geminicli.com/docs/cli/telemetry) -
Usage tracking.
- [**Tools reference**](https://www.geminicli.com/docs/reference/tools) -
Built-in tools overview.
- [**Local development**](https://www.geminicli.com/docs/local-development) -
Local development tooling.
### Troubleshooting & Support
- [**Troubleshooting Guide**](./docs/resources/troubleshooting.md) - Common
issues and solutions.
- [**FAQ**](./docs/resources/faq.md) - Frequently asked questions.
- [**Troubleshooting Guide**](https://www.geminicli.com/docs/resources/troubleshooting) -
Common issues and solutions.
- [**FAQ**](https://www.geminicli.com/docs/resources/faq) - Frequently asked
questions.
- Use `/bug` command to report issues directly from the CLI.
### Using MCP Servers
@@ -344,8 +349,9 @@ custom tools:
> @database Run a query to find inactive users
```
See the [MCP Server Integration guide](./docs/tools/mcp-server.md) for setup
instructions.
See the
[MCP Server Integration guide](https://www.geminicli.com/docs/tools/mcp-server)
for setup instructions.
## 🤝 Contributing
@@ -366,7 +372,8 @@ for planned features and priorities.
## 📖 Resources
- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
- **[Changelog](./docs/changelogs/index.md)** - See recent notable updates.
- **[Changelog](https://www.geminicli.com/docs/changelogs)** - See recent
notable updates.
- **[NPM Package](https://www.npmjs.com/package/@google/gemini-cli)** - Package
registry.
- **[GitHub Issues](https://github.com/google-gemini/gemini-cli/issues)** -
@@ -376,13 +383,14 @@ for planned features and priorities.
### Uninstall
See the [Uninstall Guide](./docs/resources/uninstall.md) for removal
instructions.
See the [Uninstall Guide](https://www.geminicli.com/docs/resources/uninstall)
for removal instructions.
## 📄 Legal
- **License**: [Apache License 2.0](LICENSE)
- **Terms of Service**: [Terms & Privacy](./docs/resources/tos-privacy.md)
- **Terms of Service**:
[Terms & Privacy](https://www.geminicli.com/docs/resources/tos-privacy)
- **Security**: [Security Policy](SECURITY.md)
---
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+6 -3
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.35.2
# Latest stable release: v0.35.3
Released: March 26, 2026
Released: March 28, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -29,6 +29,9 @@ npm install -g @google/gemini-cli
## What's Changed
- fix(patch): cherry-pick 765fb67 to release/v0.35.2-pr-24055 [CONFLICTS] by
@gemini-cli-robot in
[#24063](https://github.com/google-gemini/gemini-cli/pull/24063)
- fix(core): allow disabling environment variable redaction by @galz10 in
[#23927](https://github.com/google-gemini/gemini-cli/pull/23927)
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
@@ -385,4 +388,4 @@ npm install -g @google/gemini-cli
[#23585](https://github.com/google-gemini/gemini-cli/pull/23585)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.34.0...v0.35.2
https://github.com/google-gemini/gemini-cli/compare/v0.34.0...v0.35.3
+7 -3
View File
@@ -1,6 +1,6 @@
# Preview release: v0.36.0-preview.5
# Preview release: v0.36.0-preview.7
Released: March 27, 2026
Released: March 31, 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).
@@ -31,6 +31,10 @@ npm install -g @google/gemini-cli@preview
## What's Changed
- fix(patch): cherry-pick 765fb67 to release/v0.36.0-preview.5-pr-24055 to patch
version v0.36.0-preview.5 and create version 0.36.0-preview.6 by
@gemini-cli-robot in
[#24061](https://github.com/google-gemini/gemini-cli/pull/24061)
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
@kschaab in [#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
- docs(core): document agent_card_json string literal options for remote agents
@@ -386,4 +390,4 @@ npm install -g @google/gemini-cli@preview
[#23666](https://github.com/google-gemini/gemini-cli/pull/23666)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.35.0-preview.5...v0.36.0-preview.5
https://github.com/google-gemini/gemini-cli/compare/v0.35.0-preview.5...v0.36.0-preview.7
+1 -1
View File
@@ -52,7 +52,7 @@ These commands are available within the interactive REPL.
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--worktree` | `-w` | string | - | Start Gemini in a new git worktree. If no name is provided, one is generated automatically. Requires `experimental.worktrees: true` in settings. |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo`, `plan` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
+9 -7
View File
@@ -56,19 +56,21 @@ Gemini CLI takes action.
1. **Provide a goal:** Start by describing what you want to achieve. Gemini CLI
will then enter Plan Mode (if it's not already) to research the task.
2. **Review research and provide input:** As Gemini CLI analyzes your codebase,
it may ask you questions or present different implementation options using
[`ask_user`](../tools/ask-user.md). Provide your preferences to help guide
the design.
3. **Review the plan:** Once Gemini CLI has a proposed strategy, it creates a
detailed implementation plan as a Markdown file in your plans directory.
2. **Discuss and agree on strategy:** As Gemini CLI analyzes your codebase, it
will discuss its findings and proposed strategy with you to ensure
alignment. It may ask you questions or present different implementation
options using [`ask_user`](../tools/ask-user.md). **Gemini CLI will stop and
wait for your confirmation** before drafting the formal plan. You should
reach an informal agreement on the approach before proceeding.
3. **Review the plan:** Once you've agreed on the strategy, Gemini CLI creates
a detailed implementation plan as a Markdown file in your plans directory.
- **View:** You can open and read this file to understand the proposed
changes.
- **Edit:** Press `Ctrl+X` to open the plan directly in your configured
external editor.
4. **Approve or iterate:** Gemini CLI will present the finalized plan for your
approval.
formal approval.
- **Approve:** If you're satisfied with the plan, approve it to start the
implementation immediately: **Yes, automatically accept edits** or **Yes,
manually accept edits**.
+14 -15
View File
@@ -30,6 +30,7 @@ they appear in the UI.
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. | `false` |
| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
@@ -59,6 +60,7 @@ they appear in the UI.
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Compact Tool Output | `ui.compactToolOutput` | Display tool outputs (like directory listings and file reads) in a compact, structured format. | `false` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
@@ -72,6 +74,8 @@ they appear in the UI.
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Render Process | `ui.renderProcess` | Enable Ink render process for the UI. | `true` |
| Terminal Buffer | `ui.terminalBuffer` | Use the new terminal buffer architecture for rendering. | `true` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
@@ -155,21 +159,16 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| ---------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
| Agent History Truncation | `experimental.agentHistoryTruncation` | Enable truncation window logic for the Agent History Provider. | `false` |
| Agent History Truncation Threshold | `experimental.agentHistoryTruncationThreshold` | The maximum number of messages before history is truncated. | `30` |
| Agent History Retained Messages | `experimental.agentHistoryRetainedMessages` | The number of recent messages to retain after truncation. | `15` |
| Agent History Summarization | `experimental.agentHistorySummarization` | Enable summarization of truncated content via a small model for the Agent History Provider. | `false` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
| UI Label | Setting | Description | Default |
| ------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
### Skills
+5
View File
@@ -19,6 +19,7 @@ using the `/theme` command within Gemini CLI:
- `Holiday`
- `Shades Of Purple`
- `Solarized Dark`
- `Tokyo Night`
- **Light themes:**
- `ANSI Light`
- `Ayu Light`
@@ -252,6 +253,10 @@ identify their source, for example: `shades-of-green (green-extension)`.
<img src="/docs/assets/theme-solarized-dark.png" alt="Solarized Dark theme" width="600">
### Tokyo Night
<img src="/docs/assets/theme-tokyonight-dark.png" alt="Tokyo Night theme" width="600">
## Light themes
### ANSI Light
+2 -2
View File
@@ -7,8 +7,8 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
## Navigating this section
- **[Sub-agents (experimental)](./subagents.md):** Learn how to create and use
specialized sub-agents for complex tasks.
- **[Sub-agents](./subagents.md):** Learn how to create and use specialized
sub-agents for complex tasks.
- **[Core tools reference](../reference/tools.md):** Information on how tools
are defined, registered, and used by the core.
- **[Memory Import Processor](../reference/memport.md):** Documentation for the
+134 -23
View File
@@ -1,20 +1,16 @@
# Subagents (experimental)
# Subagents
Subagents are specialized agents that operate within your main Gemini CLI
session. They are designed to handle specific, complex tasks—like deep codebase
analysis, documentation lookup, or domain-specific reasoning—without cluttering
the main agent's context or toolset.
<!-- prettier-ignore -->
> [!NOTE]
> Subagents are currently an experimental feature.
>
To use custom subagents, you must ensure they are enabled in your
`settings.json` (enabled by default):
Subagents are enabled by default. To disable them, set `enableAgents` to `false`
in your `settings.json`:
```json
{
"experimental": { "enableAgents": true }
"experimental": { "enableAgents": false }
}
```
@@ -226,19 +222,65 @@ the `click_at` tool for precise, coordinate-based interactions.
> The visual agent requires API key or Vertex AI authentication. It is
> not available when using "Sign in with Google".
#### Sandbox support
The browser agent adjusts its behavior automatically when running inside a
sandbox.
##### macOS seatbelt (`sandbox-exec`)
When the CLI runs under the macOS seatbelt sandbox, `persistent` and `isolated`
session modes are forced to `isolated` with `headless` enabled. This avoids
permission errors caused by seatbelt file-system restrictions on persistent
browser profiles. If `sessionMode` is set to `existing`, no override is applied.
##### Container sandboxes (Docker / Podman)
Chrome is not available inside the container, so the browser agent is
**disabled** unless `sessionMode` is set to `"existing"`. When enabled with
`existing` mode, the agent automatically connects to Chrome on the host via the
resolved IP of `host.docker.internal:9222` instead of using local pipe
discovery. Port `9222` is currently hardcoded and cannot be customized.
To use the browser agent in a Docker sandbox:
1. Start Chrome on the host with remote debugging enabled:
```bash
# Option A: Launch Chrome from the command line
google-chrome --remote-debugging-port=9222
# Option B: Enable in Chrome settings
# Navigate to chrome://inspect/#remote-debugging and enable
```
2. Configure `sessionMode` and allowed domains in your project's
`.gemini/settings.json`:
```json
{
"agents": {
"overrides": {
"browser_agent": { "enabled": true }
},
"browser": {
"sessionMode": "existing",
"allowedDomains": ["example.com"]
}
}
}
```
3. Launch the CLI with port forwarding:
```bash
GEMINI_SANDBOX=docker SANDBOX_PORTS=9222 gemini
```
## Creating custom subagents
You can create your own subagents to automate specific workflows or enforce
specific personas. To use custom subagents, you must enable them in your
`settings.json`:
```json
{
"experimental": {
"enableAgents": true
}
}
```
specific personas.
### Agent definition files
@@ -290,6 +332,7 @@ it yourself; just report it.
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
| `kind` | string | No | `local` (default) or `remote`. |
| `tools` | array | No | List of tool names this agent can use. Supports wildcards: `*` (all tools), `mcp_*` (all MCP tools), `mcp_server_*` (all tools from a server). **If omitted, it inherits all tools from the parent session.** |
| `mcpServers` | object | No | Configuration for inline Model Context Protocol (MCP) servers isolated to this specific agent. |
| `model` | string | No | Specific model to use (e.g., `gemini-3-preview`). Defaults to `inherit` (uses the main session model). |
| `temperature` | number | No | Model temperature (0.0 - 2.0). Defaults to `1`. |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `30`. |
@@ -317,6 +360,78 @@ Each subagent runs in its own isolated context loop. This means:
subagents **cannot** call other subagents. If a subagent is granted the `*`
tool wildcard, it will still be unable to see or invoke other agents.
## Subagent tool isolation
Subagent tool isolation moves Gemini CLI away from a single global tool
registry. By providing isolated execution environments, you can ensure that
subagents only interact with the parts of the system they are designed for. This
prevents unintended side effects, improves reliability by avoiding state
contamination, and enables fine-grained permission control.
With this feature, you can:
- **Specify tool access:** Define exactly which tools an agent can access using
a `tools` list in the agent definition.
- **Define inline MCP servers:** Configure Model Context Protocol (MCP) servers
(which provide a standardized way to connect AI models to external tools and
data sources) directly in the subagent's markdown frontmatter, isolating them
to that specific agent.
- **Maintain state isolation:** Ensure that subagents only interact with their
own set of tools and servers, preventing side effects and state contamination.
- **Apply subagent-specific policies:** Enforce granular rules in your
[Policy Engine](../reference/policy-engine.md) TOML configuration based on the
executing subagent's name.
### Configuring isolated tools and servers
You can configure tool isolation for a subagent by updating its markdown
frontmatter. This allows you to explicitly state which tools the subagent can
use, rather than relying on the global registry.
Add an `mcpServers` object to define inline MCP servers that are unique to the
agent.
**Example:**
```yaml
---
name: my-isolated-agent
tools:
- grep_search
- read_file
mcpServers:
my-custom-server:
command: 'node'
args: ['path/to/server.js']
---
```
### Subagent-specific policies
You can enforce fine-grained control over subagents using the
[Policy Engine's](../reference/policy-engine.md) TOML configuration. This allows
you to grant or restrict permissions specifically for an agent, without
affecting the rest of your CLI session.
To restrict a policy rule to a specific subagent, add the `subagent` property to
the `[[rules]]` block in your `policy.toml` file.
**Example:**
```toml
[[rules]]
name = "Allow pr-creator to push code"
subagent = "pr-creator"
description = "Permit pr-creator to push branches automatically."
action = "allow"
toolName = "run_shell_command"
commandPrefix = "git push"
```
In this configuration, the policy rule only triggers if the executing subagent's
name matches `pr-creator`. Rules without the `subagent` property apply
universally to all agents.
## Managing subagents
You can manage subagents interactively using the `/agents` command or
@@ -406,15 +521,11 @@ If you need to further tune your subagent, you can do so by selecting the model
to optimize for with `/model` and then asking the model why it does not think
that your subagent was called with a specific prompt and the given description.
## Remote subagents (Agent2Agent) (experimental)
## Remote subagents (Agent2Agent)
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
(A2A) protocol.
<!-- prettier-ignore -->
> [!NOTE]
> Remote subagents are currently an experimental feature.
See the [Remote Subagents documentation](remote-agents) for detailed
configuration, authentication, and usage instructions.
+1 -3
View File
@@ -17,8 +17,6 @@ Slash commands provide meta-level control over the CLI itself.
### `/agents`
- **Description:** Manage local and remote subagents.
- **Note:** This command is experimental and requires
`experimental.enableAgents: true` in your `settings.json`.
- **Sub-commands:**
- **`list`**:
- **Description:** Lists all discovered agents, including built-in, local,
@@ -305,7 +303,7 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Switch to Plan Mode (read-only) and view the current plan if
one has been generated.
- **Note:** This feature is enabled by default. It can be disabled via the
`experimental.plan` setting in your configuration.
`general.plan.enabled` setting in your configuration.
- **Sub-commands:**
- **`copy`**:
- **Description:** Copy the currently approved plan to your clipboard.
+86 -47
View File
@@ -141,6 +141,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`general.plan.enabled`** (boolean):
- **Description:** Enable Plan Mode for read-only safety during planning.
- **Default:** `true`
- **Requires restart:** Yes
- **`general.plan.directory`** (string):
- **Description:** The directory where planning artifacts are stored. If not
specified, defaults to the system temporary directory. A custom directory
@@ -257,6 +262,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Show the "? for shortcuts" hint above the input.
- **Default:** `true`
- **`ui.compactToolOutput`** (boolean):
- **Description:** Display tool outputs (like directory listings and file
reads) in a compact, structured format.
- **Default:** `false`
- **`ui.hideBanner`** (boolean):
- **Description:** Hide the application banner
- **Default:** `false`
@@ -327,6 +337,16 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`ui.renderProcess`** (boolean):
- **Description:** Enable Ink render process for the UI.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.terminalBuffer`** (boolean):
- **Description:** Use the new terminal buffer architecture for rendering.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.useBackgroundColor`** (boolean):
- **Description:** Whether to use background colors in the UI.
- **Default:** `true`
@@ -1577,28 +1597,6 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.toolOutputMasking.enabled`** (boolean):
- **Description:** Enables tool output masking to save tokens.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.toolOutputMasking.toolProtectionThreshold`** (number):
- **Description:** Minimum number of tokens to protect from masking (most
recent tool outputs).
- **Default:** `50000`
- **Requires restart:** Yes
- **`experimental.toolOutputMasking.minPrunableTokensThreshold`** (number):
- **Description:** Minimum prunable tokens required to trigger a masking pass.
- **Default:** `30000`
- **Requires restart:** Yes
- **`experimental.toolOutputMasking.protectLatestTurn`** (boolean):
- **Description:** Ensures the absolute latest turn is never masked,
regardless of token count.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents.
- **Default:** `true`
@@ -1637,7 +1635,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading.
- **Default:** `true`
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
@@ -1652,11 +1650,6 @@ their corresponding top-level category object in your `settings.json` file.
configured to allow it).
- **Default:** `false`
- **`experimental.plan`** (boolean):
- **Description:** Enable Plan Mode.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.taskTracker`** (boolean):
- **Description:** Enable task tracker tools.
- **Default:** `false`
@@ -1702,25 +1695,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.agentHistoryTruncation`** (boolean):
- **Description:** Enable truncation window logic for the Agent History
Provider.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.agentHistoryTruncationThreshold`** (number):
- **Description:** The maximum number of messages before history is truncated.
- **Default:** `30`
- **Requires restart:** Yes
- **`experimental.agentHistoryRetainedMessages`** (number):
- **Description:** The number of recent messages to retain after truncation.
- **Default:** `15`
- **Requires restart:** Yes
- **`experimental.agentHistorySummarization`** (boolean):
- **Description:** Enable summarization of truncated content via a small model
for the Agent History Provider.
- **`experimental.contextManagement`** (boolean):
- **Description:** Enable logic for context management.
- **Default:** `false`
- **Requires restart:** Yes
@@ -1815,6 +1791,69 @@ their corresponding top-level category object in your `settings.json` file.
prioritize available tools dynamically.
- **Default:** `[]`
#### `contextManagement`
- **`contextManagement.historyWindow.maxTokens`** (number):
- **Description:** The number of tokens to allow before triggering
compression.
- **Default:** `150000`
- **Requires restart:** Yes
- **`contextManagement.historyWindow.retainedTokens`** (number):
- **Description:** The number of tokens to always retain.
- **Default:** `40000`
- **Requires restart:** Yes
- **`contextManagement.messageLimits.normalMaxTokens`** (number):
- **Description:** The target number of tokens to budget for a normal
conversation turn.
- **Default:** `2500`
- **Requires restart:** Yes
- **`contextManagement.messageLimits.retainedMaxTokens`** (number):
- **Description:** The maximum number of tokens a single conversation turn can
consume before truncation.
- **Default:** `12000`
- **Requires restart:** Yes
- **`contextManagement.messageLimits.normalizationHeadRatio`** (number):
- **Description:** The ratio of tokens to retain from the beginning of a
truncated message (0.0 to 1.0).
- **Default:** `0.25`
- **Requires restart:** Yes
- **`contextManagement.tools.distillation.maxOutputTokens`** (number):
- **Description:** Maximum tokens to show to the model when truncating large
tool outputs.
- **Default:** `10000`
- **Requires restart:** Yes
- **`contextManagement.tools.distillation.summarizationThresholdTokens`**
(number):
- **Description:** Threshold above which truncated tool outputs will be
summarized by an LLM.
- **Default:** `20000`
- **Requires restart:** Yes
- **`contextManagement.tools.outputMasking.protectionThresholdTokens`**
(number):
- **Description:** Minimum number of tokens to protect from masking (most
recent tool outputs).
- **Default:** `50000`
- **Requires restart:** Yes
- **`contextManagement.tools.outputMasking.minPrunableThresholdTokens`**
(number):
- **Description:** Minimum prunable tokens required to trigger a masking pass.
- **Default:** `30000`
- **Requires restart:** Yes
- **`contextManagement.tools.outputMasking.protectLatestTurn`** (boolean):
- **Description:** Ensures the absolute latest turn is never masked,
regardless of token count.
- **Default:** `true`
- **Requires restart:** Yes
#### `admin`
- **`admin.secureModeEnabled`** (boolean):
+12 -1
View File
@@ -102,7 +102,8 @@ available combinations.
| `app.showFullTodos` | Toggle the full TODO list. | `Ctrl+T` |
| `app.showIdeContextDetail` | Show IDE context details. | `Ctrl+G` |
| `app.toggleMarkdown` | Toggle Markdown rendering. | `Alt+M` |
| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `Ctrl+S` |
| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `F9` |
| `app.toggleMouseMode` | Toggle mouse mode (scrolling and clicking). | `Ctrl+S` |
| `app.toggleYolo` | Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` |
| `app.cycleApprovalMode` | Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` |
| `app.showMoreLines` | Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` |
@@ -126,6 +127,16 @@ available combinations.
| `background.unfocus` | Move focus from background shell to Gemini. | `Shift+Tab` |
| `background.unfocusList` | Move focus from background shell list to Gemini. | `Tab` |
| `background.unfocusWarning` | Show warning when trying to move focus away from background shell. | `Tab` |
| `app.dumpFrame` | Dump the current frame as a snapshot. | `F8` |
| `app.startRecording` | Start recording the session. | `F6` |
| `app.stopRecording` | Stop recording the session. | `F7` |
#### Extension Controls
| Command | Action | Keys |
| ------------------ | ------------------------------------------- | ---- |
| `extension.update` | Update the current extension if available. | `I` |
| `extension.link` | Link the current extension to a local path. | `L` |
<!-- KEYBINDINGS-AUTOGEN:END -->
+16 -30
View File
@@ -29,13 +29,12 @@ To create your first policy:
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git status"
decision = "allow"
commandPrefix = "rm -rf"
decision = "deny"
priority = 100
```
3. **Run a command** that triggers the policy (e.g., ask Gemini CLI to
`git status`). The tool will now execute automatically without prompting for
confirmation.
`rm -rf /`). The tool will now be blocked automatically.
## Core concepts
@@ -143,25 +142,26 @@ engine transforms this into a final priority using the following formula:
This system guarantees that:
- Admin policies always override User, Workspace, and Default policies.
- Admin policies always override User, Workspace, and Default policies (defined
in policy TOML files).
- User policies override Workspace and Default policies.
- Workspace policies override Default policies.
- You can still order rules within a single tier with fine-grained control.
For example:
- A `priority: 50` rule in a Default policy file becomes `1.050`.
- A `priority: 10` rule in a Workspace policy policy file becomes `2.010`.
- A `priority: 100` rule in a User policy file becomes `3.100`.
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
- A `priority: 50` rule in a Default policy TOML becomes `1.050`.
- A `priority: 10` rule in a Workspace policy TOML becomes `2.010`.
- A `priority: 100` rule in a User policy TOML becomes `3.100`.
- A `priority: 20` rule in an Admin policy TOML becomes `4.020`.
### Approval modes
Approval modes allow the policy engine to apply different sets of rules based on
the CLI's operational mode. A rule can be associated with one or more modes
(e.g., `yolo`, `autoEdit`, `plan`). The rule will only be active if the CLI is
running in one of its specified modes. If a rule has no modes specified, it is
always active.
the CLI's operational mode. A rule in a TOML policy file can be associated with
one or more modes (e.g., `yolo`, `autoEdit`, `plan`). The rule will only be
active if the CLI is running in one of its specified modes. If a rule has no
modes specified, it is always active.
- `default`: The standard interactive mode where most write tools require
confirmation.
@@ -179,8 +179,8 @@ outcome.
A rule matches a tool call if all of its conditions are met:
1. **Tool name**: The `toolName` in the rule must match the name of the tool
being called.
1. **Tool name**: The `toolName` in the TOML rule must match the name of the
tool being called.
- **Wildcards**: You can use wildcards like `*`, `mcp_server_*`, or
`mcp_*_toolName` to match multiple tools. See [Tool Name](#tool-name) for
details.
@@ -264,7 +264,7 @@ toolName = "run_shell_command"
# (Optional) The name of a subagent. If provided, the rule only applies to tool
# calls made by this specific subagent.
subagent = "generalist"
subagent = "codebase_investigator"
# (Optional) The name of an MCP server. Can be combined with toolName
# to form a composite FQN internally like "mcp_mcpName_toolName".
@@ -419,20 +419,6 @@ decision = "ask_user"
priority = 10
```
**4. Targeting a tool name across all servers**
Use `mcpName = "*"` with a specific `toolName` to target that operation
regardless of which server provides it.
```toml
# Allow the `search` tool across all connected MCP servers
[[rule]]
mcpName = "*"
toolName = "search"
decision = "allow"
priority = 50
```
## Default policies
The Gemini CLI ships with a set of default policies to provide a safe
-2
View File
@@ -138,12 +138,10 @@
{ "label": "Plan mode", "slug": "docs/cli/plan-mode" },
{
"label": "Subagents",
"badge": "🔬",
"slug": "docs/core/subagents"
},
{
"label": "Remote subagents",
"badge": "🔬",
"slug": "docs/core/remote-agents"
},
{ "label": "Rewind", "slug": "docs/cli/rewind" },
+6 -4
View File
@@ -32,7 +32,9 @@ and planning.
## 2. `exit_plan_mode` (ExitPlanMode)
`exit_plan_mode` signals that the planning phase is complete. It presents the
finalized plan to the user and requests approval to start the implementation.
finalized plan to the user and requests formal approval to start the
implementation. The agent MUST reach an informal agreement with the user in the
chat regarding the proposed strategy BEFORE calling this tool.
- **Tool name:** `exit_plan_mode`
- **Display name:** Exit Plan Mode
@@ -44,7 +46,7 @@ finalized plan to the user and requests approval to start the implementation.
- **Behavior:**
- Validates that the `plan_path` is within the allowed directory and that the
file exists and has content.
- Presents the plan to the user for review.
- Presents the plan to the user for formal review.
- If the user approves the plan:
- Switches the CLI's approval mode to the user's chosen approval mode (
`DEFAULT` or `AUTO_EDIT`).
@@ -56,5 +58,5 @@ finalized plan to the user and requests approval to start the implementation.
- On approval: A message indicating the plan was approved and the new approval
mode.
- On rejection: A message containing the user's feedback.
- **Confirmation:** Yes. Shows the finalized plan and asks for user approval to
proceed with implementation.
- **Confirmation:** Yes. Shows the finalized plan and asks for user formal
approval to proceed with implementation.
+82 -3
View File
@@ -15,7 +15,9 @@ import {
describe('plan_mode', () => {
const TEST_PREFIX = 'Plan Mode: ';
const settings = {
experimental: { plan: true },
general: {
plan: { enabled: true },
},
};
const getWriteTargets = (logs: any[]) =>
@@ -172,7 +174,8 @@ describe('plan_mode', () => {
params: {
settings,
},
prompt: 'Create a plan for a new login feature.',
prompt:
'I agree with the strategy to use a JWT-based login. Create a plan for a new login feature.',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
@@ -209,7 +212,7 @@ describe('plan_mode', () => {
'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.',
'I want to refactor our math utilities. I agree with the strategy to move the `sum` function from `src/mathUtils.ts` to a new file `src/basicMath.ts` and update `src/main.ts`. Please create a detailed implementation plan first, then execute it.',
assert: async (rig, result) => {
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
expect(
@@ -281,4 +284,80 @@ describe('plan_mode', () => {
assertModelHasOutput(result);
},
});
evalTest('ALWAYS_PASSES', {
name: 'should transition from plan mode to normal execution and create a plan file from scratch',
params: {
settings,
},
prompt:
'Enter plan mode and plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
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 the plan file was written successfully
const planWrite = toolLogs.find(
(log) =>
log.toolRequest.name === 'write_file' &&
log.toolRequest.args.includes('foo-plan.md'),
);
expect(
planWrite,
'Expected write_file to be called for foo-plan.md',
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but got error: ${planWrite?.toolRequest.error}`,
).toBe(true);
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should not exit plan mode or draft before informal agreement',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
prompt: 'I need to build a new login feature. Please plan it.',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const exitPlanCall = toolLogs.find(
(log) => log.toolRequest.name === 'exit_plan_mode',
);
expect(
exitPlanCall,
'Should NOT call exit_plan_mode before informal agreement',
).toBeUndefined();
const planWrite = toolLogs.find(
(log) =>
log.toolRequest.name === 'write_file' &&
log.toolRequest.args.includes('/plans/'),
);
expect(
planWrite,
'Should NOT draft the plan file before informal agreement',
).toBeUndefined();
assertModelHasOutput(result);
},
});
});
+17
View File
@@ -113,4 +113,21 @@ describe('tracker_mode', () => {
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should correctly identify the task tracker storage location from the system prompt',
params: {
settings: { experimental: { taskTracker: true } },
},
prompt:
'Where is my task tracker storage located? Please provide the absolute path in your response.',
assert: async (rig, result) => {
// The rig sets GEMINI_CLI_HOME to rig.homeDir
const homeDir = rig.homeDir!;
// The response should contain the dynamic path which includes the home directory
// and follows the .gemini/tmp/.../tracker structure.
expect(result).toContain(homeDir);
expect(result).toMatch(/\.gemini\/tmp\/.*\/tracker/);
},
});
});
+116
View File
@@ -0,0 +1,116 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('update_topic_behavior', () => {
// Constants for tool names and params for robustness
const UPDATE_TOPIC_TOOL_NAME = 'update_topic';
/**
* Verifies the desired behavior of the update_topic tool. update_topic is used by the
* agent to share periodic, concise updates about what the agent is working on, independent
* of the regular model output and/or thoughts. This tool is expected to be called at least
* at the start and end of the session, and typically at least once in the middle, but no
* more than 1/4 turns.
*/
evalTest('USUALLY_PASSES', {
name: 'update_topic should be used at start, end and middle for complex tasks',
prompt: `Create a simple users REST API using Express.
1. Initialize a new npm project and install express.
2. Create src/app.ts as the main entry point.
3. Create src/routes/userRoutes.ts for user routes.
4. Create src/controllers/userController.ts for user logic.
5. Implement GET /users, POST /users, and GET /users/:id using an in-memory array.
6. Add a 'start' script to package.json.
7. Finally, run a quick grep to verify the routes are in src/app.ts.`,
files: {
'package.json': JSON.stringify(
{
name: 'users-api',
version: '1.0.0',
private: true,
},
null,
2,
),
'.gemini/settings.json': JSON.stringify({
experimental: {
topicUpdateNarration: true,
},
}),
},
assert: async (rig, result) => {
const toolLogs = rig.readToolLogs();
const topicCalls = toolLogs.filter(
(l) => l.toolRequest.name === UPDATE_TOPIC_TOOL_NAME,
);
// 1. Assert that update_topic is called at least 3 times (start, middle, end)
expect(
topicCalls.length,
`Expected at least 3 update_topic calls, but found ${topicCalls.length}`,
).toBeGreaterThanOrEqual(3);
// 2. Assert update_topic is called at the very beginning (first tool call)
expect(
toolLogs[0].toolRequest.name,
'First tool call should be update_topic',
).toBe(UPDATE_TOPIC_TOOL_NAME);
// 3. Assert update_topic is called near the end
const lastTopicCallIndex = toolLogs
.map((l) => l.toolRequest.name)
.lastIndexOf(UPDATE_TOPIC_TOOL_NAME);
expect(
lastTopicCallIndex,
'Expected update_topic to be used near the end of the task',
).toBeGreaterThanOrEqual(toolLogs.length * 0.7);
// 4. Assert there is at least one update_topic call in the middle (between start and end phases)
const middleTopicCalls = topicCalls.slice(1, -1);
expect(
middleTopicCalls.length,
'Expected at least one update_topic call in the middle of the task',
).toBeGreaterThanOrEqual(1);
// 5. Turn Ratio Assertion: update_topic should be <= 1/2 of total turns.
// We only enforce this for tasks that take more than 5 turns, as shorter tasks
// naturally have a higher ratio when following the "start, middle, end" rule.
const uniquePromptIds = new Set(
toolLogs
.map((l) => l.toolRequest.prompt_id)
.filter((id) => id !== undefined),
);
const totalTurns = uniquePromptIds.size;
if (totalTurns > 5) {
const topicTurns = new Set(
topicCalls
.map((l) => l.toolRequest.prompt_id)
.filter((id) => id !== undefined),
);
const topicTurnCount = topicTurns.size;
const ratio = topicTurnCount / totalTurns;
expect(
ratio,
`update_topic was used in ${topicTurnCount} out of ${totalTurns} turns (${(ratio * 100).toFixed(1)}%). Expected <= 50%.`,
).toBeLessThanOrEqual(0.5);
// Ideal ratio is closer to 1/5 (20%). We log high usage as a warning.
if (ratio > 0.25) {
console.warn(
`[Efficiency Warning] update_topic usage is high: ${(ratio * 100).toFixed(1)}% (Goal: ~20%)`,
);
}
}
},
});
});
+1 -1
View File
@@ -1 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Part 1. "}],"role":"model"},"index":0}]},{"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":10,"totalTokenCount":110}},{"candidates":[{"content":{"parts":[{"text":"Part 2."}],"role":"model"},"index":0}],"finishReason":"STOP"}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Part 1. "}],"role":"model"},"index":0}]},{"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":10,"totalTokenCount":110}},{"candidates":[{"content":{"parts":[{"text":"Part 2."}],"role":"model"},"index":0,"finishReason":"STOP"}]}]}
+123 -94
View File
@@ -10,8 +10,13 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
import { env } from 'node:process';
import stripAnsi from 'strip-ansi';
// Browser agent Chrome DevTools MCP connection is flaky in Docker sandbox.
// See: https://github.com/google-gemini/gemini-cli/issues/24382
const isDockerSandbox = env['GEMINI_SANDBOX'] === 'docker';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -59,122 +64,146 @@ describe.skipIf(!chromeAvailable)('browser-policy', () => {
await rig.cleanup();
});
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
agents: {
overrides: {
browser_agent: {
enabled: true,
it.skipIf(isDockerSandbox)(
'should skip confirmation when "Allow all server tools for this session" is chosen',
async () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
agents: {
overrides: {
browser_agent: {
enabled: true,
},
},
browser: {
headless: true,
sessionMode: 'isolated',
allowedDomains: ['example.com'],
},
},
browser: {
headless: true,
sessionMode: 'isolated',
allowedDomains: ['example.com'],
},
},
},
});
});
// Manually trust the folder to avoid the dialog and enable option 3
const geminiDir = join(rig.homeDir!, '.gemini');
mkdirSync(geminiDir, { recursive: true });
// Manually trust the folder to avoid the dialog and enable option 3
const geminiDir = join(rig.homeDir!, '.gemini');
mkdirSync(geminiDir, { recursive: true });
// Write to trustedFolders.json
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
const trustedFolders = {
[rig.testDir!]: 'TRUST_FOLDER',
};
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
// Write to trustedFolders.json
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
const trustedFolders = {
[rig.testDir!]: 'TRUST_FOLDER',
};
writeFileSync(
trustedFoldersPath,
JSON.stringify(trustedFolders, null, 2),
);
// Force confirmation for browser agent.
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
// a rule in the user tier (4.x) like the one from this TOML.
// By removing the explicit mcp rule, the first MCP tool will still prompt
// due to default approvalMode = 'default', and then "Allow all" will correctly
// bypass subsequent tools.
const policyFile = join(rig.testDir!, 'force-confirm.toml');
writeFileSync(
policyFile,
`
// Force confirmation for browser agent.
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
// a rule in the user tier (4.x) like the one from this TOML.
// By removing the explicit mcp rule, the first MCP tool will still prompt
// due to default approvalMode = 'default', and then "Allow all" will correctly
// bypass subsequent tools.
const policyFile = join(rig.testDir!, 'force-confirm.toml');
writeFileSync(
policyFile,
`
[[rule]]
name = "Force confirm browser_agent"
toolName = "browser_agent"
decision = "ask_user"
priority = 200
`,
);
);
// Update settings.json in both project and home directories to point to the policy file
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
const settingsPath = join(baseDir, '.gemini', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
settings.policyPaths = [policyFile];
// Ensure folder trust is enabled
settings.security = settings.security || {};
settings.security.folderTrust = settings.security.folderTrust || {};
settings.security.folderTrust.enabled = true;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
// Update settings.json in both project and home directories to point to the policy file
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
const settingsPath = join(baseDir, '.gemini', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
settings.policyPaths = [policyFile];
// Ensure folder trust is enabled
settings.security = settings.security || {};
settings.security.folderTrust = settings.security.folderTrust || {};
settings.security.folderTrust.enabled = true;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
}
}
}
const run = await rig.runInteractive({
approvalMode: 'default',
env: {
GEMINI_CLI_INTEGRATION_TEST: 'true',
},
});
const run = await rig.runInteractive({
approvalMode: 'default',
env: {
GEMINI_CLI_INTEGRATION_TEST: 'true',
},
});
await run.sendKeys(
'Open https://example.com and check if there is a heading\r',
);
await run.sendKeys('\r');
await run.sendKeys(
'Open https://example.com and check if there is a heading\r',
);
await run.sendKeys('\r');
// Handle confirmations.
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
await poll(
() => stripAnsi(run.output).toLowerCase().includes('action required'),
60000,
1000,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 2000));
// Handle confirmations.
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
await poll(
() => stripAnsi(run.output).toLowerCase().includes('action required'),
60000,
1000,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 2000));
// Handle privacy notice
await poll(
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
5000,
100,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 5000));
// Handle privacy notice
await poll(
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
5000,
100,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 5000));
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
await poll(
() => {
const stripped = stripAnsi(run.output).toLowerCase();
return (
stripped.includes('new_page') &&
stripped.includes('allow all server tools for this session')
);
},
60000,
1000,
);
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
await poll(
() => {
const stripped = stripAnsi(run.output).toLowerCase();
return (
stripped.includes('new_page') &&
stripped.includes('allow all server tools for this session')
);
},
60000,
1000,
);
// Select "Allow all server tools for this session" (option 3)
await run.sendKeys('3\r');
await new Promise((r) => setTimeout(r, 30000));
// Select "Allow all server tools for this session" (option 3)
await run.sendKeys('3\r');
const output = stripAnsi(run.output).toLowerCase();
// Wait for the browser agent to finish (success or failure)
await poll(
() => {
const stripped = stripAnsi(run.output).toLowerCase();
return (
stripped.includes('completed successfully') ||
stripped.includes('agent error')
);
},
120000,
1000,
);
expect(output).toContain('browser_agent');
expect(output).toContain('completed successfully');
});
const output = stripAnsi(run.output).toLowerCase();
expect(output).toContain('browser_agent');
// The test validates that "Allow all server tools" skips subsequent
// tool confirmations — the browser agent may still fail due to
// Chrome/MCP issues in CI, which is acceptable for this policy test.
expect(
output.includes('completed successfully') ||
output.includes('agent error'),
).toBe(true);
},
);
it('should show the visible warning when browser agent starts in existing session mode', async () => {
rig.setup('browser-session-warning', {
+1
View File
@@ -121,6 +121,7 @@ describe('file-system', () => {
const result = await rig.run({
args: `write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
timeout: 600000, // 10 min — real LLM can be slow in Docker sandbox
});
const foundToolCall = await rig.waitForToolCall('write_file');
+10 -15
View File
@@ -23,7 +23,9 @@ describe('Plan Mode', () => {
'should allow read-only tools but deny write tools in plan mode',
{
settings: {
experimental: { plan: true },
general: {
plan: { enabled: true },
},
tools: {
core: [
'run_shell_command',
@@ -67,15 +69,12 @@ describe('Plan Mode', () => {
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
plan: { enabled: true, directory: plansDir },
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
},
},
},
});
@@ -120,22 +119,19 @@ describe('Plan Mode', () => {
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
plan: { enabled: true, directory: plansDir },
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
},
},
},
});
await rig.run({
approvalMode: 'plan',
args: 'Create a file called hello.txt in the current directory.',
args: 'Attempt to create a file named "hello.txt" in the current directory. Do not create a plan file, try to write hello.txt directly.',
});
const toolLogs = rig.readToolLogs();
@@ -156,7 +152,9 @@ describe('Plan Mode', () => {
it('should be able to enter plan mode from default mode', async () => {
await rig.setup('should be able to enter plan mode from default mode', {
settings: {
experimental: { plan: true },
general: {
plan: { enabled: true },
},
tools: {
core: ['enter_plan_mode'],
allowed: ['enter_plan_mode'],
@@ -184,15 +182,12 @@ describe('Plan Mode', () => {
await rig.setup(testName, {
settings: {
experimental: { plan: true },
tools: {
core: ['write_file', 'read_file', 'list_directory'],
},
general: {
plan: { enabled: true, directory: plansDir },
defaultApprovalMode: 'plan',
plan: {
directory: plansDir,
},
},
},
});
+25 -50
View File
@@ -11,7 +11,7 @@
"packages/*"
],
"dependencies": {
"ink": "npm:@jrichman/ink@6.5.0",
"ink": "npm:@jrichman/ink@6.6.5",
"latest-version": "^9.0.0",
"node-fetch-native": "^1.6.7",
"proper-lockfile": "^4.1.2",
@@ -92,46 +92,6 @@
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/@alcalzone/ansi-tokenize": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.2.tgz",
"integrity": "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -10089,14 +10049,13 @@
},
"node_modules/ink": {
"name": "@jrichman/ink",
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.5.0.tgz",
"integrity": "sha512-S4g/ng7fPZmFwclO82iWkOce8vDLy/FIDgHIfkCWGOehqHe6dexHsmq3kNQD21okh198pA5SAQTCqNQJb/svRQ==",
"version": "6.6.5",
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.5.tgz",
"integrity": "sha512-Q+XCBkDH63qCpUkq31CfIYaWUVheCUps/bCjhZa1d0dhMDLT5xiG4BCV660Rz5N5ZsHCQbANTF/lleoL9Uo00Q==",
"license": "MIT",
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
"ansi-styles": "^6.2.1",
"ansi-styles": "^6.2.3",
"auto-bind": "^5.0.1",
"chalk": "^5.6.0",
"cli-boxes": "^3.0.0",
@@ -10105,6 +10064,7 @@
"code-excerpt": "^4.0.0",
"es-toolkit": "^1.39.10",
"indent-string": "^5.0.0",
"is-fullwidth-code-point": "^5.0.0",
"is-in-ci": "^2.0.0",
"mnemonist": "^0.40.3",
"patch-console": "^2.0.0",
@@ -10174,9 +10134,9 @@
}
},
"node_modules/ink/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -10197,6 +10157,21 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ink/node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/is-in-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz",
@@ -17551,7 +17526,7 @@
"fzf": "^0.5.2",
"glob": "^12.0.0",
"highlight.js": "^11.11.1",
"ink": "npm:@jrichman/ink@6.5.0",
"ink": "npm:@jrichman/ink@6.6.5",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
+3 -3
View File
@@ -38,7 +38,7 @@
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"build:binary": "node scripts/build_binary.js",
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && npm run bundle:browser-mcp -w @google/gemini-cli-core && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present && npm run test:sea-launch",
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts && npm run test:sea-launch",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
@@ -68,7 +68,7 @@
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {
"ink": "npm:@jrichman/ink@6.5.0",
"ink": "npm:@jrichman/ink@6.6.5",
"wrap-ansi": "9.0.2",
"cliui": {
"wrap-ansi": "7.0.0"
@@ -136,7 +136,7 @@
"yargs": "^17.7.2"
},
"dependencies": {
"ink": "npm:@jrichman/ink@6.5.0",
"ink": "npm:@jrichman/ink@6.6.5",
"latest-version": "^9.0.0",
"node-fetch-native": "^1.6.7",
"proper-lockfile": "^4.1.2",
@@ -109,12 +109,8 @@ export function createMockConfig(
enableEnvironmentVariableRedaction: false,
},
}),
isExperimentalAgentHistoryTruncationEnabled: vi.fn().mockReturnValue(false),
getExperimentalAgentHistoryTruncationThreshold: vi.fn().mockReturnValue(50),
getExperimentalAgentHistoryRetainedMessages: vi.fn().mockReturnValue(30),
isExperimentalAgentHistorySummarizationEnabled: vi
.fn()
.mockReturnValue(false),
isAutoDistillationEnabled: vi.fn().mockReturnValue(false),
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
...overrides,
} as unknown as Config;
+1 -1
View File
@@ -49,7 +49,7 @@
"fzf": "^0.5.2",
"glob": "^12.0.0",
"highlight.js": "^11.11.1",
"ink": "npm:@jrichman/ink@6.5.0",
"ink": "npm:@jrichman/ink@6.6.5",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
+75 -12
View File
@@ -27,6 +27,7 @@ import {
type MessageBus,
LlmRole,
type GitService,
type ModelRouterService,
processSingleFileContent,
InvalidStreamError,
} from '@google/gemini-cli-core';
@@ -102,17 +103,7 @@ vi.mock(
...actual,
updatePolicy: vi.fn(),
createPolicyUpdater: vi.fn(),
ReadManyFilesTool: vi.fn().mockImplementation(() => ({
name: 'read_many_files',
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Read files',
toolLocations: () => [],
execute: vi.fn().mockResolvedValue({
llmContent: ['--- file.txt ---\n\nFile content\n\n'],
}),
}),
})),
ReadManyFilesTool: vi.fn(),
logToolCall: vi.fn(),
LlmRole: {
MAIN: 'main',
@@ -421,6 +412,26 @@ describe('GeminiAgent', () => {
);
});
it('should include gemini-3.1-flash-lite when useGemini31FlashLite is true', async () => {
mockConfig.getHasAccessToPreviewModel = vi.fn().mockReturnValue(true);
mockConfig.getGemini31LaunchedSync = vi.fn().mockReturnValue(true);
mockConfig.getGemini31FlashLiteLaunchedSync = vi.fn().mockReturnValue(true);
const response = await agent.newSession({
cwd: '/tmp',
mcpServers: [],
});
expect(response.models?.availableModels).toEqual(
expect.arrayContaining([
expect.objectContaining({
modelId: 'gemini-3.1-flash-lite-preview',
name: 'gemini-3.1-flash-lite-preview',
}),
]),
);
});
it('should return modes with plan mode when plan is enabled', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
@@ -646,6 +657,7 @@ describe('Session', () => {
sendMessageStream: vi.fn(),
addHistory: vi.fn(),
recordCompletedToolCalls: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
} as unknown as Mocked<GeminiChat>;
mockTool = {
kind: 'read',
@@ -667,6 +679,9 @@ describe('Session', () => {
mockConfig = {
getModel: vi.fn().mockReturnValue('gemini-pro'),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getModelRouterService: vi.fn().mockReturnValue({
route: vi.fn().mockResolvedValue({ model: 'resolved-model' }),
}),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getMcpServers: vi.fn(),
getFileService: vi.fn().mockReturnValue({
@@ -713,10 +728,22 @@ describe('Session', () => {
},
errors: [],
} as unknown as LoadedSettings);
(ReadManyFilesTool as unknown as Mock).mockImplementation(() => ({
name: 'read_many_files',
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Read files',
toolLocations: () => [],
execute: vi.fn().mockResolvedValue({
llmContent: ['--- file.txt ---\n\nFile content\n\n'],
}),
}),
}));
});
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});
it('should send available commands', async () => {
@@ -786,6 +813,42 @@ describe('Session', () => {
expect(result).toMatchObject({ stopReason: 'end_turn' });
});
it('should use model router to determine model', async () => {
const mockRouter = {
route: vi.fn().mockResolvedValue({ model: 'routed-model' }),
} as unknown as ModelRouterService;
mockConfig.getModelRouterService.mockReturnValue(mockRouter);
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
},
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Hi' }],
});
expect(mockRouter.route).toHaveBeenCalledWith(
expect.objectContaining({
requestedModel: 'gemini-pro',
request: [{ text: 'Hi' }],
}),
);
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
expect.objectContaining({ model: 'routed-model' }),
expect.any(Array),
expect.any(String),
expect.any(Object),
expect.any(String),
);
});
it('should handle prompt with empty response (InvalidStreamError)', async () => {
mockChat.sendMessageStream.mockRejectedValue(
new InvalidStreamError('Empty response', 'NO_RESPONSE_TEXT'),
+43 -7
View File
@@ -28,7 +28,7 @@ import {
debugLogger,
ReadManyFilesTool,
REFERENCE_CONTENT_START,
resolveModel,
type RoutingContext,
createWorkingStdio,
startupProfiler,
Kind,
@@ -42,6 +42,7 @@ import {
DEFAULT_GEMINI_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
@@ -758,10 +759,15 @@ export class Session {
const functionCalls: FunctionCall[] = [];
try {
const model = resolveModel(
this.context.config.getModel(),
(await this.context.config.getGemini31Launched?.()) ?? false,
);
const routingContext: RoutingContext = {
history: chat.getHistory(/*curated=*/ true),
request: nextMessage?.parts ?? [],
signal: pendingSend.signal,
requestedModel: this.context.config.getModel(),
};
const router = this.context.config.getModelRouterService();
const { model } = await router.route(routingContext);
const responseStream = await chat.sendMessageStream(
{ model },
nextMessage?.parts ?? [],
@@ -2009,10 +2015,31 @@ function buildAvailableModels(
const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite =
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
const selectedAuthType = settings.merged.security.auth.selectedType;
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
// --- DYNAMIC PATH ---
if (
config.getExperimentalDynamicModelConfiguration?.() === true &&
config.getModelConfigService
) {
const options = config.getModelConfigService().getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
});
return {
availableModels: options,
currentModelId: preferredModel,
};
}
// --- LEGACY PATH ---
const mainOptions = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
@@ -2056,7 +2083,7 @@ function buildAvailableModels(
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
: previewProModel;
manualOptions.unshift(
const previewOptions = [
{
value: previewProValue,
title: getDisplayString(previewProModel),
@@ -2065,7 +2092,16 @@ function buildAvailableModels(
value: PREVIEW_GEMINI_FLASH_MODEL,
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
},
);
];
if (useGemini31FlashLite) {
previewOptions.push({
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
});
}
manualOptions.unshift(...previewOptions);
}
const scaleOptions = (
+34 -26
View File
@@ -1364,8 +1364,8 @@ describe('Approval mode tool exclusion logic', () => {
'test',
];
const settings = createTestMergedSettings({
experimental: {
plan: true,
general: {
plan: { enabled: true },
},
});
const argv = await parseArguments(createTestMergedSettings());
@@ -1479,9 +1479,7 @@ describe('Approval mode tool exclusion logic', () => {
const settings = createTestMergedSettings({
general: {
defaultApprovalMode: 'plan',
},
experimental: {
plan: false,
plan: { enabled: false },
},
});
const argv = await parseArguments(settings);
@@ -1489,14 +1487,12 @@ describe('Approval mode tool exclusion logic', () => {
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should allow plan approval mode if experimental plan is enabled', async () => {
it('should allow plan approval mode if plan is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
defaultApprovalMode: 'plan',
},
experimental: {
plan: true,
plan: { enabled: true },
},
});
const argv = await parseArguments(settings);
@@ -2742,12 +2738,12 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
});
it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => {
it('should set Plan approval mode when --approval-mode=plan is used and plan is enabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
plan: true,
general: {
plan: { enabled: true },
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
@@ -2767,12 +2763,12 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
});
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
it('should throw error when --approval-mode=plan is used but plan is disabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
plan: false,
general: {
plan: { enabled: false },
},
});
@@ -2893,22 +2889,26 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
});
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
it('should respect plan mode from settings when plan is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'plan' },
experimental: { plan: true },
general: {
defaultApprovalMode: 'plan',
plan: { enabled: true },
},
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
});
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
it('should fall back to default if plan mode is in settings but disabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'plan' },
experimental: { plan: false },
general: {
defaultApprovalMode: 'plan',
plan: { enabled: false },
},
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
@@ -3696,7 +3696,9 @@ describe('loadCliConfig mcpEnabled', () => {
it('should use plan directory from active extension when user has not specified one', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
general: {
plan: { enabled: true },
},
});
const argv = await parseArguments(settings);
@@ -3715,9 +3717,11 @@ describe('loadCliConfig mcpEnabled', () => {
it('should NOT use plan directory from active extension when user has specified one', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
general: {
plan: { directory: 'user-plans-dir' },
plan: {
enabled: true,
directory: 'user-plans-dir',
},
},
});
const argv = await parseArguments(settings);
@@ -3738,7 +3742,9 @@ describe('loadCliConfig mcpEnabled', () => {
it('should NOT use plan directory from inactive extension', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
general: {
plan: { enabled: true },
},
});
const argv = await parseArguments(settings);
@@ -3759,7 +3765,9 @@ describe('loadCliConfig mcpEnabled', () => {
it('should use default path if neither user nor extension settings provide a plan directory', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
general: {
plan: { enabled: true },
},
});
const argv = await parseArguments(settings);
+9 -12
View File
@@ -669,9 +669,9 @@ export async function loadCliConfig(
approvalMode = ApprovalMode.AUTO_EDIT;
break;
case 'plan':
if (!(settings.experimental?.plan ?? false)) {
if (!(settings.general?.plan?.enabled ?? true)) {
debugLogger.warn(
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
'Approval mode "plan" is disabled in your settings. Falling back to "default".',
);
approvalMode = ApprovalMode.DEFAULT;
} else {
@@ -966,7 +966,7 @@ export async function loadCliConfig(
extensionRegistryURI,
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
plan: settings.experimental?.plan,
plan: settings.general?.plan?.enabled ?? true,
tracker: settings.experimental?.taskTracker,
directWebFetch: settings.experimental?.directWebFetch,
planSettings: settings.general?.plan?.directory
@@ -977,17 +977,12 @@ export async function loadCliConfig(
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
experimentalAgentHistoryTruncation:
settings.experimental?.agentHistoryTruncation,
experimentalAgentHistoryTruncationThreshold:
settings.experimental?.agentHistoryTruncationThreshold,
experimentalAgentHistoryRetainedMessages:
settings.experimental?.agentHistoryRetainedMessages,
experimentalAgentHistorySummarization:
settings.experimental?.agentHistorySummarization,
contextManagement: {
enabled: settings.experimental?.contextManagement,
...settings?.contextManagement,
},
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
toolOutputMasking: settings.experimental?.toolOutputMasking,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
@@ -998,6 +993,8 @@ export async function loadCliConfig(
trustedFolder,
useBackgroundColor: settings.ui?.useBackgroundColor,
useAlternateBuffer: settings.ui?.useAlternateBuffer,
useTerminalBuffer: settings.ui?.terminalBuffer,
useRenderProcess: settings.ui?.renderProcess,
useRipgrep: settings.tools?.useRipgrep,
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
shellBackgroundCompletionBehavior: settings.tools?.shell
@@ -11,7 +11,8 @@ This extension will exclude the following core tools: tool1,tool2
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform."
understand the permissions it requires and the actions it may perform.
"
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should include warning when hooks are present 1`] = `
@@ -21,7 +22,8 @@ exports[`consent > maybeRequestConsentOrFail > consent string generation > shoul
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform."
understand the permissions it requires and the actions it may perform.
"
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if extension is migrated 1`] = `
@@ -30,7 +32,8 @@ exports[`consent > maybeRequestConsentOrFail > consent string generation > shoul
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform."
understand the permissions it requires and the actions it may perform.
"
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if skills change 1`] = `
@@ -60,7 +63,8 @@ understand the permissions it requires and the actions it may perform.
Agent skills inject specialized instructions and domain-specific knowledge into the agent's system
prompt. This can change how the agent interprets your requests and interacts with your environment.
Review the skill definitions at the location(s) provided below to ensure they meet your security
standards."
standards.
"
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should show a warning if the skill directory cannot be read 1`] = `
@@ -82,7 +86,8 @@ understand the permissions it requires and the actions it may perform.
Agent skills inject specialized instructions and domain-specific knowledge into the agent's system
prompt. This can change how the agent interprets your requests and interacts with your environment.
Review the skill definitions at the location(s) provided below to ensure they meet your security
standards."
standards.
"
`;
exports[`consent > skillsConsentString > should generate a consent string for skills 1`] = `
@@ -98,5 +103,6 @@ Install Destination: /mock/target/dir
Agent skills inject specialized instructions and domain-specific knowledge into the agent's system
prompt. This can change how the agent interprets your requests and interacts with your environment.
Review the skill definitions at the location(s) provided below to ensure they meet your security
standards."
standards.
"
`;
+143 -77
View File
@@ -5,87 +5,153 @@
*/
import { describe, it, expect } from 'vitest';
import { deriveItemsFromLegacySettings } from './footerItems.js';
import {
deriveItemsFromLegacySettings,
resolveFooterState,
} from './footerItems.js';
import { createMockSettings } from '../test-utils/settings.js';
describe('deriveItemsFromLegacySettings', () => {
it('returns defaults when no legacy settings are customized', () => {
const settings = createMockSettings({
ui: { footer: { hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toEqual([
'workspace',
'git-branch',
'sandbox',
'model-name',
'quota',
]);
});
describe('footerItems', () => {
describe('deriveItemsFromLegacySettings', () => {
it('returns defaults when no legacy settings are customized', () => {
const settings = createMockSettings({
ui: { footer: { hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toEqual([
'workspace',
'git-branch',
'sandbox',
'model-name',
'quota',
]);
});
it('removes workspace when hideCWD is true', () => {
const settings = createMockSettings({
ui: { footer: { hideCWD: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('workspace');
});
it('removes workspace when hideCWD is true', () => {
const settings = createMockSettings({
ui: { footer: { hideCWD: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('workspace');
});
it('removes sandbox when hideSandboxStatus is true', () => {
const settings = createMockSettings({
ui: { footer: { hideSandboxStatus: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('sandbox');
});
it('removes model-name, context-used, and quota when hideModelInfo is true', () => {
const settings = createMockSettings({
ui: { footer: { hideModelInfo: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('model-name');
expect(items).not.toContain('context-used');
expect(items).not.toContain('quota');
});
it('includes context-used when hideContextPercentage is false', () => {
const settings = createMockSettings({
ui: { footer: { hideContextPercentage: false } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toContain('context-used');
// Should be after model-name
const modelIdx = items.indexOf('model-name');
const contextIdx = items.indexOf('context-used');
expect(contextIdx).toBe(modelIdx + 1);
});
it('includes memory-usage when showMemoryUsage is true', () => {
const settings = createMockSettings({
ui: { showMemoryUsage: true, footer: { hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toContain('memory-usage');
});
it('handles combination of settings', () => {
const settings = createMockSettings({
ui: {
showMemoryUsage: true,
footer: {
hideCWD: true,
hideModelInfo: true,
hideContextPercentage: false,
it('removes sandbox when hideSandboxStatus is true', () => {
const settings = createMockSettings({
ui: {
footer: { hideSandboxStatus: true, hideContextPercentage: true },
},
},
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toEqual([
'git-branch',
'sandbox',
'context-used',
'memory-usage',
]);
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('sandbox');
});
it('removes model-name, context-used, and quota when hideModelInfo is true', () => {
const settings = createMockSettings({
ui: { footer: { hideModelInfo: true, hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).not.toContain('model-name');
expect(items).not.toContain('context-used');
expect(items).not.toContain('quota');
});
it('includes context-used when hideContextPercentage is false', () => {
const settings = createMockSettings({
ui: { footer: { hideContextPercentage: false } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toContain('context-used');
// Should be after model-name
const modelIdx = items.indexOf('model-name');
const contextIdx = items.indexOf('context-used');
expect(contextIdx).toBe(modelIdx + 1);
});
it('includes memory-usage when showMemoryUsage is true', () => {
const settings = createMockSettings({
ui: { showMemoryUsage: true, footer: { hideContextPercentage: true } },
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toContain('memory-usage');
});
it('handles combination of settings', () => {
const settings = createMockSettings({
ui: {
showMemoryUsage: true,
footer: {
hideCWD: true,
hideModelInfo: true,
hideContextPercentage: false,
},
},
}).merged;
const items = deriveItemsFromLegacySettings(settings);
expect(items).toEqual([
'git-branch',
'sandbox',
'context-used',
'memory-usage',
]);
});
});
describe('resolveFooterState', () => {
it('filters out auth item when showUserIdentity is false', () => {
const settings = createMockSettings({
ui: {
showUserIdentity: false,
footer: {
items: ['workspace', 'auth', 'model-name'],
},
},
}).merged;
const state = resolveFooterState(settings);
expect(state.orderedIds).not.toContain('auth');
expect(state.selectedIds.has('auth')).toBe(false);
// It should also not be in the 'others' part of orderedIds
expect(state.orderedIds).toEqual([
'workspace',
'model-name',
'git-branch',
'sandbox',
'context-used',
'quota',
'memory-usage',
'session-id',
'code-changes',
'token-count',
]);
});
it('includes auth item when showUserIdentity is true', () => {
const settings = createMockSettings({
ui: {
showUserIdentity: true,
footer: {
items: ['workspace', 'auth', 'model-name'],
},
},
}).merged;
const state = resolveFooterState(settings);
expect(state.orderedIds).toContain('auth');
expect(state.selectedIds.has('auth')).toBe(true);
});
it('includes auth item by default when showUserIdentity is undefined (defaults to true)', () => {
const settings = createMockSettings({
ui: {
footer: {
items: ['workspace', 'auth', 'model-name'],
},
},
}).merged;
const state = resolveFooterState(settings);
expect(state.orderedIds).toContain('auth');
expect(state.selectedIds.has('auth')).toBe(true);
});
});
});
+17 -2
View File
@@ -47,6 +47,11 @@ export const ALL_ITEMS = [
header: 'session',
description: 'Unique identifier for the current session',
},
{
id: 'auth',
header: '/auth',
description: 'Current authentication info',
},
{
id: 'code-changes',
header: 'diff',
@@ -70,6 +75,7 @@ export const DEFAULT_ORDER = [
'quota',
'memory-usage',
'session-id',
'auth',
'code-changes',
'token-count',
];
@@ -121,10 +127,19 @@ export function resolveFooterState(settings: MergedSettings): {
orderedIds: string[];
selectedIds: Set<string>;
} {
const showUserIdentity = settings.ui?.showUserIdentity !== false;
const filteredValidIds = showUserIdentity
? VALID_IDS
: new Set([...VALID_IDS].filter((id) => id !== 'auth'));
const source = (
settings.ui?.footer?.items ?? deriveItemsFromLegacySettings(settings)
).filter((id: string) => VALID_IDS.has(id));
const others = DEFAULT_ORDER.filter((id) => !source.includes(id));
).filter((id: string) => filteredValidIds.has(id));
const others = DEFAULT_ORDER.filter(
(id) => !source.includes(id) && filteredValidIds.has(id),
);
return {
orderedIds: [...source, ...others],
selectedIds: new Set(source),
+23 -4
View File
@@ -1124,15 +1124,15 @@ function migrateExperimentalSettings(
};
let modified = false;
const migrateExperimental = (
const migrateExperimental = <T = Record<string, unknown>>(
oldKey: string,
migrateFn: (oldValue: Record<string, unknown>) => void,
migrateFn: (oldValue: T) => void,
) => {
const old = experimentalSettings[oldKey];
if (old) {
if (old !== undefined) {
foundDeprecated?.push(`experimental.${oldKey}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
migrateFn(old as Record<string, unknown>);
migrateFn(old as T);
modified = true;
}
};
@@ -1197,6 +1197,24 @@ function migrateExperimentalSettings(
agentsOverrides['cli_help'] = override;
});
// Migrate experimental.plan -> general.plan.enabled
migrateExperimental<boolean>('plan', (planValue) => {
const generalSettings =
(settings.general as Record<string, unknown> | undefined) || {};
const newGeneral = { ...generalSettings };
const planSettings =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(newGeneral['plan'] as Record<string, unknown> | undefined) || {};
const newPlan = { ...planSettings };
if (newPlan['enabled'] === undefined) {
newPlan['enabled'] = planValue;
newGeneral['plan'] = newPlan;
loadedSettings.setValue(scope, 'general', newGeneral);
modified = true;
}
});
if (modified) {
agentsSettings['overrides'] = agentsOverrides;
loadedSettings.setValue(scope, 'agents', agentsSettings);
@@ -1205,6 +1223,7 @@ function migrateExperimentalSettings(
const newExperimental = { ...experimentalSettings };
delete newExperimental['codebaseInvestigatorSettings'];
delete newExperimental['cliHelpAgentSettings'];
delete newExperimental['plan'];
loadedSettings.setValue(scope, 'experimental', newExperimental);
}
return true;
@@ -418,14 +418,17 @@ describe('SettingsSchema', () => {
});
it('should have plan setting in schema', () => {
const setting = getSettingsSchema().experimental.properties.plan;
const setting =
getSettingsSchema().general.properties.plan.properties.enabled;
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.category).toBe('General');
expect(setting.default).toBe(true);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(true);
expect(setting.description).toBe('Enable Plan Mode.');
expect(setting.description).toBe(
'Enable Plan Mode for read-only safety during planning.',
);
});
it('should have hooksConfig.notifications setting in schema', () => {
+207 -96
View File
@@ -293,6 +293,16 @@ const SETTINGS_SCHEMA = {
description: 'Planning features configuration.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Plan Mode',
category: 'General',
requiresRestart: true,
default: true,
description:
'Enable Plan Mode for read-only safety during planning.',
showInDialog: true,
},
directory: {
type: 'string',
label: 'Plan Directory',
@@ -561,6 +571,16 @@ const SETTINGS_SCHEMA = {
description: 'Show the "? for shortcuts" hint above the input.',
showInDialog: true,
},
compactToolOutput: {
type: 'boolean',
label: 'Compact Tool Output',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Display tool outputs (like directory listings and file reads) in a compact, structured format.',
showInDialog: true,
},
hideBanner: {
type: 'boolean',
label: 'Hide Banner',
@@ -723,6 +743,24 @@ const SETTINGS_SCHEMA = {
'Use an alternate screen buffer for the UI, preserving shell history.',
showInDialog: true,
},
renderProcess: {
type: 'boolean',
label: 'Render Process',
category: 'UI',
requiresRestart: true,
default: true,
description: 'Enable Ink render process for the UI.',
showInDialog: true,
},
terminalBuffer: {
type: 'boolean',
label: 'Terminal Buffer',
category: 'UI',
requiresRestart: true,
default: true,
description: 'Use the new terminal buffer architecture for rendering.',
showInDialog: true,
},
useBackgroundColor: {
type: 'boolean',
label: 'Use Background Color',
@@ -1913,58 +1951,6 @@ const SETTINGS_SCHEMA = {
description: 'Setting to enable experimental features',
showInDialog: false,
properties: {
toolOutputMasking: {
type: 'object',
label: 'Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
ignoreInDocs: false,
default: {},
description:
'Advanced settings for tool output masking to manage context window efficiency.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
default: true,
description: 'Enables tool output masking to save tokens.',
showInDialog: true,
},
toolProtectionThreshold: {
type: 'number',
label: 'Tool Protection Threshold',
category: 'Experimental',
requiresRestart: true,
default: 50000,
description:
'Minimum number of tokens to protect from masking (most recent tool outputs).',
showInDialog: false,
},
minPrunableTokensThreshold: {
type: 'number',
label: 'Min Prunable Tokens Threshold',
category: 'Experimental',
requiresRestart: true,
default: 30000,
description:
'Minimum prunable tokens required to trigger a masking pass.',
showInDialog: false,
},
protectLatestTurn: {
type: 'boolean',
label: 'Protect Latest Turn',
category: 'Experimental',
requiresRestart: true,
default: true,
description:
'Ensures the absolute latest turn is never masked, regardless of token count.',
showInDialog: false,
},
},
},
enableAgents: {
type: 'boolean',
label: 'Enable Agents',
@@ -2036,7 +2022,7 @@ const SETTINGS_SCHEMA = {
label: 'JIT Context Loading',
category: 'Experimental',
requiresRestart: true,
default: true,
default: false,
description: 'Enable Just-In-Time (JIT) context loading.',
showInDialog: false,
},
@@ -2060,15 +2046,6 @@ const SETTINGS_SCHEMA = {
'Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
showInDialog: true,
},
plan: {
type: 'boolean',
label: 'Plan',
category: 'Experimental',
requiresRestart: true,
default: true,
description: 'Enable Plan Mode.',
showInDialog: true,
},
taskTracker: {
type: 'boolean',
label: 'Task Tracker',
@@ -2169,44 +2146,13 @@ const SETTINGS_SCHEMA = {
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
showInDialog: true,
},
agentHistoryTruncation: {
contextManagement: {
type: 'boolean',
label: 'Agent History Truncation',
label: 'Enable Context Management',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable truncation window logic for the Agent History Provider.',
showInDialog: true,
},
agentHistoryTruncationThreshold: {
type: 'number',
label: 'Agent History Truncation Threshold',
category: 'Experimental',
requiresRestart: true,
default: 30,
description:
'The maximum number of messages before history is truncated.',
showInDialog: true,
},
agentHistoryRetainedMessages: {
type: 'number',
label: 'Agent History Retained Messages',
category: 'Experimental',
requiresRestart: true,
default: 15,
description:
'The number of recent messages to retain after truncation.',
showInDialog: true,
},
agentHistorySummarization: {
type: 'boolean',
label: 'Agent History Summarization',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable summarization of truncated content via a small model for the Agent History Provider.',
description: 'Enable logic for context management.',
showInDialog: true,
},
topicUpdateNarration: {
@@ -2485,6 +2431,171 @@ const SETTINGS_SCHEMA = {
},
},
contextManagement: {
type: 'object',
label: 'Context Management',
category: 'Experimental',
requiresRestart: true,
default: {},
description:
'Settings for agent history and tool distillation context management.',
showInDialog: false,
properties: {
historyWindow: {
type: 'object',
label: 'History Window Settings',
category: 'Context Management',
requiresRestart: true,
default: {},
showInDialog: false,
properties: {
maxTokens: {
type: 'number',
label: 'Max Tokens',
category: 'Context Management',
requiresRestart: true,
default: 150_000,
description:
'The number of tokens to allow before triggering compression.',
showInDialog: false,
},
retainedTokens: {
type: 'number',
label: 'Retained Tokens',
category: 'Context Management',
requiresRestart: true,
default: 40_000,
description: 'The number of tokens to always retain.',
showInDialog: false,
},
},
},
messageLimits: {
type: 'object',
label: 'Message Limits',
category: 'Context Management',
requiresRestart: true,
default: {},
showInDialog: false,
properties: {
normalMaxTokens: {
type: 'number',
label: 'Normal Maximum Tokens',
category: 'Context Management',
requiresRestart: true,
default: 2500,
description:
'The target number of tokens to budget for a normal conversation turn.',
showInDialog: false,
},
retainedMaxTokens: {
type: 'number',
label: 'Retained Maximum Tokens',
category: 'Context Management',
requiresRestart: true,
default: 12000,
description:
'The maximum number of tokens a single conversation turn can consume before truncation.',
showInDialog: false,
},
normalizationHeadRatio: {
type: 'number',
label: 'Normalization Head Ratio',
category: 'Context Management',
requiresRestart: true,
default: 0.25,
description:
'The ratio of tokens to retain from the beginning of a truncated message (0.0 to 1.0).',
showInDialog: false,
},
},
},
tools: {
type: 'object',
label: 'Context Management Tools',
category: 'Context Management',
requiresRestart: true,
default: {},
showInDialog: false,
properties: {
distillation: {
type: 'object',
label: 'Tool Distillation',
category: 'Context Management',
requiresRestart: true,
default: {},
showInDialog: false,
properties: {
maxOutputTokens: {
type: 'number',
label: 'Max Output Tokens',
category: 'Context Management',
requiresRestart: true,
default: 10_000,
description:
'Maximum tokens to show to the model when truncating large tool outputs.',
showInDialog: false,
},
summarizationThresholdTokens: {
type: 'number',
label: 'Tool Summarization Threshold',
category: 'Context Management',
requiresRestart: true,
default: 20_000,
description:
'Threshold above which truncated tool outputs will be summarized by an LLM.',
showInDialog: false,
},
},
},
outputMasking: {
type: 'object',
label: 'Tool Output Masking',
category: 'Context Management',
requiresRestart: true,
ignoreInDocs: false,
default: {},
description:
'Advanced settings for tool output masking to manage context window efficiency.',
showInDialog: false,
properties: {
protectionThresholdTokens: {
type: 'number',
label: 'Tool Protection Threshold (Tokens)',
category: 'Context Management',
requiresRestart: true,
default: 50_000,
description:
'Minimum number of tokens to protect from masking (most recent tool outputs).',
showInDialog: false,
},
minPrunableThresholdTokens: {
type: 'number',
label: 'Min Prunable Tokens Threshold',
category: 'Context Management',
requiresRestart: true,
default: 30_000,
description:
'Minimum prunable tokens required to trigger a masking pass.',
showInDialog: false,
},
protectLatestTurn: {
type: 'boolean',
label: 'Protect Latest Turn',
category: 'Context Management',
requiresRestart: true,
default: true,
description:
'Ensures the absolute latest turn is never masked, regardless of token count.',
showInDialog: false,
},
},
},
},
},
},
},
admin: {
type: 'object',
label: 'Admin',
+1
View File
@@ -327,6 +327,7 @@ describe('gemini.tsx main function cleanup', () => {
refreshAuth: vi.fn(),
getRemoteAdminSettings: vi.fn(() => undefined),
getUseAlternateBuffer: vi.fn(() => false),
getUseTerminalBuffer: vi.fn(() => false),
...overrides,
} as unknown as Config;
}
+7 -3
View File
@@ -43,9 +43,9 @@ import { KeypressProvider } from './ui/contexts/KeypressContext.js';
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { profiler } from './ui/components/DebugProfiler.js';
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
const SLOW_RENDER_MS = 200;
@@ -57,12 +57,13 @@ export async function startInteractiveUI(
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
) {
initializeConsoleStore();
// Never enter Ink alternate buffer mode when screen reader mode is enabled
// as there is no benefit of alternate buffer mode when using a screen reader
// and the Ink alternate buffer mode requires line wrapping harmful to
// screen readers.
const useAlternateBuffer = shouldEnterAlternateScreen(
isAlternateBufferEnabled(config),
config.getUseAlternateBuffer(),
config.getScreenReader(),
);
const mouseEventsEnabled = useAlternateBuffer;
@@ -131,7 +132,6 @@ export async function startInteractiveUI(
// Wait a moment for shpool to stabilize terminal size and state.
await new Promise((resolve) => setTimeout(resolve, 100));
}
const instance = render(
process.env['DEBUG'] ? (
<React.StrictMode>
@@ -152,8 +152,12 @@ export async function startInteractiveUI(
}
profiler.reportFrameRendered();
},
standardReactLayoutTiming:
useAlternateBuffer || config.getUseTerminalBuffer(),
patchConsole: false,
alternateBuffer: useAlternateBuffer,
renderProcess: config.getUseRenderProcess(),
terminalBuffer: config.getUseTerminalBuffer(),
incrementalRendering:
settings.merged.ui.incrementalRendering !== false &&
useAlternateBuffer &&
@@ -36,15 +36,11 @@ export async function toMatchSvgSnapshot(
}
let textContent: string;
if (renderInstance.lastFrameRaw) {
textContent = renderInstance.lastFrameRaw({
allowEmpty: options?.allowEmpty,
});
} else if (renderInstance.lastFrame) {
if (renderInstance.lastFrame) {
textContent = renderInstance.lastFrame({ allowEmpty: options?.allowEmpty });
} else {
throw new Error(
'toMatchSvgSnapshot requires a renderInstance with either lastFrameRaw or lastFrame',
'toMatchSvgSnapshot requires a renderInstance with lastFrame',
);
}
const svgContent = renderInstance.generateSvg();
@@ -61,6 +61,7 @@ export const createMockCommandContext = (
toggleCorgiMode: vi.fn(),
toggleShortcutsHelp: vi.fn(),
toggleVimEnabled: vi.fn(),
reloadCommands: vi.fn(),
openAgentConfigDialog: vi.fn(),
closeAgentConfigDialog: vi.fn(),
extensionsUpdateState: new Map(),
+13
View File
@@ -175,6 +175,8 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
validatePathAccess: vi.fn().mockReturnValue(null),
getUseAlternateBuffer: vi.fn().mockReturnValue(false),
getUseTerminalBuffer: vi.fn().mockReturnValue(false),
getUseRenderProcess: vi.fn().mockReturnValue(false),
...overrides,
}) as unknown as Config;
@@ -194,6 +196,17 @@ export function createMockSettings(
user: { settings: {} },
workspace: { settings: {} },
errors: [],
subscribe: vi.fn().mockReturnValue(() => {}),
getSnapshot: vi.fn().mockReturnValue({
system: { settings: {} },
systemDefaults: { settings: {} },
user: { settings: {} },
workspace: { settings: {} },
isTrusted: true,
errors: [],
merged,
}),
setValue: vi.fn(),
...overrides,
merged,
} as unknown as LoadedSettings;
+29 -9
View File
@@ -42,6 +42,7 @@ import {
type OverflowState,
} from '../ui/contexts/OverflowContext.js';
import { makeFakeConfig } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
@@ -51,7 +52,6 @@ import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
import { generateSvgForTerminal } from './svg.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
export const persistentStateMock = new FakePersistentState();
@@ -223,7 +223,7 @@ class XtermStdout extends EventEmitter {
this.once('render', resolve),
);
const timeoutPromise = new Promise((resolve) =>
setTimeout(resolve, 50),
setTimeout(resolve, 1000),
);
await Promise.race([renderPromise, timeoutPromise]);
}
@@ -254,7 +254,12 @@ class XtermStdout extends EventEmitter {
const isMatch = () => {
if (expectedFrame === '...') {
return currentFrame !== '';
// '...' is our fallback when output isn't in metrics, meaning Ink rendered *something*
// but we don't know what it is. If terminal has content, we consider it a match.
// However, if the component rendered null, both would be empty, but our fallback
// made expectedFrame '...'. In that case, we can't easily know if it's ready,
// but we can assume if there are no pending writes, it's ready.
return currentFrame !== '' || this.pendingWrites === 0;
}
// If Ink expects nothing (no new static content and no dynamic output),
@@ -613,6 +618,7 @@ export const renderWithProviders = async (
mouseEventsEnabled = false,
config,
uiActions,
toolActions,
persistentState,
appState = mockAppState,
}: {
@@ -623,6 +629,11 @@ export const renderWithProviders = async (
mouseEventsEnabled?: boolean;
config?: Config;
uiActions?: Partial<UIActions>;
toolActions?: Partial<{
isExpanded: (callId: string) => boolean;
toggleExpansion: (callId: string) => void;
toggleAllExpansion: (callIds: string[]) => void;
}>;
persistentState?: {
get?: typeof persistentStateMock.get;
set?: typeof persistentStateMock.set;
@@ -660,12 +671,11 @@ export const renderWithProviders = async (
const terminalWidth = width ?? baseState.terminalWidth;
if (!config) {
config = await loadCliConfig(
settings.merged,
'random-session-id',
{} as unknown as CliArgs,
{ cwd: '/' },
);
config = makeFakeConfig({
useAlternateBuffer: settings.merged.ui?.useAlternateBuffer,
showMemoryUsage: settings.merged.ui?.showMemoryUsage,
accessibility: settings.merged.ui?.accessibility,
});
}
const mainAreaWidth = providedUiState?.mainAreaWidth ?? terminalWidth;
@@ -710,6 +720,16 @@ export const renderWithProviders = async (
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
isExpanded={
toolActions?.isExpanded ??
vi.fn().mockReturnValue(false)
}
toggleExpansion={
toolActions?.toggleExpansion ?? vi.fn()
}
toggleAllExpansion={
toolActions?.toggleAllExpansion ?? vi.fn()
}
>
<AskUserActionsProvider
request={null}
+22 -7
View File
@@ -346,6 +346,7 @@ describe('AppContainer State Management', () => {
// Initialize mock stdout for terminal title tests
mocks.mockStdout.write.mockClear();
(disableMouseEvents as import('vitest').Mock).mockClear();
capturedUIState = null!;
@@ -470,6 +471,7 @@ describe('AppContainer State Management', () => {
// Mock Config
mockConfig = makeFakeConfig();
vi.spyOn(mockConfig, 'getUseRenderProcess').mockReturnValue(false);
// Mock config's getTargetDir to return consistent workspace directory
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
@@ -1356,6 +1358,7 @@ describe('AppContainer State Management', () => {
beforeEach(() => {
// Reset mock stdout for each test
mocks.mockStdout.write.mockClear();
(disableMouseEvents as import('vitest').Mock).mockClear();
});
it('verifies useStdout is mocked', async () => {
@@ -2459,7 +2462,7 @@ describe('AppContainer State Management', () => {
});
});
describe('Copy Mode (CTRL+S)', () => {
describe('Copy Mode (F9)', () => {
let rerender: () => void;
let unmount: () => void;
let stdin: Awaited<ReturnType<typeof render>>['stdin'];
@@ -2468,6 +2471,8 @@ describe('AppContainer State Management', () => {
isAlternateMode = false,
childHandler?: Mock,
) => {
vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(
isAlternateMode,
);
@@ -2512,6 +2517,8 @@ describe('AppContainer State Management', () => {
beforeEach(() => {
mocks.mockStdout.write.mockClear();
(disableMouseEvents as import('vitest').Mock).mockClear();
vi.useFakeTimers();
});
@@ -2532,12 +2539,13 @@ describe('AppContainer State Management', () => {
modeName: 'Alternate Buffer Mode',
},
])('$modeName', ({ isAlternateMode, shouldEnable }) => {
it(`should ${shouldEnable ? 'toggle' : 'NOT toggle'} mouse off when Ctrl+S is pressed`, async () => {
it(`should ${shouldEnable ? 'toggle' : 'NOT toggle'} mouse off when F9 is pressed`, async () => {
await setupCopyModeTest(isAlternateMode);
mocks.mockStdout.write.mockClear(); // Clear initial enable call
(disableMouseEvents as import('vitest').Mock).mockClear();
act(() => {
stdin.write('\x13'); // Ctrl+S
stdin.write('\x1b[20~'); // F9
});
rerender();
@@ -2550,13 +2558,13 @@ describe('AppContainer State Management', () => {
});
if (shouldEnable) {
it('should toggle mouse back on when Ctrl+S is pressed again', async () => {
it('should toggle mouse back on when F9 is pressed again', async () => {
await setupCopyModeTest(isAlternateMode);
(writeToStdout as Mock).mockClear();
// Turn it on (disable mouse)
act(() => {
stdin.write('\x13'); // Ctrl+S
stdin.write('\x1b[20~'); // F9
});
rerender();
expect(disableMouseEvents).toHaveBeenCalled();
@@ -2576,7 +2584,7 @@ describe('AppContainer State Management', () => {
// Enter copy mode
act(() => {
stdin.write('\x13'); // Ctrl+S
stdin.write('\x1b[20~'); // F9
});
rerender();
@@ -2656,7 +2664,7 @@ describe('AppContainer State Management', () => {
// 2. Enter copy mode
act(() => {
stdin.write('\x13'); // Ctrl+S
stdin.write('\x1b[20~'); // F9
});
rerender();
@@ -3093,6 +3101,7 @@ describe('AppContainer State Management', () => {
// Clear previous calls
mocks.mockStdout.write.mockClear();
(disableMouseEvents as import('vitest').Mock).mockClear();
const { unmount } = await act(async () => renderAppContainer());
@@ -3135,6 +3144,7 @@ describe('AppContainer State Management', () => {
// Reset mock stdout to clear any initial writes
mocks.mockStdout.write.mockClear();
(disableMouseEvents as import('vitest').Mock).mockClear();
// Submit
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
@@ -3154,6 +3164,8 @@ describe('AppContainer State Management', () => {
);
vi.mocked(checkPermissions).mockResolvedValue([]);
vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
const { unmount } = await act(async () =>
@@ -3170,6 +3182,7 @@ describe('AppContainer State Management', () => {
// Reset mock stdout
mocks.mockStdout.write.mockClear();
(disableMouseEvents as import('vitest').Mock).mockClear();
// Submit
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
@@ -3403,6 +3416,8 @@ describe('AppContainer State Management', () => {
ui: { useAlternateBuffer: true },
});
vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
const { unmount } = await act(async () =>
+149 -18
View File
@@ -11,6 +11,7 @@ import {
useEffect,
useRef,
useLayoutEffect,
useContext,
} from 'react';
import {
type DOMElement,
@@ -19,6 +20,7 @@ import {
useStdout,
useStdin,
type AppProps,
AppContext as InkAppContext,
} from 'ink';
import { App } from './App.js';
import { AppContext } from './contexts/AppContext.js';
@@ -38,6 +40,8 @@ import {
import { checkPermissions } from './hooks/atCommandProcessor.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import { MouseProvider } from './contexts/MouseContext.js';
import { ScrollProvider } from './contexts/ScrollProvider.js';
import {
type StartupWarning,
type EditorType,
@@ -168,6 +172,7 @@ import { useSuspend } from './hooks/useSuspend.js';
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
import {
getLastTurnToolCallIds,
isToolExecuting,
isToolAwaitingConfirmation,
getAllToolCalls,
@@ -208,12 +213,30 @@ export const AppContainer = (props: AppContainerProps) => {
const { reset } = useOverflowActions()!;
const notificationsEnabled = isNotificationsEnabled(settings);
const { setOptions, dumpCurrentFrame, startRecording, stopRecording } =
useContext(InkAppContext);
const recordingFilenameRef = useRef<string | null>(null);
const historyManager = useHistory({
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
});
useMemoryMonitor(historyManager);
const isAlternateBuffer = config.getUseAlternateBuffer();
const [mouseMode, setMouseMode] = useState(() =>
config.getUseAlternateBuffer(),
);
useEffect(() => {
setOptions({
stickyHeadersInBackbuffer: mouseMode,
});
if (mouseMode) {
enableMouseEvents();
} else {
disableMouseEvents();
}
}, [mouseMode, setOptions]);
const [corgiMode, setCorgiMode] = useState(false);
const [forceRerenderKey, setForceRerenderKey] = useState(0);
const [debugMessage, setDebugMessage] = useState<string>('');
@@ -238,6 +261,39 @@ export const AppContainer = (props: AppContainerProps) => {
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
const toggleExpansion = useCallback((callId: string) => {
setExpandedTools((prev) => {
const next = new Set(prev);
if (next.has(callId)) {
next.delete(callId);
} else {
next.add(callId);
}
return next;
});
}, []);
const toggleAllExpansion = useCallback((callIds: string[]) => {
setExpandedTools((prev) => {
const next = new Set(prev);
const anyCollapsed = callIds.some((id) => !next.has(id));
if (anyCollapsed) {
callIds.forEach((id) => next.add(id));
} else {
callIds.forEach((id) => next.delete(id));
}
return next;
});
}, []);
const isExpanded = useCallback(
(callId: string) => expandedTools.has(callId),
[expandedTools],
);
const [shellModeActive, setShellModeActive] = useState(false);
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
useState<boolean>(false);
@@ -579,11 +635,11 @@ export const AppContainer = (props: AppContainerProps) => {
});
const refreshStatic = useCallback(() => {
if (!isAlternateBuffer) {
if (!isAlternateBuffer && !config.getUseTerminalBuffer()) {
stdout.write(ansiEscapes.clearTerminal);
setHistoryRemountKey((prev) => prev + 1);
}
setHistoryRemountKey((prev) => prev + 1);
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
}, [setHistoryRemountKey, isAlternateBuffer, stdout, config]);
const shouldUseAlternateScreen = shouldEnterAlternateScreen(
isAlternateBuffer,
@@ -993,6 +1049,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (config.isJitContextEnabled()) {
await config.getContextManager()?.refresh();
config.updateSystemInstructionIfInitialized();
flattenedMemory = flattenMemory(config.getUserMemory());
fileCount = config.getGeminiMdFileCount();
} else {
@@ -1137,11 +1194,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
);
const hasPendingToolConfirmation = useMemo(
() => isToolAwaitingConfirmation(pendingHistoryItems),
[pendingHistoryItems],
);
toggleBackgroundTasksRef.current = toggleBackgroundTasks;
isBackgroundTaskVisibleRef.current = isBackgroundTaskVisible;
backgroundTasksRef.current = backgroundTasks;
@@ -1396,6 +1448,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
!copyModeEnabled;
const observerRef = useRef<ResizeObserver | null>(null);
useEffect(
() => () => {
observerRef.current?.disconnect();
},
[],
);
const [controlsHeight, setControlsHeight] = useState(0);
const [lastNonCopyControlsHeight, setLastNonCopyControlsHeight] = useState(0);
@@ -1694,6 +1754,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
setShortcutsHelpVisible(false);
}
if (keyMatchers[Command.TOGGLE_MOUSE_MODE](key)) {
setMouseMode((prev) => {
const next = !prev;
if (!next && !isAlternateBuffer) {
appEvents.emit(AppEvent.ScrollToBottom);
}
return next;
});
return true;
}
if (isAlternateBuffer && keyMatchers[Command.TOGGLE_COPY_MODE](key)) {
setCopyModeEnabled(true);
disableMouseEvents();
@@ -1716,6 +1787,32 @@ Logging in with Google... Restarting Gemini CLI to continue.
return true;
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
handleSuspend();
} else if (keyMatchers[Command.DUMP_FRAME](key)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `snapshot-${timestamp}.json`;
if (dumpCurrentFrame) {
dumpCurrentFrame(filename);
debugLogger.log(`Dumped frame to: ${filename}`);
}
return true;
} else if (keyMatchers[Command.START_RECORDING](key)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `recording-${timestamp}.json`;
if (startRecording) {
startRecording(filename);
recordingFilenameRef.current = filename;
debugLogger.log(`Started recording to: ${filename}`);
}
return true;
} else if (keyMatchers[Command.STOP_RECORDING](key)) {
if (stopRecording) {
stopRecording();
debugLogger.log(
`Stopped recording, saved to: ${recordingFilenameRef.current ?? 'unknown'}`,
);
recordingFilenameRef.current = null;
}
return true;
} else if (
keyMatchers[Command.TOGGLE_COPY_MODE](key) &&
!isAlternateBuffer
@@ -1727,13 +1824,25 @@ Logging in with Google... Restarting Gemini CLI to continue.
return true;
}
const toggleLastTurnTools = () => {
triggerExpandHint(true);
const targetToolCallIds = getLastTurnToolCallIds(
historyManager.history,
pendingHistoryItems,
);
if (targetToolCallIds.length > 0) {
toggleAllExpansion(targetToolCallIds);
}
};
let enteringConstrainHeightMode = false;
if (!constrainHeight) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
// If the user manually collapses the view, show the hint and reset the x-second timer.
triggerExpandHint(true);
toggleLastTurnTools();
}
if (!isAlternateBuffer) {
refreshStatic();
@@ -1781,11 +1890,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
!enteringConstrainHeightMode
) {
setConstrainHeight(false);
// If the user manually expands the view, show the hint and reset the x-second timer.
triggerExpandHint(true);
if (!isAlternateBuffer) {
refreshStatic();
}
toggleLastTurnTools();
refreshStatic();
return true;
} else if (
(keyMatchers[Command.FOCUS_SHELL_INPUT](key) ||
@@ -1890,6 +1996,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
triggerExpandHint,
keyMatchers,
isHelpDismissKey,
historyManager.history,
pendingHistoryItems,
toggleAllExpansion,
dumpCurrentFrame,
startRecording,
stopRecording,
],
);
@@ -1909,7 +2021,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
setCopyModeEnabled(false);
enableMouseEvents();
if (mouseMode) {
enableMouseEvents();
}
return true;
},
{
@@ -2033,6 +2147,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
authState === AuthState.AwaitingApiKeyInput ||
!!newAgents;
const hasPendingToolConfirmation = useMemo(
() => isToolAwaitingConfirmation(pendingHistoryItems),
[pendingHistoryItems],
);
const hasConfirmUpdateExtensionRequests =
confirmUpdateExtensionRequests.length > 0;
const hasLoopDetectionConfirmationRequest =
@@ -2221,6 +2340,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
editorError,
isEditorDialogOpen,
showPrivacyNotice,
mouseMode,
corgiMode,
debugMessage,
quittingMessages,
@@ -2347,6 +2467,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
editorError,
isEditorDialogOpen,
showPrivacyNotice,
mouseMode,
corgiMode,
debugMessage,
quittingMessages,
@@ -2639,9 +2760,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
startupWarnings: props.startupWarnings || [],
}}
>
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
isExpanded={isExpanded}
toggleExpansion={toggleExpansion}
toggleAllExpansion={toggleAllExpansion}
>
<ShellFocusContext.Provider value={isFocused}>
<App key={`app-${forceRerenderKey}`} />
<MouseProvider mouseEventsEnabled={mouseMode}>
<ScrollProvider>
<App key={`app-${forceRerenderKey}`} />
</ScrollProvider>
</MouseProvider>
</ShellFocusContext.Provider>
</ToolActionsProvider>
</AppContext.Provider>
@@ -528,6 +528,7 @@ describe('skillsCommand', () => {
await actionPromise;
expect(reloadSkillsMock).toHaveBeenCalled();
expect(context.ui.reloadCommands).toHaveBeenCalled();
expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
@@ -285,6 +285,8 @@ async function reloadAction(
context.ui.setPendingItem(null);
}
context.ui.reloadCommands();
const afterSkills = skillManager.getSkills();
const afterNames = new Set(afterSkills.map((s) => s.name));
@@ -26,6 +26,7 @@ import { OverflowProvider } from '../contexts/OverflowContext.js';
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
import { TodoTray } from './messages/Todo.js';
import { useComposerStatus } from '../hooks/useComposerStatus.js';
import { appEvents, AppEvent } from '../../utils/events.js';
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const uiState = useUIState();
@@ -55,6 +56,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const { setShortcutsHelpVisible } = uiActions;
useEffect(() => {
if (hasPendingActionRequired) {
appEvents.emit(AppEvent.ScrollToBottom);
}
}, [hasPendingActionRequired]);
useEffect(() => {
if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) {
setShortcutsHelpVisible(false);
@@ -35,10 +35,7 @@ vi.mock('./shared/ScrollableList.js', () => ({
describe('DetailedMessagesDisplay', () => {
beforeEach(() => {
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: [],
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue([]);
});
it('renders nothing when messages are empty', async () => {
const { lastFrame, unmount } = await renderWithProviders(
@@ -58,10 +55,7 @@ describe('DetailedMessagesDisplay', () => {
{ type: 'error', content: 'Error message', count: 1 },
{ type: 'debug', content: 'Debug message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -79,10 +73,7 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'error', content: 'Error message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -98,10 +89,7 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'error', content: 'Error message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -117,10 +105,7 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'log', content: 'Repeated message', count: 5 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
vi.mocked(useConsoleMessages).mockReturnValue(messages);
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
@@ -29,7 +29,7 @@ export const DetailedMessagesDisplay: React.FC<
> = ({ maxHeight, width, hasFocus }) => {
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
const { consoleMessages } = useConsoleMessages();
const consoleMessages = useConsoleMessages();
const config = useConfig();
const messages = useMemo(() => {
@@ -166,6 +166,7 @@ Implement a comprehensive authentication system with multiple providers.
writeTextFile: vi.fn(),
}),
getUseAlternateBuffer: () => useAlternateBuffer,
getUseTerminalBuffer: () => false,
} as unknown as import('@google/gemini-cli-core').Config,
settings: createMockSettings({ ui: { useAlternateBuffer } }),
},
@@ -466,6 +467,7 @@ Implement a comprehensive authentication system with multiple providers.
writeTextFile: vi.fn(),
}),
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
getUseTerminalBuffer: () => false,
} as unknown as import('@google/gemini-cli-core').Config,
settings: createMockSettings({
ui: { useAlternateBuffer: useAlternateBuffer ?? true },
@@ -18,7 +18,7 @@ vi.mock('../../utils/processUtils.js', () => ({
}));
const mockedExit = vi.hoisted(() => vi.fn());
const mockedCwd = vi.hoisted(() => vi.fn());
const mockedCwd = vi.hoisted(() => vi.fn().mockReturnValue('/mock/cwd'));
const mockedRows = vi.hoisted(() => ({ current: 24 }));
vi.mock('node:process', async () => {
+82 -3
View File
@@ -8,7 +8,11 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { Footer } from './Footer.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { type Config } from '@google/gemini-cli-core';
import {
type Config,
UserAccountManager,
AuthType,
} from '@google/gemini-cli-core';
import path from 'node:path';
// Normalize paths to POSIX slashes for stable cross-platform snapshots.
@@ -69,14 +73,17 @@ const defaultProps = {
branchName: 'main',
};
const mockConfig = {
const mockConfigPlain = {
getTargetDir: () => defaultProps.targetDir,
getDebugMode: () => false,
getModel: () => defaultProps.model,
getIdeMode: () => false,
isTrustedFolder: () => true,
getExtensionRegistryURI: () => undefined,
} as unknown as Config;
getContentGeneratorConfig: () => ({ authType: undefined }),
};
const mockConfig = mockConfigPlain as unknown as Config;
const mockSessionStats = {
sessionId: 'test-session-id',
@@ -434,6 +441,7 @@ describe('<Footer />', () => {
it('renders footer with all optional sections hidden (minimal footer)', async () => {
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
config: mockConfig,
width: 120,
uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({
@@ -551,6 +559,7 @@ describe('<Footer />', () => {
describe('Footer Token Formatting', () => {
const renderWithTokens = async (tokens: number) => {
const result = await renderWithProviders(<Footer />, {
config: mockConfig,
width: 120,
uiState: {
sessionStats: {
@@ -675,6 +684,75 @@ describe('<Footer />', () => {
});
describe('Footer Custom Items', () => {
it('renders auth item with email', async () => {
const authConfig = {
...mockConfigPlain,
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
} as unknown as Config;
const getCachedAccountSpy = vi
.spyOn(UserAccountManager.prototype, 'getCachedGoogleAccount')
.mockReturnValue('test@example.com');
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
config: authConfig,
width: 120,
uiState: {
currentModel: 'gemini-pro',
sessionStats: mockSessionStats,
},
settings: createMockSettings({
ui: {
footer: {
items: ['auth'],
},
},
}),
});
expect(lastFrame()).toContain('auth');
expect(lastFrame()).toContain('test@example.com');
unmount();
getCachedAccountSpy.mockRestore();
});
it('does NOT render auth item when showUserIdentity is false', async () => {
const authConfig = {
...mockConfigPlain,
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
} as unknown as Config;
const getCachedAccountSpy = vi
.spyOn(UserAccountManager.prototype, 'getCachedGoogleAccount')
.mockReturnValue('test@example.com');
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
config: authConfig,
width: 120,
uiState: {
currentModel: 'gemini-pro',
sessionStats: mockSessionStats,
},
settings: createMockSettings({
ui: {
showUserIdentity: false,
footer: {
items: ['workspace', 'auth'],
},
},
}),
});
const output = lastFrame();
expect(output).toContain('workspace');
expect(output).not.toContain('auth');
expect(output).not.toContain('test@example.com');
unmount();
getCachedAccountSpy.mockRestore();
});
it('renders items in the specified order', async () => {
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
config: mockConfig,
@@ -734,6 +812,7 @@ describe('<Footer />', () => {
it('handles empty items array', async () => {
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
config: mockConfig,
width: 120,
uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({
+35
View File
@@ -5,6 +5,7 @@
*/
import type React from 'react';
import { useState, useEffect } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import {
@@ -12,6 +13,8 @@ import {
tildeifyPath,
getDisplayString,
checkExhaustive,
AuthType,
UserAccountManager,
} from '@google/gemini-cli-core';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
import process from 'node:process';
@@ -183,6 +186,18 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
const settings = useSettings();
const { vimEnabled, vimMode } = useVimMode();
const authType = config.getContentGeneratorConfig()?.authType;
const [email, setEmail] = useState<string | undefined>();
useEffect(() => {
if (authType) {
const userAccountManager = new UserAccountManager();
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
} else {
setEmail(undefined);
}
}, [authType]);
if (copyModeEnabled) {
return <Box height={1} />;
}
@@ -221,6 +236,7 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
errorCount > 0 &&
(isFullErrorVerbosity || debugMode || isDevelopment);
const displayVimMode = vimEnabled ? vimMode : undefined;
const items =
settings.merged.ui.footer.items ??
deriveItemsFromLegacySettings(settings.merged);
@@ -385,6 +401,25 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
);
break;
}
case 'auth': {
if (!settings.merged.ui.showUserIdentity) break;
if (!authType) break;
const displayStr =
authType === AuthType.LOGIN_WITH_GOOGLE
? (email ?? 'google')
: authType;
addCol(
id,
header,
() => (
<Text color={itemColor} wrap="truncate-end">
{displayStr}
</Text>
),
displayStr.length,
);
break;
}
case 'code-changes': {
const added = uiState.sessionStats.metrics.files.totalLinesAdded;
const removed = uiState.sessionStats.metrics.files.totalLinesRemoved;
@@ -116,7 +116,7 @@ describe('<FooterConfigDialog />', () => {
expect(lastFrame()).toContain('~/project/path');
// Move focus down to 'code-changes' (which has colored elements)
for (let i = 0; i < 8; i++) {
for (let i = 0; i < 9; i++) {
act(() => {
stdin.write('\u001b[B'); // Down arrow
});
@@ -255,6 +255,7 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
<Text color={getColor('code-changes', theme.status.error)}>-4</Text>
</Box>
),
auth: <Text color={getColor('auth', itemColor)}>test@example.com</Text>,
'token-count': (
<Text color={getColor('token-count', itemColor)}>1.5k tokens</Text>
),
+1 -1
View File
@@ -72,7 +72,7 @@ describe('Help Component', () => {
expect(output).toContain('Keyboard Shortcuts:');
expect(output).toContain('Ctrl+C');
expect(output).toContain('Ctrl+S');
expect(output).toContain('Shift+Tab');
expect(output).toContain('Page Up/Page Down');
unmount();
});
@@ -17,6 +17,7 @@ import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { CompressionMessage } from './messages/CompressionMessage.js';
import { WarningMessage } from './messages/WarningMessage.js';
import { SubagentHistoryMessage } from './messages/SubagentHistoryMessage.js';
import { Box } from 'ink';
import { AboutBox } from './AboutBox.js';
import { StatsDisplay } from './StatsDisplay.js';
@@ -215,6 +216,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
isExpandable={isExpandable}
/>
)}
{itemForDisplay.type === 'subagent' && (
<SubagentHistoryMessage
item={itemForDisplay}
terminalWidth={terminalWidth}
/>
)}
{itemForDisplay.type === 'compression' && (
<CompressionMessage compression={itemForDisplay.compression} />
)}
@@ -2222,85 +2222,67 @@ describe('InputPrompt', () => {
name: 'mid-word',
text: 'hello world',
visualCursor: [0, 3],
expected: `hel${chalk.inverse('l')}o world`,
},
{
name: 'at the beginning of the line',
text: 'hello',
visualCursor: [0, 0],
expected: `${chalk.inverse('h')}ello`,
},
{
name: 'at the end of the line',
text: 'hello',
visualCursor: [0, 5],
expected: `hello${chalk.inverse(' ')}`,
},
{
name: 'on a highlighted token',
text: 'run @path/to/file',
visualCursor: [0, 9],
expected: `@path/${chalk.inverse('t')}o/file`,
},
{
name: 'for multi-byte unicode characters',
text: 'hello 👍 world',
visualCursor: [0, 6],
expected: `hello ${chalk.inverse('👍')} world`,
},
{
name: 'after multi-byte unicode characters',
text: '👍A',
visualCursor: [0, 1],
expected: `👍${chalk.inverse('A')}`,
},
{
name: 'at the end of a line with unicode characters',
text: 'hello 👍',
visualCursor: [0, 8],
expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug
},
{
name: 'at the end of a short line with unicode characters',
text: '👍',
visualCursor: [0, 1],
expected: `👍${chalk.inverse(' ')}`,
},
{
name: 'on an empty line',
text: '',
visualCursor: [0, 0],
expected: chalk.inverse(' '),
},
{
name: 'on a space between words',
text: 'hello world',
visualCursor: [0, 5],
expected: `hello${chalk.inverse(' ')}world`,
},
])(
'should display cursor correctly $name',
async ({ name, text, visualCursor, expected }) => {
async ({ text, visualCursor }) => {
mockBuffer.text = text;
mockBuffer.lines = [text];
mockBuffer.viewportVisualLines = [text];
mockBuffer.visualCursor = visualCursor as [number, number];
props.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrameRaw();
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
if (
name !== 'at the end of a line with unicode characters' &&
name !== 'on a highlighted token'
) {
expect(frame).toContain('\u001b[7m');
}
});
unmount();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
},
);
});
@@ -2316,7 +2298,6 @@ describe('InputPrompt', () => {
[1, 0],
[2, 0],
],
expected: `sec${chalk.inverse('o')}nd line`,
},
{
name: 'at the beginning of a line',
@@ -2326,7 +2307,6 @@ describe('InputPrompt', () => {
[0, 0],
[1, 0],
],
expected: `${chalk.inverse('s')}econd line`,
},
{
name: 'at the end of a line',
@@ -2336,11 +2316,10 @@ describe('InputPrompt', () => {
[0, 0],
[1, 0],
],
expected: `first line${chalk.inverse(' ')}`,
},
])(
'should display cursor correctly $name in a multiline block',
async ({ name, text, visualCursor, expected, visualToLogicalMap }) => {
async ({ text, visualCursor, visualToLogicalMap }) => {
mockBuffer.text = text;
mockBuffer.lines = text.split('\n');
mockBuffer.viewportVisualLines = text.split('\n');
@@ -2350,20 +2329,12 @@ describe('InputPrompt', () => {
>;
props.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrameRaw();
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
if (
name !== 'at the end of a line with unicode characters' &&
name !== 'on a highlighted token'
) {
expect(frame).toContain('\u001b[7m');
}
});
unmount();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
},
);
@@ -2380,18 +2351,12 @@ describe('InputPrompt', () => {
];
props.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrameRaw();
const lines = frame.split('\n');
// The line with the cursor should just be an inverted space inside the box border
expect(
lines.find((l) => l.includes(chalk.inverse(' '))),
).not.toBeUndefined();
});
unmount();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
});
});
@@ -2412,22 +2377,14 @@ describe('InputPrompt', () => {
];
props.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrameRaw();
// Check that all lines, including the empty one, are rendered.
// This implicitly tests that the Box wrapper provides height for the empty line.
expect(frame).toContain('hello');
expect(frame).toContain('world');
expect(frame).toContain(chalk.inverse(' '));
const outputLines = frame.trim().split('\n');
// The number of lines should be 2 for the border plus 3 for the content.
expect(outputLines.length).toBe(5);
});
unmount();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
});
@@ -4088,14 +4045,12 @@ describe('InputPrompt', () => {
it('should not show inverted cursor when shell is focused', async () => {
props.isEmbeddedShellFocused = true;
props.focus = false;
const { stdout, unmount } = await renderWithProviders(
const renderResult = await renderWithProviders(
<InputPrompt {...props} />,
);
await waitFor(() => {
expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
});
expect(stdout.lastFrame()).toMatchSnapshot();
unmount();
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
});
@@ -338,6 +338,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const showCursor =
focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled;
useEffect(() => {
appEvents.emit(AppEvent.ScrollToBottom);
}, [buffer.text, buffer.cursor]);
// Notify parent component about escape prompt state changes
useEffect(() => {
if (onEscapePromptChange) {
@@ -27,6 +27,10 @@ import {
} from '../hooks/useConfirmingTool.js';
// Mock dependencies
vi.mock('ink-spinner', () => ({
default: () => <Text></Text>,
}));
const mockUseSettings = vi.fn().mockReturnValue({
merged: {
ui: {
+79 -19
View File
@@ -12,6 +12,7 @@ import { useAppContext } from '../contexts/AppContext.js';
import { AppHeader } from './AppHeader.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { useConfig } from '../contexts/ConfigContext.js';
import {
SCROLL_TO_ITEM_END,
type VirtualizedListRef,
@@ -22,6 +23,7 @@ import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
import { isTopicTool } from './messages/TopicMessage.js';
import { appEvents, AppEvent } from '../../utils/events.js';
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
const MemoizedAppHeader = memo(AppHeader);
@@ -33,7 +35,10 @@ const MemoizedAppHeader = memo(AppHeader);
export const MainContent = () => {
const { version } = useAppContext();
const uiState = useUIState();
const isAlternateBuffer = useAlternateBuffer();
const isAlternateBufferOrTerminalBuffer = useAlternateBuffer();
const config = useConfig();
const useTerminalBuffer = config.getUseTerminalBuffer();
const isAlternateBuffer = config.getUseAlternateBuffer();
const confirmingTool = useConfirmingTool();
const showConfirmationQueue = confirmingTool !== null;
@@ -47,12 +52,23 @@ export const MainContent = () => {
}
}, [showConfirmationQueue, confirmingToolCallId]);
useEffect(() => {
const handleScroll = () => {
scrollableListRef.current?.scrollToEnd();
};
appEvents.on(AppEvent.ScrollToBottom, handleScroll);
return () => {
appEvents.off(AppEvent.ScrollToBottom, handleScroll);
};
}, []);
const {
pendingHistoryItems,
mainAreaWidth,
staticAreaMaxItemHeight,
availableTerminalHeight,
cleanUiDetailsVisible,
mouseMode,
} = uiState;
const showHeaderDetails = cleanUiDetailsVisible;
@@ -284,24 +300,68 @@ export const MainContent = () => {
],
);
if (isAlternateBuffer) {
return (
<ScrollableList
ref={scrollableListRef}
hasFocus={!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused}
width={uiState.terminalWidth}
data={virtualizedData}
renderItem={renderItem}
estimatedItemHeight={() => 100}
keyExtractor={(item, _index) => {
if (item.type === 'header') return 'header';
if (item.type === 'history') return item.item.id.toString();
return 'pending';
}}
initialScrollIndex={SCROLL_TO_ITEM_END}
initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
/>
);
const estimatedItemHeight = useCallback(() => 100, []);
const keyExtractor = useCallback(
(item: (typeof virtualizedData)[number], _index: number) => {
if (item.type === 'header') return 'header';
if (item.type === 'history') return item.item.id.toString();
return 'pending';
},
[],
);
const isStaticItem = useCallback(
(item: (typeof virtualizedData)[number]) => item.type !== 'pending',
[],
);
const scrollableList = useMemo(() => {
if (isAlternateBufferOrTerminalBuffer) {
return (
<ScrollableList
ref={scrollableListRef}
hasFocus={
!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused
}
width={uiState.terminalWidth}
data={virtualizedData}
renderItem={renderItem}
estimatedItemHeight={estimatedItemHeight}
keyExtractor={keyExtractor}
initialScrollIndex={SCROLL_TO_ITEM_END}
initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
renderStatic={useTerminalBuffer}
isStaticItem={useTerminalBuffer ? isStaticItem : undefined}
overflowToBackbuffer={useTerminalBuffer && !isAlternateBuffer}
scrollbar={mouseMode}
/>
// TODO(jacobr): consider adding stableScrollback={!config.getUseAlternateBuffer()}
// as that will reduce the # of cases where we will have to clear the
// scrollback buffer due to the scrollback size changing but we need to
// work out ensuring we only attempt it within a smaller range of
// scrollback vals. Right now it sometimes triggers adding more white
// space than it should.
);
}
return null;
}, [
isAlternateBufferOrTerminalBuffer,
uiState.isEditorDialogOpen,
uiState.embeddedShellFocused,
uiState.terminalWidth,
virtualizedData,
renderItem,
estimatedItemHeight,
keyExtractor,
useTerminalBuffer,
isStaticItem,
mouseMode,
isAlternateBuffer,
]);
if (isAlternateBufferOrTerminalBuffer) {
return scrollableList;
}
return (
@@ -21,7 +21,6 @@ import {
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
AuthType,
UserTierId,
} from '@google/gemini-cli-core';
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
@@ -56,7 +55,6 @@ describe('<ModelDialog />', () => {
const mockGetGemini31FlashLiteLaunchedSync = vi.fn();
const mockGetProModelNoAccess = vi.fn();
const mockGetProModelNoAccessSync = vi.fn();
const mockGetUserTier = vi.fn();
interface MockConfig extends Partial<Config> {
setModel: (model: string, isTemporary?: boolean) => void;
@@ -67,7 +65,6 @@ describe('<ModelDialog />', () => {
getGemini31FlashLiteLaunchedSync: () => boolean;
getProModelNoAccess: () => Promise<boolean>;
getProModelNoAccessSync: () => boolean;
getUserTier: () => UserTierId | undefined;
}
const mockConfig: MockConfig = {
@@ -79,7 +76,6 @@ describe('<ModelDialog />', () => {
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
getProModelNoAccess: mockGetProModelNoAccess,
getProModelNoAccessSync: mockGetProModelNoAccessSync,
getUserTier: mockGetUserTier,
};
beforeEach(() => {
@@ -90,7 +86,6 @@ describe('<ModelDialog />', () => {
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(false);
mockGetProModelNoAccess.mockResolvedValue(false);
mockGetProModelNoAccessSync.mockReturnValue(false);
mockGetUserTier.mockReturnValue(UserTierId.STANDARD);
// Default implementation for getDisplayString
mockGetDisplayString.mockImplementation((val: string) => {
@@ -136,7 +131,6 @@ describe('<ModelDialog />', () => {
mockGetProModelNoAccess.mockResolvedValue(true);
mockGetHasAccessToPreviewModel.mockReturnValue(true);
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(true);
mockGetUserTier.mockReturnValue(UserTierId.FREE);
mockGetDisplayString.mockImplementation((val: string) => val);
const { lastFrame, unmount } = await renderComponent();
@@ -442,34 +436,11 @@ describe('<ModelDialog />', () => {
unmount();
});
it('hides Flash Lite Preview model for users with pro access', async () => {
mockGetProModelNoAccessSync.mockReturnValue(false);
mockGetProModelNoAccess.mockResolvedValue(false);
mockGetHasAccessToPreviewModel.mockReturnValue(true);
const { lastFrame, stdin, waitUntilReady, unmount } =
await renderComponent();
// Go to manual view
await act(async () => {
stdin.write('\u001B[B'); // Manual
});
await waitUntilReady();
await act(async () => {
stdin.write('\r');
});
await waitUntilReady();
const output = lastFrame();
expect(output).not.toContain(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL);
unmount();
});
it('shows Flash Lite Preview model for free tier users', async () => {
it('shows Flash Lite Preview model regardless of tier when flag is enabled', async () => {
mockGetProModelNoAccessSync.mockReturnValue(false);
mockGetProModelNoAccess.mockResolvedValue(false);
mockGetHasAccessToPreviewModel.mockReturnValue(true);
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(true);
mockGetUserTier.mockReturnValue(UserTierId.FREE);
const { lastFrame, stdin, waitUntilReady, unmount } =
await renderComponent();
+48 -81
View File
@@ -23,7 +23,6 @@ import {
AuthType,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
isProModel,
UserTierId,
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js';
import { theme } from '../semantic-colors.js';
@@ -72,9 +71,11 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
const manualModelSelected = useMemo(() => {
if (
config?.getExperimentalDynamicModelConfiguration?.() === true &&
config.modelConfigService
config.getModelConfigService
) {
const def = config.modelConfigService.getModelDefinition(preferredModel);
const def = config
.getModelConfigService()
.getModelDefinition(preferredModel);
// Only treat as manual selection if it's a visible, non-auto model.
return def && def.tier !== 'auto' && def.isVisible === true
? preferredModel
@@ -120,30 +121,25 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
// --- DYNAMIC PATH ---
if (
config?.getExperimentalDynamicModelConfiguration?.() === true &&
config.modelConfigService
config.getModelConfigService
) {
const list = Object.entries(
config.modelConfigService.getModelDefinitions?.() ?? {},
)
.filter(([_, m]) => {
// Basic visibility and Preview access
if (m.isVisible !== true) return false;
if (m.isPreview && !shouldShowPreviewModels) return false;
// Only auto models are shown on the main menu
if (m.tier !== 'auto') return false;
return true;
})
.map(([id, m]) => ({
value: id,
title: m.displayName ?? getDisplayString(id, config ?? undefined),
description:
id === 'auto-gemini-3' && useGemini31
? (m.dialogDescription ?? '').replace(
'gemini-3-pro',
'gemini-3.1-pro',
)
: (m.dialogDescription ?? ''),
key: id,
const allOptions = config
.getModelConfigService()
.getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
hasAccessToProModel,
});
const list = allOptions
.filter((o) => o.tier === 'auto')
.map((o) => ({
value: o.modelId,
title: o.name,
description: o.description,
key: o.modelId,
}));
list.push({
@@ -187,68 +183,39 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
});
}
return list;
}, [config, shouldShowPreviewModels, manualModelSelected, useGemini31]);
}, [
config,
shouldShowPreviewModels,
manualModelSelected,
useGemini31,
useGemini31FlashLite,
useCustomToolModel,
hasAccessToProModel,
]);
const manualOptions = useMemo(() => {
const isFreeTier = config?.getUserTier() === UserTierId.FREE;
// --- DYNAMIC PATH ---
if (
config?.getExperimentalDynamicModelConfiguration?.() === true &&
config.modelConfigService
config.getModelConfigService
) {
const list = Object.entries(
config.modelConfigService.getModelDefinitions?.() ?? {},
)
.filter(([id, m]) => {
// Basic visibility and Preview access
if (m.isVisible !== true) return false;
if (m.isPreview && !shouldShowPreviewModels) return false;
// Auto models are for main menu only
if (m.tier === 'auto') return false;
// Pro models are shown for users with pro access
if (!hasAccessToProModel && m.tier === 'pro') return false;
// 3.1 Preview Flash-lite is only available on free tier
if (m.tier === 'flash-lite' && m.isPreview && !isFreeTier)
return false;
// Flag Guard: Versioned models only show if their flag is active.
if (id === PREVIEW_GEMINI_3_1_MODEL && !useGemini31) return false;
if (
id === PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL &&
!useGemini31FlashLite
)
return false;
return true;
})
.map(([id, m]) => {
const resolvedId = config.modelConfigService.resolveModelId(id, {
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
});
// Title ID is the resolved ID without custom tools flag
const titleId = config.modelConfigService.resolveModelId(id, {
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
});
return {
value: resolvedId,
title:
m.displayName ?? getDisplayString(titleId, config ?? undefined),
key: id,
};
const allOptions = config
.getModelConfigService()
.getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
hasAccessToProModel,
});
// Deduplicate: only show one entry per unique resolved model value.
// This is needed because 3 pro and 3.1 pro models can resolve to the same
// value, depending on the useGemini31 flag.
const seen = new Set<string>();
return list.filter((option) => {
if (seen.has(option.value)) return false;
seen.add(option.value);
return true;
});
return allOptions
.filter((o) => o.tier !== 'auto')
.map((o) => ({
value: o.modelId,
title: o.name,
key: o.modelId,
}));
}
// --- LEGACY PATH ---
@@ -292,7 +259,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
},
];
if (isFreeTier && useGemini31FlashLite) {
if (useGemini31FlashLite) {
previewOptions.push({
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
@@ -22,7 +22,7 @@ import * as processUtils from '../../utils/processUtils.js';
import { usePermissionsModifyTrust } from '../hooks/usePermissionsModifyTrust.js';
// Hoist mocks for dependencies of the usePermissionsModifyTrust hook
const mockedCwd = vi.hoisted(() => vi.fn());
const mockedCwd = vi.hoisted(() => vi.fn().mockReturnValue('/mock/cwd'));
const mockedLoadTrustedFolders = vi.hoisted(() => vi.fn());
const mockedIsWorkspaceTrusted = vi.hoisted(() => vi.fn());
+15 -1
View File
@@ -5,7 +5,7 @@
*/
import type React from 'react';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useRef, useState, useEffect } from 'react';
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
import {
isUserVisibleHook,
@@ -77,6 +77,13 @@ export const StatusNode: React.FC<{
}) => {
const observerRef = useRef<ResizeObserver | null>(null);
useEffect(
() => () => {
observerRef.current?.disconnect();
},
[],
);
const onRefChange = useCallback(
(node: DOMElement | null) => {
if (observerRef.current) {
@@ -169,6 +176,13 @@ export const StatusRow: React.FC<StatusRowProps> = ({
const [tipWidth, setTipWidth] = useState(0);
const tipObserverRef = useRef<ResizeObserver | null>(null);
useEffect(
() => () => {
tipObserverRef.current?.disconnect();
},
[],
);
const onTipRefChange = useCallback((node: DOMElement | null) => {
if (tipObserverRef.current) {
tipObserverRef.current.disconnect();
@@ -59,6 +59,7 @@ describe('ToolConfirmationQueue', () => {
getPlansDir: () => '/mock/temp/plans',
},
getUseAlternateBuffer: () => false,
getUseTerminalBuffer: () => false,
} as unknown as Config;
beforeEach(() => {
@@ -5,7 +5,7 @@
*/
import type React from 'react';
import { useMemo } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import {
@@ -21,12 +21,15 @@ interface UserIdentityProps {
export const UserIdentity: React.FC<UserIdentityProps> = ({ config }) => {
const authType = config.getContentGeneratorConfig()?.authType;
const email = useMemo(() => {
const [email, setEmail] = useState<string | undefined>();
useEffect(() => {
if (authType) {
const userAccountManager = new UserAccountManager();
return userAccountManager.getCachedGoogleAccount() ?? undefined;
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
} else {
setEmail(undefined);
}
return undefined;
}, [authType]);
const tierName = useMemo(
@@ -43,10 +43,12 @@ Tips for getting started:
│ ✓ tool1 Description for tool 1 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────╮
│ o tool3 Description for tool 3 │
│ │
@@ -93,6 +95,7 @@ Tips for getting started:
│ ✓ tool1 Description for tool 1 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
@@ -15,7 +15,8 @@ Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results"
4. Be specific for the best results
"
`;
exports[`AppHeader Icon Rendering > renders the symmetric icon in Apple Terminal 1`] = `
@@ -33,5 +34,6 @@ Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results"
4. Be specific for the best results
"
`;
@@ -4,13 +4,15 @@ 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`] = `
@@ -18,11 +20,13 @@ 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 │
╰──────────────────────────────────────────────────────────────────────────────╯"
╰──────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="700" viewBox="0 0 920 700">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="734" viewBox="0 0 920 734">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="700" fill="#000000" />
<rect width="920" height="734" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -74,85 +74,92 @@
<text x="45" y="359" fill="#afafaf" textLength="378" lengthAdjust="spacingAndGlyphs"> Unique identifier for the current session</text>
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="374" width="9" height="17" fill="#001a00" />
<text x="27" y="376" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">&gt;</text>
<rect x="36" y="374" width="9" height="17" fill="#001a00" />
<rect x="45" y="374" width="27" height="17" fill="#001a00" />
<text x="45" y="376" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<rect x="72" y="374" width="117" height="17" fill="#001a00" />
<text x="72" y="376" fill="#00cd00" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<rect x="189" y="374" width="684" height="17" fill="#001a00" />
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="376" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="391" width="18" height="17" fill="#001a00" />
<rect x="45" y="391" width="513" height="17" fill="#001a00" />
<text x="45" y="393" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<rect x="558" y="391" width="315" height="17" fill="#001a00" />
<text x="45" y="393" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="410" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<rect x="27" y="408" width="9" height="17" fill="#001a00" />
<text x="27" y="410" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">&gt;</text>
<rect x="36" y="408" width="9" height="17" fill="#001a00" />
<rect x="45" y="408" width="27" height="17" fill="#001a00" />
<text x="45" y="410" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<rect x="72" y="408" width="117" height="17" fill="#001a00" />
<text x="72" y="410" fill="#00cd00" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<rect x="189" y="408" width="684" height="17" fill="#001a00" />
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<rect x="27" y="425" width="18" height="17" fill="#001a00" />
<rect x="45" y="425" width="513" height="17" fill="#001a00" />
<text x="45" y="427" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<rect x="558" y="425" width="315" height="17" fill="#001a00" />
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="444" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[]</text>
<text x="72" y="444" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="444" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<text x="45" y="478" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="478" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="529" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="27" y="563" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="580" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="597" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<text x="297" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
<text x="405" y="597" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="513" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="693" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<rect x="801" y="595" width="36" height="17" fill="#001a00" />
<text x="801" y="597" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">diff</text>
<rect x="837" y="595" width="18" height="17" fill="#001a00" />
<text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<text x="297" y="614" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="405" y="614" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="513" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="693" y="614" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<rect x="801" y="612" width="27" height="17" fill="#001a00" />
<text x="801" y="614" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">+12</text>
<rect x="828" y="612" width="9" height="17" fill="#001a00" />
<rect x="837" y="612" width="18" height="17" fill="#001a00" />
<text x="837" y="614" fill="#ff87af" textLength="18" lengthAdjust="spacingAndGlyphs">-4</text>
<text x="45" y="614" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="631" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="27" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="631" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<text x="297" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
<text x="405" y="631" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="513" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="693" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<rect x="801" y="629" width="36" height="17" fill="#001a00" />
<text x="801" y="631" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">diff</text>
<rect x="837" y="629" width="18" height="17" fill="#001a00" />
<text x="864" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<text x="297" y="648" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="405" y="648" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="513" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="693" y="648" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<rect x="801" y="646" width="27" height="17" fill="#001a00" />
<text x="801" y="648" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">+12</text>
<rect x="828" y="646" width="9" height="17" fill="#001a00" />
<rect x="837" y="646" width="18" height="17" fill="#001a00" />
<text x="837" y="648" fill="#ff87af" textLength="18" lengthAdjust="spacingAndGlyphs">-4</text>
<text x="864" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="0" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="665" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="700" viewBox="0 0 920 700">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="734" viewBox="0 0 920 734">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="700" fill="#000000" />
<rect width="920" height="734" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -84,70 +84,77 @@
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="376" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<text x="72" y="376" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="393" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<text x="45" y="393" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="410" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<text x="72" y="410" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<text x="45" y="427" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="444" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[]</text>
<text x="72" y="444" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="444" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<text x="45" y="478" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="478" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="529" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="27" y="563" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="580" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="595" width="198" height="17" fill="#001a00" />
<text x="45" y="597" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<text x="324" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
<text x="459" y="597" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="594" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="801" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="612" width="126" height="17" fill="#001a00" />
<text x="45" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<rect x="171" y="612" width="72" height="17" fill="#001a00" />
<text x="324" y="614" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="459" y="614" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="594" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="801" y="614" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<text x="45" y="614" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="631" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="27" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="629" width="198" height="17" fill="#001a00" />
<text x="45" y="631" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<text x="324" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
<text x="459" y="631" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="594" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="801" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<text x="864" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="646" width="126" height="17" fill="#001a00" />
<text x="45" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<rect x="171" y="646" width="72" height="17" fill="#001a00" />
<text x="324" y="648" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="459" y="648" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="594" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="801" y="648" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<text x="864" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="0" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="665" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="683" viewBox="0 0 920 683">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="717" viewBox="0 0 920 717">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="683" fill="#000000" />
<rect width="920" height="717" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -75,69 +75,76 @@
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="376" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<text x="72" y="376" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="393" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<text x="45" y="393" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="410" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<text x="72" y="410" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<text x="45" y="427" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="442" width="9" height="17" fill="#001a00" />
<text x="27" y="444" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">&gt;</text>
<rect x="36" y="442" width="9" height="17" fill="#001a00" />
<rect x="45" y="442" width="27" height="17" fill="#001a00" />
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<rect x="72" y="442" width="171" height="17" fill="#001a00" />
<text x="72" y="444" fill="#00cd00" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<rect x="243" y="442" width="630" height="17" fill="#001a00" />
<text x="72" y="444" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="459" width="846" height="17" fill="#001a00" />
<text x="45" y="461" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<rect x="27" y="476" width="9" height="17" fill="#001a00" />
<text x="27" y="478" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">&gt;</text>
<rect x="36" y="476" width="9" height="17" fill="#001a00" />
<rect x="45" y="476" width="27" height="17" fill="#001a00" />
<text x="45" y="478" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<rect x="72" y="476" width="171" height="17" fill="#001a00" />
<text x="72" y="478" fill="#00cd00" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<rect x="243" y="476" width="630" height="17" fill="#001a00" />
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="493" width="846" height="17" fill="#001a00" />
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="529" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="27" y="563" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="580" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="597" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<text x="207" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="279" y="597" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="351" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="432" y="597" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="522" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="594" y="597" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="756" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="828" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="614" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="631" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<text x="207" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="279" y="631" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="351" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="432" y="631" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="522" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="594" y="631" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="756" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="828" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<text x="864" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="0" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="648" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -23,6 +23,8 @@ exports[`<FooterConfigDialog /> > highlights the active item in the preview 1`]
│ Memory used by the application │
│ [ ] session-id │
│ Unique identifier for the current session │
│ [ ] auth │
│ Current authentication info │
│ > [✓] code-changes │
│ Lines added/removed in the session (not shown when zero) │
│ [ ] token-count │
@@ -40,7 +42,8 @@ exports[`<FooterConfigDialog /> > highlights the active item in the preview 1`]
│ │ ~/project/path main docker gemini-2.5-pro 97% +12 -4 │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<FooterConfigDialog /> > renders correctly with default settings 1`] = `
@@ -66,6 +69,8 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 1`] =
│ Memory used by the application │
│ [ ] session-id │
│ Unique identifier for the current session │
│ [ ] auth │
│ Current authentication info │
│ [ ] code-changes │
│ Lines added/removed in the session (not shown when zero) │
│ [ ] token-count │
@@ -110,6 +115,8 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 2`] =
│ Memory used by the application │
│ [ ] session-id │
│ Unique identifier for the current session │
│ [ ] auth │
│ Current authentication info │
│ [ ] code-changes │
│ Lines added/removed in the session (not shown when zero) │
│ [ ] token-count │
@@ -127,7 +134,8 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 2`] =
│ │ ~/project/path main docker gemini-2.5-pro 97% │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<FooterConfigDialog /> > updates the preview when Show footer labels is toggled off 1`] = `
@@ -153,6 +161,8 @@ exports[`<FooterConfigDialog /> > updates the preview when Show footer labels is
│ Memory used by the application │
│ [ ] session-id │
│ Unique identifier for the current session │
│ [ ] auth │
│ Current authentication info │
│ [ ] code-changes │
│ Lines added/removed in the session (not shown when zero) │
│ [ ] token-count │
@@ -169,5 +179,6 @@ exports[`<FooterConfigDialog /> > updates the preview when Show footer labels is
│ │ ~/project/path · main · docker · gemini-2.5-pro · 97% │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -1,6 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<HookStatusDisplay /> > matches SVG snapshot for single hook 1`] = `"Executing Hook: test-hook"`;
exports[`<HookStatusDisplay /> > matches SVG snapshot for single hook 1`] = `
"Executing Hook: test-hook
"
`;
exports[`<HookStatusDisplay /> > should render a single executing hook 1`] = `
"Executing Hook: test-hook
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="88" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="36" y="34" width="9" height="17" fill="#ffffff" />
<text x="36" y="36" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">s</text>
<text x="45" y="36" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">econd line</text>
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="88" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
<rect x="126" y="17" width="9" height="17" fill="#ffffff" />
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs">second line</text>
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="105" viewBox="0 0 920 105">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="105" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="36" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">sec</text>
<rect x="63" y="34" width="9" height="17" fill="#ffffff" />
<text x="63" y="36" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">o</text>
<text x="72" y="36" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">nd line</text>
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">third line</text>
<text x="891" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="105" viewBox="0 0 920 105">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="105" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">first line</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="36" y="34" width="9" height="17" fill="#ffffff" />
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">third line</text>
<text x="891" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">👍</text>
<rect x="45" y="17" width="9" height="17" fill="#ffffff" />
<text x="45" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">A</text>
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<rect x="36" y="17" width="9" height="17" fill="#ffffff" />
<text x="36" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">h</text>
<text x="45" y="19" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">ello</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">hello 👍</text>
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="9" lengthAdjust="spacingAndGlyphs">👍</text>
<rect x="45" y="17" width="9" height="17" fill="#ffffff" />
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">hello</text>
<rect x="81" y="17" width="9" height="17" fill="#ffffff" />
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">hello </text>
<rect x="90" y="17" width="9" height="17" fill="#ffffff" />
<text x="90" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">👍</text>
<text x="99" y="19" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs"> world</text>
<text x="882" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">hel</text>
<rect x="63" y="17" width="9" height="17" fill="#ffffff" />
<text x="63" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">l</text>
<text x="72" y="19" fill="#ffffff" textLength="63" lengthAdjust="spacingAndGlyphs">o world</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">run </text>
<text x="72" y="19" fill="#d7afff" textLength="45" lengthAdjust="spacingAndGlyphs">@path</text>
<rect x="117" y="17" width="9" height="17" fill="#d7afff" />
<text x="117" y="19" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">/</text>
<text x="126" y="19" fill="#d7afff" textLength="63" lengthAdjust="spacingAndGlyphs">to/file</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">hello</text>
<rect x="81" y="17" width="9" height="17" fill="#ffffff" />
<text x="90" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">world</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<rect x="36" y="17" width="9" height="17" fill="#ffffff" />
<text x="45" y="19" fill="#afafaf" textLength="315" lengthAdjust="spacingAndGlyphs"> Type your message or @path/to/file</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="105" viewBox="0 0 920 105">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="105" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
<text x="0" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<text x="36" y="19" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">hello</text>
<text x="891" y="19" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="36" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="53" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">world</text>
<rect x="81" y="51" width="9" height="17" fill="#ffffff" />
<text x="891" y="53" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#00cd00" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<rect x="0" y="0" width="900" height="17" fill="#1f1f1f" />
<text x="0" y="2" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
<rect x="0" y="17" width="9" height="17" fill="#1f1f1f" />
<rect x="9" y="17" width="18" height="17" fill="#1f1f1f" />
<text x="9" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">&gt; </text>
<rect x="27" y="17" width="324" height="17" fill="#1f1f1f" />
<text x="27" y="19" fill="#afafaf" textLength="324" lengthAdjust="spacingAndGlyphs"> Type your message or @path/to/file</text>
<rect x="351" y="17" width="549" height="17" fill="#1f1f1f" />
<rect x="0" y="34" width="900" height="17" fill="#1f1f1f" />
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,5 +1,109 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'at the beginning of a line' in a multiline block 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > first line │
│ second line │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'at the end of a line' in a multiline block 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > first line │
│ second line │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'in the middle of a line' in a multiline block 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > first line │
│ second line │
│ third line │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor on a blank line in a multiline block 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > first line │
│ │
│ third line │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'after multi-byte unicode characters' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > 👍A │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the beginning of the line' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of a line with unicode cha…' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello 👍 │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of a short line with unico…' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > 👍 │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of the line' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'for multi-byte unicode characters' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello 👍 world │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'mid-word' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello world │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a highlighted token' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > run @path/to/file │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a space between words' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello world │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on an empty line' 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > Type your message or @path/to/file │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> second message
@@ -78,6 +182,15 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
"
`;
exports[`InputPrompt > multiline rendering > should correctly render multiline input including blank lines 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello │
│ │
│ world │
────────────────────────────────────────────────────────────────────────────────────────────────────
"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file
@@ -4,14 +4,13 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focuse
"ScrollableList
AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
Shell Command Running a long command... │
Shell Command Running a long command... │
│ │
│ Line 10 │
│ Line 11 │
│ Line 12 │
│ Line 13 │
│ Line 14 │
│ Line 15
│ Line 15
│ Line 16 █ │
│ Line 17 █ │
│ Line 18 █ │
@@ -25,14 +24,13 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocu
"ScrollableList
AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
Shell Command Running a long command... │
Shell Command Running a long command... │
│ │
│ Line 10 │
│ Line 11 │
│ Line 12 │
│ Line 13 │
│ Line 14 │
│ Line 15
│ Line 15
│ Line 16 █ │
│ Line 17 █ │
│ Line 18 █ │
@@ -45,10 +43,9 @@ AppHeader(full)
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
"AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
Shell Command Running a long command... │
Shell Command Running a long command... │
│ │
│ ... first 10 lines hidden (Ctrl+O to show) ... │
│ Line 11 │
│ ... first 11 lines hidden (Ctrl+O to show) ... │
│ Line 12 │
│ Line 13 │
│ Line 14 │
@@ -65,7 +62,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Con
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
"AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
Shell Command Running a long command... │
Shell Command Running a long command... │
│ │
│ Line 1 │
│ Line 2 │
@@ -228,5 +225,6 @@ AppHeader(full)
│ Refining approach
│ And finally a third multiple line paragraph for the third thinking message to
│ refine the solution."
│ refine the solution.
"
`;
@@ -76,38 +76,38 @@
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="359" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Max Chat Model Attempts</text>
<text x="855" y="512" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -76,38 +76,38 @@
<text x="0" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="359" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="45" y="359" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs">Enable Plan Mode</text>
<text x="837" y="359" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="359" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="45" y="376" fill="#afafaf" textLength="486" lengthAdjust="spacingAndGlyphs">Enable Plan Mode for read-only safety during planning.</text>
<text x="891" y="376" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="410" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="45" y="410" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">Plan Directory</text>
<text x="792" y="410" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="410" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr</text>
<text x="45" y="427" fill="#afafaf" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t</text>
<text x="891" y="427" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="45" y="461" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Plan Model Routing</text>
<text x="837" y="461" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="461" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="45" y="478" fill="#afafaf" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="478" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Max Chat Model Attempts</text>
<text x="855" y="512" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="45" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Retry Fetch Errors</text>
<text x="837" y="512" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="512" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#afafaf" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="45" y="529" fill="#afafaf" textLength="612" lengthAdjust="spacingAndGlyphs">Retry on &quot;exception TypeError: fetch failed sending request&quot; errors.</text>
<text x="891" y="529" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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