mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -07:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecbab46394 | |||
| 17fb758664 | |||
| 518cc1ab63 | |||
| b7ad7e1035 | |||
| dbcad90661 | |||
| 703d2e0dcc | |||
| 7d8ab08adb | |||
| bc40695ce4 | |||
| 646dc3154b | |||
| 7b772e9dfb | |||
| ef1e18a85a | |||
| 58fd00a3df | |||
| b923604602 | |||
| 8643d60b88 | |||
| 8feeffb29b | |||
| e64146914a | |||
| 6084708cc2 | |||
| 8ed0f8981f | |||
| 41a1a3eed1 | |||
| 10ba348a3a | |||
| db67bb106a | |||
| b828b4754b | |||
| 9383b54d50 | |||
| 181da07dd9 | |||
| 419464a8c2 | |||
| 2e229d3bb6 | |||
| 3c92bdb1ad | |||
| e0f159085e | |||
| 402148dbc4 | |||
| 70696e364b | |||
| 1e10492e55 | |||
| 9e6914d641 | |||
| 0c4fb6afd2 | |||
| edab97997a | |||
| 2515b89e2b | |||
| 60f0f19d76 | |||
| 322232e514 | |||
| 54466a3ea8 | |||
| 7da060c149 | |||
| 088f48d60f | |||
| 2b426c1d91 | |||
| bc168bbae4 | |||
| 124a6da743 | |||
| 739c02bd6d | |||
| 5d13145995 | |||
| c28ff3d5a5 | |||
| 6ddd5abd7b | |||
| bf6d0485ce | |||
| 3d486ec1e9 | |||
| a6d1245a54 | |||
| de7e1937f6 | |||
| b465e12747 | |||
| 948401a450 | |||
| bb8f181ef1 | |||
| bf90b59935 | |||
| 18698d6929 | |||
| 80c4225286 | |||
| d02f3f6809 | |||
| da85aed5aa | |||
| 7f2d33458a | |||
| 3e9a0a7628 | |||
| 26c115a4fb | |||
| db643e9166 | |||
| ba100642e3 | |||
| cc52839f19 | |||
| e79b149985 | |||
| 5e21c8c03c |
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
@@ -0,0 +1,29 @@
|
||||
description="Injects context of all relevant cli files"
|
||||
prompt = """
|
||||
The following output contains the complete
|
||||
source code of packages/core.
|
||||
**Pay extremely close attention to these files.** They define the project's
|
||||
core architecture, component patterns, and testing standards.
|
||||
|
||||
The source code contains the content of absolutely every source code file in
|
||||
packages/core.
|
||||
You should very rarely need to read any other files from packages/core to resolve
|
||||
prompts.
|
||||
|
||||
!{find packages/core \\( -path packages/cli/dist -o -path packages/core/dist -o -name node_modules \\) -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\;}
|
||||
|
||||
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines when writing code in packages/core.
|
||||
|
||||
## Configuration & Settings
|
||||
* **Settings vs Args**: Use settings for user-configurable options; do not add new CLI arguments.
|
||||
* **Schema**: Add new settings to `packages/cli/src/config/settingsSchema.ts`.
|
||||
* **Documentation**:
|
||||
* If `showInDialog: true`, document in `docs/get-started/configuration.md`.
|
||||
* Ensure `requiresRestart` is correctly set.
|
||||
|
||||
## General
|
||||
* **Logging**: Use `debugLogger` for errors. NEVER leave `console.log/warn/error` in the code.
|
||||
* **TypeScript**: Avoid the non-null assertion operator (`!`).
|
||||
|
||||
{{args}}.
|
||||
"""
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: 'Run Gemini PR size and complexity labeller'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # Use the specific commit SHA
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
with:
|
||||
|
||||
@@ -55,3 +55,4 @@ gha-creds-*.json
|
||||
patch_output.log
|
||||
|
||||
.genkit
|
||||
.gemini-clipboard/
|
||||
|
||||
@@ -18,6 +18,41 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.21.0 - 2025-12-15
|
||||
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** Better, faster and cheaper than 2.5
|
||||
Pro - and in some scenarios better than 3 Pro! For paid tiers + free tier
|
||||
users who were on the wait list enable **Preview Features** in `/settings.`
|
||||
- For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
- 🎉 Gemini CLI Extensions:
|
||||
- Rill: Utilize natural language to analyze Rill data, enabling the
|
||||
exploration of metrics and trends without the need for manual queries.
|
||||
`gemini extensions install https://github.com/rilldata/rill-gemini-extension`
|
||||
- Browserbase: Interact with web pages, take screenshots, extract information,
|
||||
and perform automated actions with atomic precision.
|
||||
`gemini extensions install https://github.com/browserbase/mcp-server-browserbase`
|
||||
- Quota Visibility: The `/stats` command now displays quota information for all
|
||||
available models, including those not used in the current session. (@sehoon38)
|
||||
- Fuzzy Setting Search: Users can now quickly find settings using fuzzy search
|
||||
within the settings dialog. (@sehoon38)
|
||||
- MCP Resource Support: Users can now discover, view, and search through
|
||||
resources using the @ command. (@MrLesk)
|
||||
- Auto-execute Simple Slash Commands: Simple slash commands are now executed
|
||||
immediately on enter. (@jackwotherspoon)
|
||||
|
||||
## Announcements: v0.20.0 - 2025-12-01
|
||||
|
||||
- **Multi-file Drag & Drop:** Users can now drag and drop multiple files into
|
||||
the terminal, and the CLI will automatically prefix each valid path with `@`.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/14832) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
- **Persistent "Always Allow" Policies:** Users can now save "Always Allow"
|
||||
decisions for tool executions, with granular control over specific shell
|
||||
commands and multi-cloud platform tools.
|
||||
([pr](https://github.com/google-gemini/gemini-cli/pull/14737) by
|
||||
[@allenhutchison](https://github.com/allenhutchison))
|
||||
|
||||
## Announcements: v0.19.0 - 2025-11-24
|
||||
|
||||
- 🎉 **New extensions:**
|
||||
@@ -54,8 +89,7 @@ on GitHub.
|
||||
- **Experimental permission improvements:** We are now experimenting with a new
|
||||
policy engine in Gemini CLI. This allows users and administrators to create
|
||||
fine-grained policy for tool calls. Currently behind a flag. See
|
||||
[https://geminicli.com/docs/core/policy-engine/](../core/policy-engine.md) for
|
||||
more information.
|
||||
[policy engine documentation](../core/policy-engine.md) for more information.
|
||||
- Blog:
|
||||
[https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/](https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/)
|
||||
- **Gemini 3 support for paid:** Gemini 3 support has been rolled out to all API
|
||||
|
||||
+209
-138
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.19.0 - v0.19.4
|
||||
# Latest stable release: v0.21.0 - v0.21.1
|
||||
|
||||
Released: December 1, 2025
|
||||
Released: December 16, 2025
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,144 +11,215 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Zed integration:** Users can now leverage Gemini 3 within the Zed
|
||||
integration after enabling "Preview Features" in their CLI’s `/settings`.
|
||||
- **Interactive shell:**
|
||||
- **Click-to-Focus:** Go to `/settings` and enable **Use Alternate Buffer** to
|
||||
click within the embedded shell output to focus it for input.
|
||||
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
|
||||
user input. ([vid](https://imgur.com/a/kjK8bUK),
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
|
||||
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
|
||||
Features** to `true` to enable Gemini 3. For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Use lenient MCP output schema validator by @cornmander in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13521
|
||||
- Update persistence state to track counts of messages instead of times banner
|
||||
has been displayed by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13428
|
||||
- update docs for http proxy by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13538
|
||||
- move stdio by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13528
|
||||
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
|
||||
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13488
|
||||
- fix(ui): Correct mouse click cursor positioning for wide characters by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
|
||||
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13544
|
||||
- Optimize and improve test coverage for cli/src/config by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13485
|
||||
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13493
|
||||
- docs: fix typos in source code and documentation by @fancive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13577
|
||||
- Improved code coverage for cli/src/zed-integration by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13570
|
||||
- feat(ui): build interactive session browser component by @bl-ue in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13351
|
||||
- Fix multiple bugs with auth flow including using the implemented but unused
|
||||
restart support. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13565
|
||||
- feat(core): add modelAvailabilityService for managing and tracking model
|
||||
health by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13426
|
||||
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13595
|
||||
- feat: custom loading phrase when interactive shell requires input by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
|
||||
- docs: Update uninstall command to reflect multiple extension support by
|
||||
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
|
||||
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13596
|
||||
- Remove useModelRouter experimental flag by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13593
|
||||
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13535
|
||||
- Fix exp id logging by @owenofbrien in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13430
|
||||
- Moved client id logging into createBasicLogEvent by @owenofbrien in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13607
|
||||
- Restore bracketed paste mode after external editor exit by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13606
|
||||
- feat(core): Add support for custom aliases for model configs. by @joshualitt
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13546
|
||||
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13591
|
||||
- Turn off alternate buffer mode by default. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13623
|
||||
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13600
|
||||
- Improve test coverage for cli/src/ui/components by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13598
|
||||
- Update ink version to 6.4.6 by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13631
|
||||
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
|
||||
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
|
||||
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
|
||||
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13548
|
||||
- feat :rephrasing the extension logging messages to run the explore command
|
||||
when there are no extensions installed by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13740
|
||||
- Improve code coverage for cli package by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13724
|
||||
- Add session subtask in /stats command by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13750
|
||||
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12863
|
||||
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9082
|
||||
- fix: (some minor improvements to configs and getPackageJson return behaviour)
|
||||
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12510
|
||||
- feat(hooks): Hook Event Handling by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9097
|
||||
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9105
|
||||
- feat(core): Land bool for alternate system prompt. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13764
|
||||
- bug(core): Add default chat compression config. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13766
|
||||
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
|
||||
- feat(hooks): Hook System Orchestration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9102
|
||||
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13777
|
||||
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
|
||||
- chore: remove console.error by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13779
|
||||
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12763
|
||||
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13800
|
||||
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
|
||||
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
|
||||
- feat(sessions): add /resume slash command to open the session browser by
|
||||
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
|
||||
- docs(sessions): add documentation for chat recording and session management by
|
||||
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
|
||||
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13698
|
||||
- fallback to flash for TerminalQuota errors by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13791
|
||||
- Update Code Wiki README badge by @PatoBeltran in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13768
|
||||
- Add Databricks auth support and custom header option to gemini cli by
|
||||
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
|
||||
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13827
|
||||
- fix(patch): cherry-pick 576fda1 to release/v0.19.0-preview.0-pr-14099
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14402
|
||||
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
|
||||
output by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14159
|
||||
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
|
||||
- implement fuzzy search inside settings by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13864
|
||||
- feat: enable message bus integration by default by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14329
|
||||
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14334
|
||||
- feat: consolidate remote MCP servers to use `url` in config by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
|
||||
- Restrict integration tests tools by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14403
|
||||
- track github repository names in telemetry events by @IamRiddhi in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13670
|
||||
- Allow telemetry exporters to GCP to utilize user's login credentials, if
|
||||
requested by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13778
|
||||
- refactor(editor): use const assertion for editor types with single source of
|
||||
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
|
||||
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
|
||||
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
|
||||
- Add new enterprise instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8641
|
||||
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14151
|
||||
- Avoid triggering refreshStatic unless there really is a banner to display. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
|
||||
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14225
|
||||
- fix: Bundle default policies for npx distribution by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14457
|
||||
- feat(hooks): Hook System Documentation by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14307
|
||||
- Fix tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14458
|
||||
- feat: add scheduled workflow to close stale issues by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14404
|
||||
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14460
|
||||
- feat: Add enableAgents experimental flag by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14371
|
||||
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14374
|
||||
- Markdown export: move the emoji to the end of the line by @mhansen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12278
|
||||
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
|
||||
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
|
||||
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14475
|
||||
- Remove example extension by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14376
|
||||
- Add commands for listing and updating per-extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12664
|
||||
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13122
|
||||
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13471
|
||||
- Update setting search UX by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14451
|
||||
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
|
||||
- do not toggle the setting item when entering space by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14489
|
||||
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14439
|
||||
- remove unused isSearching field by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14509
|
||||
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
|
||||
- feat(cli): Move key restore logic to core by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13013
|
||||
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
|
||||
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14095
|
||||
- Adding session id as part of json o/p by @MJjainam in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14504
|
||||
- fix(extensions): resolve GitHub API 415 error for source tarballs by
|
||||
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
|
||||
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13002
|
||||
- Disable flaky extension reloading test on linux by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14528
|
||||
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
|
||||
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
|
||||
- Fix privacy screen for legacy tier users by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14522
|
||||
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14532
|
||||
- Grant chained workflows proper permission. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14534
|
||||
- Make trigger_e2e manually fireable. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14547
|
||||
- Write e2e status to local repo not forked repo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14549
|
||||
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
|
||||
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
|
||||
- Trigger chained e2e tests on all pull requests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14551
|
||||
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
|
||||
- Fix issue where we were passing the model content reflecting terminal line
|
||||
wrapping. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14566
|
||||
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
|
||||
- feat(sessions): use 1-line generated session summary to describe sessions by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
|
||||
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14585
|
||||
- fix(core): improve API response error handling and retry logic by @mattKorwel
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14563
|
||||
- Docs: Model routing clarification by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14373
|
||||
- expose previewFeatures flag in a2a by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14550
|
||||
- Fix emoji width in debug console. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14593
|
||||
- Fully detach autoupgrade process by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14595
|
||||
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14601
|
||||
- Disallow floating promises. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14605
|
||||
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
|
||||
- feat(modelAvailabilityService): integrate model availability service into
|
||||
backend logic by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14470
|
||||
- Add prompt_id propagation in a2a-server task by @koxkox111 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14581
|
||||
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
|
||||
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
|
||||
- fix(audio): improve reading of audio files by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14658
|
||||
- Update automated triage workflow to stop assigning priority labels by
|
||||
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
|
||||
- set failed status when chained e2e fails by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14725
|
||||
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
|
||||
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
|
||||
auth and add testing by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14589
|
||||
- Always set status in chained_e2e workflow by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14730
|
||||
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
|
||||
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
|
||||
- feat: auto-execute on slash command completion functions by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14584
|
||||
- Docs: Proper release notes by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14405
|
||||
- Add support for user-scoped extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13748
|
||||
- refactor(core): Improve environment variable handling in shell execution by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
|
||||
- Remove old E2E Workflows by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14749
|
||||
- fix: handle missing local extension config and skip hooks when disabled by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
|
||||
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
|
||||
- feat: Add support for MCP Resources by @MrLesk in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13178
|
||||
- Always set pending status in E2E tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14756
|
||||
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14746
|
||||
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14762
|
||||
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13015
|
||||
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14747
|
||||
- (fix) Automated pr labeller by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14788
|
||||
- Update CODEOWNERS by @kklashtorny1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14830
|
||||
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14842
|
||||
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14325
|
||||
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
|
||||
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
|
||||
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
|
||||
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
|
||||
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
|
||||
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
|
||||
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
|
||||
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
|
||||
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
|
||||
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
|
||||
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
|
||||
|
||||
+115
-127
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.19.0-preview.0
|
||||
# Preview release: Release v0.22.0-preview.0
|
||||
|
||||
Released: November 25, 2025
|
||||
Released: December 16, 2025
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -11,131 +11,119 @@ To install the preview release:
|
||||
npm install -g @google/gemini-cli@preview
|
||||
```
|
||||
|
||||
## What's changed
|
||||
## What's Changed
|
||||
|
||||
- Use lenient MCP output schema validator by @cornmander in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13521
|
||||
- Update persistence state to track counts of messages instead of times banner
|
||||
has been displayed by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13428
|
||||
- update docs for http proxy by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13538
|
||||
- move stdio by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13528
|
||||
- chore(release): bump version to 0.19.0-nightly.20251120.8e531dc02 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13540
|
||||
- Skip pre-commit hooks for shadow repo (#13331) by @vishvananda in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13488
|
||||
- fix(ui): Correct mouse click cursor positioning for wide characters by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/13537
|
||||
- fix(core): correct bash @P prompt transformation detection by @pyrytakala in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13544
|
||||
- Optimize and improve test coverage for cli/src/config by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13485
|
||||
- Improve code coverage for cli/src/ui/privacy package by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13493
|
||||
- docs: fix typos in source code and documentation by @fancive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13577
|
||||
- Improved code coverage for cli/src/zed-integration by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13570
|
||||
- feat(ui): build interactive session browser component by @bl-ue in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13351
|
||||
- Fix multiple bugs with auth flow including using the implemented but unused
|
||||
restart support. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13565
|
||||
- feat(core): add modelAvailabilityService for managing and tracking model
|
||||
health by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13426
|
||||
- docs: fix grammar typo "a MCP" to "an MCP" by @noahacgn in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13595
|
||||
- feat: custom loading phrase when interactive shell requires input by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/12535
|
||||
- docs: Update uninstall command to reflect multiple extension support by
|
||||
@JayadityaGit in https://github.com/google-gemini/gemini-cli/pull/13582
|
||||
- bug(core): Ensure we use thinking budget on fallback to 2.5 by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13596
|
||||
- Remove useModelRouter experimental flag by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13593
|
||||
- feat(docs): Ensure multiline JS objects are rendered properly. by @joshualitt
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13535
|
||||
- Fix exp id logging by @owenofbrien in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13430
|
||||
- Moved client id logging into createBasicLogEvent by @owenofbrien in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13607
|
||||
- Restore bracketed paste mode after external editor exit by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13606
|
||||
- feat(core): Add support for custom aliases for model configs. by @joshualitt
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13546
|
||||
- feat(core): Add `BaseLlmClient.generateContent`. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13591
|
||||
- Turn off alternate buffer mode by default. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13623
|
||||
- fix(cli): Prevent stdout/stderr patching for extension commands by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13600
|
||||
- Improve test coverage for cli/src/ui/components by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13598
|
||||
- Update ink version to 6.4.6 by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13631
|
||||
- chore/release: bump version to 0.19.0-nightly.20251122.42c2e1b21 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13637
|
||||
- chore/release: bump version to 0.19.0-nightly.20251123.dadd606c0 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13675
|
||||
- chore/release: bump version to 0.19.0-nightly.20251124.e177314a4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13713
|
||||
- fix(core): Fix context window overflow warning for PDF files by @kkitase in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13548
|
||||
- feat :rephrasing the extension logging messages to run the explore command
|
||||
when there are no extensions installed by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13740
|
||||
- Improve code coverage for cli package by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13724
|
||||
- Add session subtask in /stats command by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13750
|
||||
- feat(core): Migrate chatCompressionService to model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12863
|
||||
- feat(hooks): Hook Telemetry Infrastructure by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9082
|
||||
- fix: (some minor improvements to configs and getPackageJson return behaviour)
|
||||
by @grMLEqomlkkU5Eeinz4brIrOVCUCkJuN in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12510
|
||||
- feat(hooks): Hook Event Handling by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9097
|
||||
- feat(hooks): Hook Agent Lifecycle Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9105
|
||||
- feat(core): Land bool for alternate system prompt. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13764
|
||||
- bug(core): Add default chat compression config. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13766
|
||||
- feat(model-availability): introduce ModelPolicy and PolicyCatalog by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13751
|
||||
- feat(hooks): Hook System Orchestration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9102
|
||||
- feat(config): add isModelAvailabilityServiceEnabled setting by @adamfweidman
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13777
|
||||
- chore/release: bump version to 0.19.0-nightly.20251125.f6d97d448 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13782
|
||||
- chore: remove console.error by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13779
|
||||
- fix: Add $schema property to settings.schema.json by @sacrosanctic in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12763
|
||||
- fix(cli): allow non-GitHub SCP-styled URLs for extension installation by @m0ps
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13800
|
||||
- fix(resume): allow passing a prompt via stdin while resuming using --resume by
|
||||
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13520
|
||||
- feat(sessions): add /resume slash command to open the session browser by
|
||||
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13621
|
||||
- docs(sessions): add documentation for chat recording and session management by
|
||||
@bl-ue in https://github.com/google-gemini/gemini-cli/pull/13667
|
||||
- Fix TypeError: "URL.parse is not a function" for Node.js < v22 by @macarronesc
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13698
|
||||
- fallback to flash for TerminalQuota errors by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13791
|
||||
- Update Code Wiki README badge by @PatoBeltran in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13768
|
||||
- Add Databricks auth support and custom header option to gemini cli by
|
||||
@AarushiShah in https://github.com/google-gemini/gemini-cli/pull/11893
|
||||
- Update dependency for modelcontextprotocol/sdk to 1.23.0 by @bbiggs in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13827
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.18.0-preview.4...v0.19.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
|
||||
|
||||
+475
-9
@@ -12,13 +12,479 @@ on GitHub.
|
||||
|
||||
## Current Releases
|
||||
|
||||
| Release channel | Notes |
|
||||
| :----------------------------------------- | :---------------------------------------------- |
|
||||
| Nightly | Nightly release with the most recent changes. |
|
||||
| [Preview](#release-v0190-preview0-preview) | Experimental features ready for early feedback. |
|
||||
| [Latest](#release-v0190---v0194-latest) | Stable, recommended for general use. |
|
||||
| Release channel | Notes |
|
||||
| :------------------------------------------ | :---------------------------------------------- |
|
||||
| Nightly | Nightly release with the most recent changes. |
|
||||
| [Preview](#release-v0220-preview-0-preview) | Experimental features ready for early feedback. |
|
||||
| [Latest](#release-v0210---v0211-latest) | Stable, recommended for general use. |
|
||||
|
||||
## Release v0.19.0 - v0.19.4 (Latest)
|
||||
## Release v0.21.0 - v0.21.1 (Latest)
|
||||
|
||||
### Highlights
|
||||
|
||||
- **⚡️⚡️⚡️ Gemini 3 Flash + Gemini CLI:** If you are a paid user, you can now
|
||||
enable Gemini 3 Pro and Gemini 3 Flash. Go to `/settings` and set **Preview
|
||||
Features** to `true` to enable Gemini 3. For more information:
|
||||
[Gemini 3 Flash is now available in Gemini CLI](https://developers.googleblog.com/gemini-3-flash-is-now-available-in-gemini-cli/).
|
||||
|
||||
### What's Changed
|
||||
|
||||
- refactor(stdio): always patch stdout and use createWorkingStdio for clean
|
||||
output by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14159
|
||||
- chore(release): bump version to 0.21.0-nightly.20251202.2d935b379 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14409
|
||||
- implement fuzzy search inside settings by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13864
|
||||
- feat: enable message bus integration by default by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14329
|
||||
- docs: Recommend using --debug intead of --verbose for CLI debugging by @bbiggs
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14334
|
||||
- feat: consolidate remote MCP servers to use `url` in config by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/13762
|
||||
- Restrict integration tests tools by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14403
|
||||
- track github repository names in telemetry events by @IamRiddhi in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13670
|
||||
- Allow telemetry exporters to GCP to utilize user's login credentials, if
|
||||
requested by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13778
|
||||
- refactor(editor): use const assertion for editor types with single source of
|
||||
truth by @amsminn in https://github.com/google-gemini/gemini-cli/pull/8604
|
||||
- fix(security): Fix npm audit vulnerabilities in glob and body-parser by
|
||||
@afarber in https://github.com/google-gemini/gemini-cli/pull/14090
|
||||
- Add new enterprise instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8641
|
||||
- feat(hooks): Hook Session Lifecycle & Compression Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14151
|
||||
- Avoid triggering refreshStatic unless there really is a banner to display. by
|
||||
@jacob314 in https://github.com/google-gemini/gemini-cli/pull/14328
|
||||
- feat(hooks): Hooks Commands Panel, Enable/Disable, and Migrate by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14225
|
||||
- fix: Bundle default policies for npx distribution by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14457
|
||||
- feat(hooks): Hook System Documentation by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14307
|
||||
- Fix tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14458
|
||||
- feat: add scheduled workflow to close stale issues by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14404
|
||||
- feat: Support Extension Hooks with Security Warning by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14460
|
||||
- feat: Add enableAgents experimental flag by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14371
|
||||
- docs: fix typo 'socus' to 'focus' in todos.md by @Viktor286 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14374
|
||||
- Markdown export: move the emoji to the end of the line by @mhansen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12278
|
||||
- fix(acp): prevent unnecessary credential cache clearing on re-authent… by
|
||||
@h-michael in https://github.com/google-gemini/gemini-cli/pull/9410
|
||||
- fix(cli): Fix word navigation for CJK characters by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14475
|
||||
- Remove example extension by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14376
|
||||
- Add commands for listing and updating per-extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12664
|
||||
- chore(tests): remove obsolete test for hierarchical memory by @pareshjoshij in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13122
|
||||
- feat(cli): support /copy in remote sessions using OSC52 by @ismellpillows in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13471
|
||||
- Update setting search UX by @Adib234 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14451
|
||||
- Fix(cli): Improve Homebrew update instruction to specify gemini-cli by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/14502
|
||||
- do not toggle the setting item when entering space by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14489
|
||||
- fix: improve retry logic for fetch errors and network codes by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14439
|
||||
- remove unused isSearching field by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14509
|
||||
- feat(mcp): add `--type` alias for `--transport` flag in gemini mcp add by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14503
|
||||
- feat(cli): Move key restore logic to core by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13013
|
||||
- feat: add auto-execute on Enter behavior to argumentless MCP prompts by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14510
|
||||
- fix(shell): cursor visibility when using interactive mode by @aswinashok44 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14095
|
||||
- Adding session id as part of json o/p by @MJjainam in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14504
|
||||
- fix(extensions): resolve GitHub API 415 error for source tarballs by
|
||||
@jpoehnelt in https://github.com/google-gemini/gemini-cli/pull/13319
|
||||
- fix(client): Correctly latch hasFailedCompressionAttempt flag by @pareshjoshij
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13002
|
||||
- Disable flaky extension reloading test on linux by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14528
|
||||
- Add support for MCP dynamic tool update by `notifications/tools/list_changed`
|
||||
by @Adib234 in https://github.com/google-gemini/gemini-cli/pull/14375
|
||||
- Fix privacy screen for legacy tier users by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14522
|
||||
- feat: Exclude maintainer labeled issues from stale issue closer by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14532
|
||||
- Grant chained workflows proper permission. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14534
|
||||
- Make trigger_e2e manually fireable. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14547
|
||||
- Write e2e status to local repo not forked repo by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14549
|
||||
- Fixes [API Error: Cannot read properties of undefined (reading 'error')] by
|
||||
@silviojr in https://github.com/google-gemini/gemini-cli/pull/14553
|
||||
- Trigger chained e2e tests on all pull requests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14551
|
||||
- Fix bug in the shellExecutionService resulting in both truncation and 3X bloat
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14545
|
||||
- Fix issue where we were passing the model content reflecting terminal line
|
||||
wrapping. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14566
|
||||
- chore/release: bump version to 0.21.0-nightly.20251204.3da4fd5f7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14476
|
||||
- feat(sessions): use 1-line generated session summary to describe sessions by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14467
|
||||
- Use Robot PAT for chained e2e merge queue skipper by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14585
|
||||
- fix(core): improve API response error handling and retry logic by @mattKorwel
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14563
|
||||
- Docs: Model routing clarification by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14373
|
||||
- expose previewFeatures flag in a2a by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14550
|
||||
- Fix emoji width in debug console. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14593
|
||||
- Fully detach autoupgrade process by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14595
|
||||
- Docs: Update Gemini 3 on Gemini CLI documentation by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14601
|
||||
- Disallow floating promises. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14605
|
||||
- chore/release: bump version to 0.21.0-nightly.20251207.025e450ac by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14662
|
||||
- feat(modelAvailabilityService): integrate model availability service into
|
||||
backend logic by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14470
|
||||
- Add prompt_id propagation in a2a-server task by @koxkox111 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14581
|
||||
- Fix: Prevent freezing in non-interactive Gemini CLI when debug mode is enabled
|
||||
by @parthasaradhie in https://github.com/google-gemini/gemini-cli/pull/14580
|
||||
- fix(audio): improve reading of audio files by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14658
|
||||
- Update automated triage workflow to stop assigning priority labels by
|
||||
@skeshive in https://github.com/google-gemini/gemini-cli/pull/14717
|
||||
- set failed status when chained e2e fails by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14725
|
||||
- feat(github action) Triage and Label Pull Requests by Size and Comple… by
|
||||
@DaanVersavel in https://github.com/google-gemini/gemini-cli/pull/5571
|
||||
- refactor(telemetry): Improve previous PR that allows telemetry to use the CLI
|
||||
auth and add testing by @mboshernitsan in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14589
|
||||
- Always set status in chained_e2e workflow by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14730
|
||||
- feat: Add OTEL log event `gemini_cli.startup_stats` for startup stats. by
|
||||
@kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/14734
|
||||
- feat: auto-execute on slash command completion functions by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14584
|
||||
- Docs: Proper release notes by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14405
|
||||
- Add support for user-scoped extension settings by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13748
|
||||
- refactor(core): Improve environment variable handling in shell execution by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14742
|
||||
- Remove old E2E Workflows by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14749
|
||||
- fix: handle missing local extension config and skip hooks when disabled by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14744
|
||||
- chore/release: bump version to 0.21.0-nightly.20251209.ec9a8c7a7 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14751
|
||||
- feat: Add support for MCP Resources by @MrLesk in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13178
|
||||
- Always set pending status in E2E tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14756
|
||||
- fix(lint): upgrade pip and use public pypi for yamllint by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14746
|
||||
- fix: use Gemini API supported image formats for clipboard by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14762
|
||||
- feat(a2a): Introduce restore command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13015
|
||||
- allow final:true to be returned on a2a server edit calls. by @DavidAPierce in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14747
|
||||
- (fix) Automated pr labeller by @DaanVersavel in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14788
|
||||
- Update CODEOWNERS by @kklashtorny1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14830
|
||||
- Docs: Fix errors preventing site rebuild. by @jkcinouye in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14842
|
||||
- chore(deps): bump express from 5.1.0 to 5.2.0 by @dependabot[bot] in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14325
|
||||
- fix(patch): cherry-pick 3f5f030 to release/v0.21.0-preview.0-pr-14843 to patch
|
||||
version v0.21.0-preview.0 and create version 0.21.0-preview.1 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14851
|
||||
- fix(patch): cherry-pick ee6556c to release/v0.21.0-preview.1-pr-14691 to patch
|
||||
version v0.21.0-preview.1 and create version 0.21.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14908
|
||||
- fix(patch): cherry-pick 54de675 to release/v0.21.0-preview.2-pr-14961 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14968
|
||||
- fix(patch): cherry-pick 12cbe32 to release/v0.21.0-preview.3-pr-15000 to patch
|
||||
version v0.21.0-preview.3 and create version 0.21.0-preview.4 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15003
|
||||
- fix(patch): cherry-pick edbe548 to release/v0.21.0-preview.4-pr-15007 to patch
|
||||
version v0.21.0-preview.4 and create version 0.21.0-preview.5 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15015
|
||||
- fix(patch): cherry-pick 2995af6 to release/v0.21.0-preview.5-pr-15131 to patch
|
||||
version v0.21.0-preview.5 and create version 0.21.0-preview.6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15153
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.20.2...v0.21.0
|
||||
|
||||
## Release v0.22.0-preview-0 (Preview)
|
||||
|
||||
### What's Changed
|
||||
|
||||
- feat(ide): fallback to GEMINI_CLI_IDE_AUTH_TOKEN env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14843
|
||||
- feat: display quota stats for unused models in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14764
|
||||
- feat: ensure codebase investigator uses preview model when main agent does by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14412
|
||||
- chore: add closing reason to stale bug workflow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14861
|
||||
- Send the model and CLI version with the user agent by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14865
|
||||
- refactor(sessions): move session summary generation to startup by
|
||||
@jackwotherspoon in https://github.com/google-gemini/gemini-cli/pull/14691
|
||||
- Limit search depth in path corrector by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14869
|
||||
- Fix: Correct typo in code comment by @kuishou68 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14671
|
||||
- feat(core): Plumbing for late resolution of model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14597
|
||||
- feat: attempt more error parsing by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14899
|
||||
- Add missing await. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14910
|
||||
- feat(core): Add support for transcript_path in hooks for git-ai/Gemini
|
||||
extension by @svarlamov in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14663
|
||||
- refactor: implement DelegateToAgentTool with discriminated union by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/14769
|
||||
- feat: reset availabilityService on /auth by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14911
|
||||
- chore/release: bump version to 0.21.0-nightly.20251211.8c83e1ea9 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14924
|
||||
- Fix: Correctly detect MCP tool errors by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14937
|
||||
- increase labeler timeout by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14922
|
||||
- tool(cli): tweak the frontend tool to be aware of more core files from the cli
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14962
|
||||
- feat(cli): polish cached token stats and simplify stats display when quota is
|
||||
present. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14961
|
||||
- feat(settings-validation): add validation for settings schema by @lifefloating
|
||||
in https://github.com/google-gemini/gemini-cli/pull/12929
|
||||
- fix(ide): Update IDE extension to write auth token in env var by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14999
|
||||
- Revert "chore(deps): bump express from 5.1.0 to 5.2.0" by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14998
|
||||
- feat(a2a): Introduce /init command for a2a server by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13419
|
||||
- feat: support multi-file drag and drop of images by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14832
|
||||
- fix(policy): allow codebase_investigator by default in read-only policy by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/15000
|
||||
- refactor(ide ext): Update port file name + switch to 1-based index for
|
||||
characters + remove truncation text by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/10501
|
||||
- fix(vscode-ide-companion): correct license generation for workspace
|
||||
dependencies by @skeshive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15004
|
||||
- fix: temp fix for subagent invocation until subagent delegation is merged to
|
||||
stable by @abhipatel12 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15007
|
||||
- test: update ide detection tests to make them more robust when run in an ide
|
||||
by @kevin-ramdass in https://github.com/google-gemini/gemini-cli/pull/15008
|
||||
- Remove flex from stats display. See snapshots for diffs. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14983
|
||||
- Add license field into package.json by @jb-perez in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14473
|
||||
- feat: Persistent "Always Allow" policies with granular shell & MCP support by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/14737
|
||||
- chore/release: bump version to 0.21.0-nightly.20251212.54de67536 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14969
|
||||
- fix(core): commandPrefix word boundary and compound command safety by
|
||||
@allenhutchison in https://github.com/google-gemini/gemini-cli/pull/15006
|
||||
- chore(docs): add 'Maintainers only' label info to CONTRIBUTING.md by @jacob314
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14914
|
||||
- Refresh hooks when refreshing extensions. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14918
|
||||
- Add clarity to error messages by @gsehgal in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14879
|
||||
- chore : remove a redundant tip by @JayadityaGit in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14947
|
||||
- chore/release: bump version to 0.21.0-nightly.20251213.977248e09 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/15029
|
||||
- Disallow redundant typecasts. by @gundermanc in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15030
|
||||
- fix(auth): prioritize GEMINI_API_KEY env var and skip unnecessary key… by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/14745
|
||||
- fix: use zod for safety check result validation by @allenhutchison in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15026
|
||||
- update(telemetry): add hashed_extension_name to field to extension events by
|
||||
@kiranani in https://github.com/google-gemini/gemini-cli/pull/15025
|
||||
- fix: similar to policy-engine, throw error in case of requiring tool execution
|
||||
confirmation for non-interactive mode by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14702
|
||||
- Clean up processes in integration tests by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15102
|
||||
- docs: update policy engine getting started and defaults by @NTaylorMullen in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15105
|
||||
- Fix tool output fragmentation by encapsulating content in functionResponse by
|
||||
@abhipatel12 in https://github.com/google-gemini/gemini-cli/pull/13082
|
||||
- Simplify method signature. by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15114
|
||||
- Show raw input token counts in json output. by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15021
|
||||
- fix: Mark A2A requests as interactive by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15108
|
||||
- use previewFeatures to determine which pro model to use for A2A by @sehoon38
|
||||
in https://github.com/google-gemini/gemini-cli/pull/15131
|
||||
- refactor(cli): fix settings merging so that settings using the new json format
|
||||
take priority over ones using the old format by @jacob314 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/15116
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.21.0-preview.6...v0.22.0-preview.0
|
||||
|
||||
## Release v0.20.0 - v0.20.2
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Update error codes when process exiting the gemini cli by @megha1188 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13728
|
||||
- chore(release): bump version to 0.20.0-nightly.20251126.d2a6cff4d by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13835
|
||||
- feat(core): Improve request token calculation accuracy by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13824
|
||||
- Changes in system instruction to adapt to gemini 3.0 to ensure that the CLI
|
||||
explains its actions before calling tools by @silviojr in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13810
|
||||
- feat(hooks): Hook Tool Execution Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9108
|
||||
- Add support for MCP server instructions behind config option by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13432
|
||||
- Update System Instructions for interactive vs non-interactive mode. by
|
||||
@aishaneeshah in https://github.com/google-gemini/gemini-cli/pull/12315
|
||||
- Add consent flag to Link command by @kevinjwang1 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13832
|
||||
- feat(mcp): Inject GoogleCredentialProvider headers in McpClient by
|
||||
@sai-sunder-s in https://github.com/google-gemini/gemini-cli/pull/13783
|
||||
- feat(core): implement towards policy-driven model fallback mechanism by
|
||||
@adamfweidman in https://github.com/google-gemini/gemini-cli/pull/13781
|
||||
- feat(core): Add configurable inactivity timeout for shell commands by @galz10
|
||||
in https://github.com/google-gemini/gemini-cli/pull/13531
|
||||
- fix(auth): improve API key authentication flow by @galz10 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13829
|
||||
- feat(hooks): Hook LLM Request/Response Integration by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9110
|
||||
- feat(ui): Show waiting MCP servers in ConfigInitDisplay by @werdnum in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13721
|
||||
- Add usage limit remaining in /stats by @sehoon38 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13843
|
||||
- feat(shell): Standardize pager to 'cat' for shell execution by model by
|
||||
@galz10 in https://github.com/google-gemini/gemini-cli/pull/13878
|
||||
- chore/release: bump version to 0.20.0-nightly.20251127.5bed97064 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/13877
|
||||
- Revert to default LICENSE (Revert #13449) by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13876
|
||||
- update(telemetry): OTel API response event with finish reasons by @kiranani in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13849
|
||||
- feat(hooks): Hooks Comprehensive Integration Testing by @Edilmo in
|
||||
https://github.com/google-gemini/gemini-cli/pull/9112
|
||||
- chore: fix session browser test and skip hook system tests by @jackwotherspoon
|
||||
in https://github.com/google-gemini/gemini-cli/pull/14099
|
||||
- feat(telemetry): Add Semantic logging for to ApiRequestEvents by @kiranani in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13912
|
||||
- test: Add verification for $schema property in settings schema by
|
||||
@maryamariyan in https://github.com/google-gemini/gemini-cli/pull/13497
|
||||
- Fixes `/clear` command to preserve input history for up-arrow navigation while
|
||||
still clearing the context window and screen by @korade-krushna in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14182
|
||||
- fix(core): handle EPIPE error in hook runner when writing to stdin by
|
||||
@SandyTao520 in https://github.com/google-gemini/gemini-cli/pull/14231
|
||||
- fix: Exclude web-fetch tool from executing in default non-interactive mode to
|
||||
avoid CLI hang. by @MayV in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14244
|
||||
- Always use MCP server instructions by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14297
|
||||
- feat: auto-execute simple slash commands on Enter by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13985
|
||||
- chore/release: bump version to 0.20.0-nightly.20251201.2fe609cb6 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14304
|
||||
- feat: Add startup profiler to measure and record application initialization
|
||||
phases. by @kevin-ramdass in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13638
|
||||
- bug(core): Avoid stateful tool use in `executor`. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14305
|
||||
- feat(themes): add built-in holiday theme 🎁 by @jackwotherspoon in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14301
|
||||
- Updated ToC on docs intro; updated title casing to match Google style by
|
||||
@pcoet in https://github.com/google-gemini/gemini-cli/pull/13717
|
||||
- feat(a2a): Urgent fix - Process modelInfo agent message by @cocosheng-g in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14315
|
||||
- feat(core): enhance availability routing with wrapped fallback and
|
||||
single-model policies by @adamfweidman in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13874
|
||||
- chore(logging): log the problematic event for #12122 by @briandealwis in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14092
|
||||
- fix: remove invalid type key in bug_report.yml by @fancive in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13576
|
||||
- update screenshot by @Transient-Onlooker in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13976
|
||||
- docs: Fix grammar error in Release Cadence (Nightly section) by @JuanCS-Dev in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13866
|
||||
- fix(async): prevent missed async errors from bypassing catch handlers by
|
||||
@amsminn in https://github.com/google-gemini/gemini-cli/pull/13714
|
||||
- fix(zed-integration): remove extra field from acp auth request by
|
||||
@marcocondrache in https://github.com/google-gemini/gemini-cli/pull/13646
|
||||
- feat(cli): Documentation for model configs. by @joshualitt in
|
||||
https://github.com/google-gemini/gemini-cli/pull/12967
|
||||
- fix(ui): misaligned markdown table rendering by @dumbbellcode in
|
||||
https://github.com/google-gemini/gemini-cli/pull/8336
|
||||
- docs: Update 4 files by @g-samroberts in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13628
|
||||
- fix: Conditionally add set -eEuo pipefail in setup-github command by @Smetalo
|
||||
in https://github.com/google-gemini/gemini-cli/pull/8550
|
||||
- fix(cli): fix issue updating a component while rendering a different component
|
||||
by @jacob314 in https://github.com/google-gemini/gemini-cli/pull/14319
|
||||
- Increase flakey test timeout by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14377
|
||||
- Remove references to deleted kind/bug label by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14383
|
||||
- Don't fail test if we can't cleanup by @scidomino in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14389
|
||||
- feat(core): Implement JIT context manager and setting by @SandyTao520 in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14324
|
||||
- Use polling for extensions-reload integration test by @chrstnb in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14391
|
||||
- Add docs directive to GEMINI.md by @g-samroberts in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14327
|
||||
- Hide sessions that don't have user messages by @bl-ue in
|
||||
https://github.com/google-gemini/gemini-cli/pull/13994
|
||||
- chore(ci): mark GitHub release as pre-release if not on "latest" npm channel
|
||||
by @ljxfstorm in https://github.com/google-gemini/gemini-cli/pull/7386
|
||||
- fix(patch): cherry-pick d284fa6 to release/v0.20.0-preview.0-pr-14545
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
https://github.com/google-gemini/gemini-cli/pull/14559
|
||||
- fix(patch): cherry-pick 828afe1 to release/v0.20.0-preview.1-pr-14159 to patch
|
||||
version v0.20.0-preview.1 and create version 0.20.0-preview.2 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14733
|
||||
- fix(patch): cherry-pick 171103a to release/v0.20.0-preview.2-pr-14742 to patch
|
||||
version v0.20.0-preview.2 and create version 0.20.0-preview.5 by
|
||||
@gemini-cli-robot in https://github.com/google-gemini/gemini-cli/pull/14752
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.19.4...v0.20.0
|
||||
|
||||
## Release v0.19.0 - v0.19.4
|
||||
|
||||
## Highlights
|
||||
|
||||
@@ -26,10 +492,10 @@ on GitHub.
|
||||
integration after enabling "Preview Features" in their CLI’s `/settings`.
|
||||
- **Interactive shell:**
|
||||
- **Click-to-Focus:** Go to `/settings` and enable **Use Alternate Buffer**
|
||||
WhenUse Alternate Buffer" setting is enabled users can click within the
|
||||
When "Use Alternate Buffer" setting is enabled users can click within the
|
||||
embedded shell output to focus it for input.
|
||||
- **Loading phrase:** Clearly indicates when the interactive shell is awaiting
|
||||
user input. ([vid](https://imgur.com/a/kjK8bUK),
|
||||
user input. ([vid](https://imgur.com/a/kjK8bUK)
|
||||
[pr](https://github.com/google-gemini/gemini-cli/pull/12535) by
|
||||
[@jackwotherspoon](https://github.com/jackwotherspoon))
|
||||
|
||||
@@ -165,7 +631,7 @@ on GitHub.
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.18.4...v0.19.0
|
||||
|
||||
## Release v0.19.0-preview.0 (Preview)
|
||||
## Release v0.19.0-preview.0
|
||||
|
||||
### What's Changed
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ overview of Gemini CLI, see the [main documentation page](../index.md).
|
||||
files and directories from being accessed by tools.
|
||||
- **[Context files (GEMINI.md)](./gemini-md.md):** Provide persistent,
|
||||
hierarchical context to the model.
|
||||
- **[System prompt override](./system-prompt.md):** Replace the built‑in system
|
||||
instructions using `GEMINI_SYSTEM_MD`.
|
||||
|
||||
## Non-interactive mode
|
||||
|
||||
|
||||
@@ -84,10 +84,10 @@ available combinations.
|
||||
|
||||
#### External Tools
|
||||
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ---------- |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V` |
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ------------------------- |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
|
||||
+11
-15
@@ -6,24 +6,20 @@ and provides resilience when the primary model is unavailable.
|
||||
|
||||
## How it works
|
||||
|
||||
Model routing is not based on prompt complexity, but is a fallback mechanism.
|
||||
Here's how it works:
|
||||
Model routing is managed by the `ModelAvailabilityService`, which monitors model
|
||||
health and automatically routes requests to available models based on defined
|
||||
policies.
|
||||
|
||||
1. **Model failure:** If the currently selected model fails to respond (for
|
||||
example, due to a server error or other issue), the CLI will initiate the
|
||||
fallback process.
|
||||
1. **Model failure:** If the currently selected model fails (e.g., due to quota
|
||||
or server errors), the CLI will iniate the fallback process.
|
||||
|
||||
2. **User consent:** The CLI will prompt you to ask if you want to switch to
|
||||
the fallback model. This is handled by the `fallbackModelHandler`.
|
||||
2. **User consent:** Depending on the failure and the model's policy, the CLI
|
||||
may prompt you to switch to a fallback model (by default always prompts
|
||||
you).
|
||||
|
||||
3. **Fallback activation:** If you consent, the CLI will activate the fallback
|
||||
mode by calling `config.setFallbackMode(true)`.
|
||||
|
||||
4. **Model switch:** On the next request, the CLI will use the
|
||||
`DEFAULT_GEMINI_FLASH_MODEL` as the fallback model. This is handled by the
|
||||
`resolveModel` function in
|
||||
`packages/cli/src/zed-integration/zedIntegration.ts` which checks if
|
||||
`isInFallbackMode()` is true.
|
||||
3. **Model switch:** If approved, or if the policy allows for silent fallback,
|
||||
the CLI will use an available fallback model for the current turn or the
|
||||
remainder of the session.
|
||||
|
||||
### Model selection precedence
|
||||
|
||||
|
||||
+30
-30
@@ -1,12 +1,13 @@
|
||||
# Gemini CLI model selection (`/model` command)
|
||||
|
||||
Select your Gemini CLI model. The `/model` command opens a dialog where you can
|
||||
configure the model used by Gemini CLI, giving you more control over your
|
||||
results.
|
||||
Select your Gemini CLI model. The `/model` command lets you configure the model
|
||||
used by Gemini CLI, giving you more control over your results. Use **Pro**
|
||||
models for complex tasks and reasoning, **Flash** models for high speed results,
|
||||
or the (recommended) **Auto** setting to choose the best model for your tasks.
|
||||
|
||||
**Note:** The `/model` command (and the `--model` flag) does not override the
|
||||
model used by sub-agents. Consequently, even when using the `/model` flag you
|
||||
may see other models used in your model usage reports.
|
||||
> **Note:** The `/model` command (and the `--model` flag) does not override the
|
||||
> model used by sub-agents. Consequently, even when using the `/model` flag you
|
||||
> may see other models used in your model usage reports.
|
||||
|
||||
## How to use the `/model` command
|
||||
|
||||
@@ -16,26 +17,25 @@ Use the following command in Gemini CLI:
|
||||
/model
|
||||
```
|
||||
|
||||
Running this command will open a dialog with your model options:
|
||||
Running this command will open a dialog with your options:
|
||||
|
||||
| Option | Description | Models |
|
||||
| ------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Auto (recommended) | Let the system choose the best model for your task. | gemini-3-pro-preview (if enabled), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite |
|
||||
| Pro | For complex tasks that require deep reasoning and creativity. | gemini-3-pro-preview (if enabled), gemini-2.5-pro |
|
||||
| Flash | For tasks that need a balance of speed and reasoning. | gemini-2.5-flash |
|
||||
| Flash-Lite | For simple tasks that need to be done quickly. | gemini-2.5-flash-lite |
|
||||
| Option | Description | Models |
|
||||
| ----------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| Auto (Gemini 3) | Let the system choose the best Gemini 3 model for your task. | gemini-3-pro-preview (if enabled), gemini-3-flash-preview (if enabled) |
|
||||
| Auto (Gemini 2.5) | Let the system choose the best Gemini 2.5 model for your task. | gemini-2.5-pro, gemini-2.5-flash |
|
||||
| Manual | Select a specific model. | Any available model. |
|
||||
|
||||
### Gemini 3 Pro and preview features
|
||||
We recommend selecting one of the above **Auto** options. However, you can
|
||||
select **Manual** to select a specific model from those available.
|
||||
|
||||
Note: Gemini 3 is not currently available on all account types. To learn more
|
||||
about Gemini 3 access, refer to
|
||||
[Gemini 3 Pro on Gemini CLI](../get-started/gemini-3.md).
|
||||
### Gemini 3 and preview features
|
||||
|
||||
To enable Gemini 3 Pro (if available), enable
|
||||
[**Preview features** by using the `settings` command](../cli/settings.md). Once
|
||||
enabled, Gemini CLI will attempt to use Gemini 3 Pro when you select **Auto** or
|
||||
**Pro**. Both **Auto** and **Pro** will try to use Gemini 3 Pro before falling
|
||||
back to Gemini 2.5 Pro.
|
||||
> **Note:** Gemini 3 is not currently available on all account types. To learn
|
||||
> more about Gemini 3 access, refer to
|
||||
> [Gemini 3 on Gemini CLI](../get-started/gemini-3.md).
|
||||
|
||||
To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
|
||||
[**Preview Features** by using the `settings` command](../cli/settings.md).
|
||||
|
||||
You can also use the `--model` flag to specify a particular Gemini model on
|
||||
startup. For more details, refer to the
|
||||
@@ -46,16 +46,16 @@ Gemini CLI.
|
||||
|
||||
## Best practices for model selection
|
||||
|
||||
- **Default to Auto (recommended).** For most users, the _Auto (recommended)_
|
||||
model provides a balance between speed and performance, automatically
|
||||
selecting the correct model based on the complexity of the task. Example:
|
||||
Developing a web application could include a mix of complex tasks (building
|
||||
architecture and scaffolding the project) and simple tasks (generating CSS).
|
||||
- **Default to Auto.** For most users, the _Auto_ option model provides a
|
||||
balance between speed and performance, automatically selecting the correct
|
||||
model based on the complexity of the task. Example: Developing a web
|
||||
application could include a mix of complex tasks (building architecture and
|
||||
scaffolding the project) and simple tasks (generating CSS).
|
||||
|
||||
- **Switch to Pro if you aren't getting the results you want.** If you think you
|
||||
need your model to be a little "smarter," use Pro. Pro will provide you with
|
||||
the highest levels of reasoning and creativity. Example: A complex or
|
||||
multi-stage debugging task.
|
||||
need your model to be a little "smarter," you can manually select Pro. Pro
|
||||
will provide you with the highest levels of reasoning and creativity. Example:
|
||||
A complex or multi-stage debugging task.
|
||||
|
||||
- **Switch to Flash or Flash-Lite if you need faster results.** If you need a
|
||||
simple response quickly, Flash or Flash-Lite is the best option. Example:
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# System Prompt Override (GEMINI_SYSTEM_MD)
|
||||
|
||||
The core system instructions that guide Gemini CLI can be completely replaced
|
||||
with your own Markdown file. This feature is controlled via the
|
||||
`GEMINI_SYSTEM_MD` environment variable.
|
||||
|
||||
## Overview
|
||||
|
||||
The `GEMINI_SYSTEM_MD` variable instructs the CLI to use an external Markdown
|
||||
file for its system prompt, completely overriding the built-in default. This is
|
||||
a full replacement, not a merge. If you use a custom file, none of the original
|
||||
core instructions will apply unless you include them yourself.
|
||||
|
||||
This feature is intended for advanced users who need to enforce strict,
|
||||
project-specific behavior or create a customized persona.
|
||||
|
||||
> Tip: You can export the current default system prompt to a file first, review
|
||||
> it, and then selectively modify or replace it (see
|
||||
> [“Export the default prompt”](#export-the-default-prompt-recommended)).
|
||||
|
||||
## How to enable
|
||||
|
||||
You can set the environment variable temporarily in your shell, or persist it
|
||||
via a `.gemini/.env` file. See
|
||||
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
|
||||
|
||||
- Use the project default path (`.gemini/system.md`):
|
||||
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
|
||||
- The CLI reads `./.gemini/system.md` (relative to your current project
|
||||
directory).
|
||||
|
||||
- Use a custom file path:
|
||||
- `GEMINI_SYSTEM_MD=/absolute/path/to/my-system.md`
|
||||
- Relative paths are supported and resolved from the current working
|
||||
directory.
|
||||
- Tilde expansion is supported (e.g., `~/my-system.md`).
|
||||
|
||||
- Disable the override (use built‑in prompt):
|
||||
- `GEMINI_SYSTEM_MD=false` or `GEMINI_SYSTEM_MD=0` or unset the variable.
|
||||
|
||||
If the override is enabled but the target file does not exist, the CLI will
|
||||
error with: `missing system prompt file '<path>'`.
|
||||
|
||||
## Quick examples
|
||||
|
||||
- One‑off session using a project file:
|
||||
- `GEMINI_SYSTEM_MD=1 gemini`
|
||||
- Persist for a project using `.gemini/.env`:
|
||||
- Create `.gemini/system.md`, then add to `.gemini/.env`:
|
||||
- `GEMINI_SYSTEM_MD=1`
|
||||
- Use a custom file under your home directory:
|
||||
- `GEMINI_SYSTEM_MD=~/prompts/SYSTEM.md gemini`
|
||||
|
||||
## UI indicator
|
||||
|
||||
When `GEMINI_SYSTEM_MD` is active, the CLI shows a `|⌐■_■|` indicator in the UI
|
||||
to signal custom system‑prompt mode.
|
||||
|
||||
## Export the default prompt (recommended)
|
||||
|
||||
Before overriding, export the current default prompt so you can review required
|
||||
safety and workflow rules.
|
||||
|
||||
- Write the built‑in prompt to the project default path:
|
||||
- `GEMINI_WRITE_SYSTEM_MD=1 gemini`
|
||||
- Or write to a custom path:
|
||||
- `GEMINI_WRITE_SYSTEM_MD=~/prompts/DEFAULT_SYSTEM.md gemini`
|
||||
|
||||
This creates the file and writes the current built‑in system prompt to it.
|
||||
|
||||
## Best practices: SYSTEM.md vs GEMINI.md
|
||||
|
||||
- SYSTEM.md (firmware):
|
||||
- Non‑negotiable operational rules: safety, tool‑use protocols, approvals, and
|
||||
mechanics that keep the CLI reliable.
|
||||
- Stable across tasks and projects (or per project when needed).
|
||||
- GEMINI.md (strategy):
|
||||
- Persona, goals, methodologies, and project/domain context.
|
||||
- Evolves per task; relies on SYSTEM.md for safe execution.
|
||||
|
||||
Keep SYSTEM.md minimal but complete for safety and tool operation. Keep
|
||||
GEMINI.md focused on high‑level guidance and project specifics.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Error: `missing system prompt file '…'`
|
||||
- Ensure the referenced path exists and is readable.
|
||||
- For `GEMINI_SYSTEM_MD=1|true`, create `./.gemini/system.md` in your project.
|
||||
- Override not taking effect
|
||||
- Confirm the variable is loaded (use `.gemini/.env` or export in your shell).
|
||||
- Paths are resolved from the current working directory; try an absolute path.
|
||||
- Restore defaults
|
||||
- Unset `GEMINI_SYSTEM_MD` or set it to `0`/`false`.
|
||||
@@ -367,6 +367,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-3-pro-preview"
|
||||
}
|
||||
},
|
||||
"gemini-3-flash-preview": {
|
||||
"extends": "chat-base-3",
|
||||
"modelConfig": {
|
||||
"model": "gemini-3-flash-preview"
|
||||
}
|
||||
},
|
||||
"gemini-2.5-pro": {
|
||||
"extends": "chat-base-2.5",
|
||||
"modelConfig": {
|
||||
@@ -496,6 +502,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-3-pro-preview"
|
||||
}
|
||||
},
|
||||
"chat-compression-3-flash": {
|
||||
"modelConfig": {
|
||||
"model": "gemini-3-flash-preview"
|
||||
}
|
||||
},
|
||||
"chat-compression-2.5-pro": {
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-pro"
|
||||
@@ -720,6 +731,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.enablePermanentToolApproval`** (boolean):
|
||||
- **Description:** Enable the "Allow for all future sessions" option in tool
|
||||
confirmation dialogs.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`security.blockGitExtensions`** (boolean):
|
||||
- **Description:** Blocks installing and loading extensions from Git.
|
||||
- **Default:** `false`
|
||||
@@ -773,7 +789,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `experimental`
|
||||
|
||||
- **`experimental.enableAgents`** (boolean):
|
||||
- **Description:** Enable local and remote subagents.
|
||||
- **Description:** Enable local and remote subagents. Warning: Experimental
|
||||
feature, uses YOLO mode for subagents
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -787,11 +804,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.isModelAvailabilityServiceEnabled`** (boolean):
|
||||
- **Description:** Enable model routing using new availability service.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.jitContext`** (boolean):
|
||||
- **Description:** Enable Just-In-Time (JIT) context loading.
|
||||
- **Default:** `false`
|
||||
@@ -821,7 +833,12 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`experimental.codebaseInvestigatorSettings.model`** (string):
|
||||
- **Description:** The model to use for the Codebase Investigator agent.
|
||||
- **Default:** `"pro"`
|
||||
- **Default:** `"auto"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.introspectionAgentSettings.enabled`** (boolean):
|
||||
- **Description:** Enable the Introspection Agent.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `hooks`
|
||||
@@ -1117,6 +1134,18 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- **`GEMINI_SANDBOX`**:
|
||||
- Alternative to the `sandbox` setting in `settings.json`.
|
||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||
- **`GEMINI_SYSTEM_MD`**:
|
||||
- Replaces the built‑in system prompt with content from a Markdown file.
|
||||
- `true`/`1`: Use project default path `./.gemini/system.md`.
|
||||
- Any other string: Treat as a path (relative/absolute supported, `~`
|
||||
expands).
|
||||
- `false`/`0` or unset: Use the built‑in prompt. See
|
||||
[System Prompt Override](../cli/system-prompt.md).
|
||||
- **`GEMINI_WRITE_SYSTEM_MD`**:
|
||||
- Writes the current built‑in system prompt to a file for review.
|
||||
- `true`/`1`: Write to `./.gemini/system.md`. Otherwise treat the value as a
|
||||
path.
|
||||
- Run the CLI once with this set to generate the file.
|
||||
- **`SEATBELT_PROFILE`** (macOS specific):
|
||||
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
||||
- `permissive-open`: (Default) Restricts writes to the project folder (and a
|
||||
|
||||
@@ -24,7 +24,7 @@ Rename the photos in my "photos" directory based on their contents.
|
||||
|
||||
Result: Gemini will ask for permission to rename your files.
|
||||
|
||||
Select **Yes, allow once.** and your files will be renamed:
|
||||
Select **Allow once** and your files will be renamed:
|
||||
|
||||
```bash
|
||||
photos/yellow_flowers.png
|
||||
|
||||
@@ -1,56 +1,37 @@
|
||||
# Gemini 3 Pro on Gemini CLI
|
||||
# Gemini 3 Pro and Gemini 3 Flash on Gemini CLI
|
||||
|
||||
We’re excited to bring Gemini 3 Pro to Gemini CLI. Gemini 3 Pro is **currently
|
||||
available** on Gemini CLI to all of the following subscribers:
|
||||
Gemini 3 Pro and Gemini 3 Flash are now available on Gemini CLI! Currently, most
|
||||
paid customers of Gemini CLI will have access to both Gemini 3 Pro and Gemini 3
|
||||
Flash, including the following subscribers:
|
||||
|
||||
- Google AI Ultra (except Google AI Ultra for Business).
|
||||
- Google AI Pro.
|
||||
- Gemini Code Assist Standard (requires
|
||||
- Google AI Pro and Google AI Ultra (excluding business customers).
|
||||
- Gemini Code Assist Standard and Enterprise (requires
|
||||
[administrative enablement](#administrator-instructions)).
|
||||
- Gemini Code Assist Enterprise (requires
|
||||
[administrative enablement](#administrator-instructions)).
|
||||
- Paid Gemini API key holders.
|
||||
- Paid Vertex API key holders.
|
||||
- Paid Gemini API and Vertex API key holders.
|
||||
|
||||
For **everyone else**, we're gradually expanding access
|
||||
[through a waitlist](https://goo.gle/geminicli-waitlist-signup). If you don't
|
||||
have one of the listed subscriptions, sign up for the waitlist to access Gemini
|
||||
3 Pro once approved.
|
||||
For free tier users:
|
||||
|
||||
**Note:** Whether you’re automatically granted access or accepted from the
|
||||
waitlist, you’ll still need to enable Gemini 3 Pro
|
||||
[using the `/settings` command](../cli/settings.md).
|
||||
- If you signed up for the waitlist, please check your email for details. We’ve
|
||||
onboarded everyone who signed up to the previously available waitlist.
|
||||
- If you were not on our waitlist, we’re rolling out additional access gradually
|
||||
to ensure the experience remains fast and reliable. Stay tuned for more
|
||||
details.
|
||||
|
||||
## How to join the waitlist
|
||||
## How to get started with Gemini 3 on Gemini CLI
|
||||
|
||||
Users not automatically granted access will need to join the waitlist. Follow
|
||||
these instructions to sign up:
|
||||
Get started by upgrading Gemini CLI to the latest version (0.21.1):
|
||||
|
||||
- Install Gemini CLI.
|
||||
- Authenticate using the **Login with Google** option. You’ll see a banner that
|
||||
says “Gemini 3 is now available.” If you do not see this banner, update your
|
||||
installation of Gemini CLI to the most recent version.
|
||||
- Fill out this Google form:
|
||||
[Access Gemini 3 in Gemini CLI](https://goo.gle/geminicli-waitlist-signup).
|
||||
Provide the email address of the account you used to authenticate with Gemini
|
||||
CLI.
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@latest
|
||||
```
|
||||
|
||||
Users will be onboarded in batches, subject to availability. When you’ve been
|
||||
granted access to Gemini 3 Pro, you’ll receive an acceptance email to your
|
||||
submitted email address.
|
||||
After you’ve confirmed your version is 0.21.1 or later:
|
||||
|
||||
**Note:** Please wait until you have been approved to use Gemini 3 Pro to enable
|
||||
**Preview Features**. If enabled early, the CLI will fallback to Gemini 2.5 Pro.
|
||||
1. Use the `/settings` command in Gemini CLI.
|
||||
2. Toggle **Preview Features** to `true`.
|
||||
3. Run `/model` and select **Auto (Gemini 3)**.
|
||||
|
||||
## How to use Gemini 3 Pro with Gemini CLI
|
||||
|
||||
Once you receive your acceptance email–or if you are automatically granted
|
||||
access–you still need to enable Gemini 3 Pro within Gemini CLI.
|
||||
|
||||
To enable Gemini 3 Pro, use the `/settings` command in Gemini CLI and set
|
||||
**Preview Features** to `true`.
|
||||
|
||||
For more information, see [Gemini CLI Settings](../cli/settings.md).
|
||||
For more information, see [Gemini CLI model selection](../cli/model.md).
|
||||
|
||||
### Usage limits and fallback
|
||||
|
||||
@@ -68,10 +49,10 @@ There may be times when the Gemini 3 Pro model is overloaded. When that happens,
|
||||
Gemini CLI will ask you to decide whether you want to keep trying Gemini 3 Pro
|
||||
or fallback to Gemini 2.5 Pro.
|
||||
|
||||
**Note:** The **Keep trying** option uses exponential backoff, in which Gemini
|
||||
CLI waits longer between each retry, when the system is busy. If the retry
|
||||
doesn't happen immediately, please wait a few minutes for the request to
|
||||
process.
|
||||
> **Note:** The **Keep trying** option uses exponential backoff, in which Gemini
|
||||
> CLI waits longer between each retry, when the system is busy. If the retry
|
||||
> doesn't happen immediately, please wait a few minutes for the request to
|
||||
> process.
|
||||
|
||||
### Model selection and routing types
|
||||
|
||||
@@ -92,7 +73,7 @@ manage your usage limits:
|
||||
To learn more about selecting a model and routing, refer to
|
||||
[Gemini CLI Model Selection](../cli/model.md).
|
||||
|
||||
## How to enable Gemini 3 Pro with Gemini CLI on Gemini Code Assist
|
||||
## How to enable Gemini 3 with Gemini CLI on Gemini Code Assist
|
||||
|
||||
If you're using Gemini Code Assist Standard or Gemini Code Assist Enterprise,
|
||||
enabling Gemini 3 Pro on Gemini CLI requires configuring your release channels.
|
||||
@@ -123,7 +104,7 @@ then:
|
||||
- Use the `/settings` command.
|
||||
- Set **Preview Features** to `true`.
|
||||
|
||||
Restart Gemini CLI and you should have access to Gemini 3 Pro.
|
||||
Restart Gemini CLI and you should have access to Gemini 3.
|
||||
|
||||
## Need help?
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ all tools with `*`, specify only the tools you need:
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "validate-writes",
|
||||
@@ -298,7 +298,7 @@ cat > test-input.json << 'EOF'
|
||||
"session_id": "test-123",
|
||||
"cwd": "/tmp/test",
|
||||
"hook_event_name": "BeforeTool",
|
||||
"tool_name": "WriteFile",
|
||||
"tool_name": "write_file",
|
||||
"tool_input": {
|
||||
"file_path": "test.txt",
|
||||
"content": "Test content"
|
||||
@@ -437,7 +437,7 @@ Add descriptions to help others understand your hooks:
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "secret-scanner",
|
||||
@@ -484,7 +484,7 @@ Verify the hook appears in the list and is enabled.
|
||||
|
||||
```bash
|
||||
# Test regex pattern
|
||||
echo "WriteFile" | grep -E "Write.*|Edit"
|
||||
echo "write_file|replace" | grep -E "write_.*|replace"
|
||||
```
|
||||
|
||||
**Check disabled list:**
|
||||
@@ -559,7 +559,7 @@ async function getCachedData(key) {
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile",
|
||||
"matcher": "write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "quick-check",
|
||||
@@ -569,7 +569,7 @@ async function getCachedData(key) {
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "WriteFile",
|
||||
"matcher": "write_file",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "deep-check",
|
||||
|
||||
+128
-23
@@ -7,6 +7,9 @@ the CLI's source code.
|
||||
See [writing hooks guide](writing-hooks.md) for a tutorial on creating your
|
||||
first hook and a comprehensive example.
|
||||
|
||||
See [hooks reference](reference.md) for the technical specification of the I/O
|
||||
schemas.
|
||||
|
||||
See [best practices](best-practices.md) for guidelines on security, performance,
|
||||
and debugging.
|
||||
|
||||
@@ -70,7 +73,7 @@ trigger the hook:
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
/* hooks for write operations */
|
||||
]
|
||||
@@ -82,8 +85,8 @@ trigger the hook:
|
||||
|
||||
**Matcher patterns:**
|
||||
|
||||
- **Exact match:** `"ReadFile"` matches only `ReadFile`
|
||||
- **Regex:** `"Write.*|Edit"` matches `WriteFile`, `WriteBinary`, `Edit`
|
||||
- **Exact match:** `"read_file"` matches only `read_file`
|
||||
- **Regex:** `"write_.*|replace"` matches `write_file`, `replace`
|
||||
- **Wildcard:** `"*"` or `""` matches all tools
|
||||
|
||||
**Session event matchers:**
|
||||
@@ -115,6 +118,7 @@ Every hook receives these base fields:
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/path/to/transcript.jsonl",
|
||||
"cwd": "/path/to/project",
|
||||
"hook_event_name": "BeforeTool",
|
||||
"timestamp": "2025-12-01T10:30:00Z"
|
||||
@@ -130,7 +134,7 @@ Every hook receives these base fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_name": "WriteFile",
|
||||
"tool_name": "write_file",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.ts",
|
||||
"content": "..."
|
||||
@@ -159,7 +163,7 @@ Or simple exit codes:
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_name": "ReadFile",
|
||||
"tool_name": "read_file",
|
||||
"tool_input": { "file_path": "..." },
|
||||
"tool_response": "file contents..."
|
||||
}
|
||||
@@ -212,7 +216,7 @@ Or simple exit codes:
|
||||
"toolConfig": {
|
||||
"functionCallingConfig": {
|
||||
"mode": "AUTO",
|
||||
"allowedFunctionNames": ["ReadFile", "WriteFile"]
|
||||
"allowedFunctionNames": ["read_file", "write_file"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +320,7 @@ Or simple exit codes:
|
||||
"toolConfig": {
|
||||
"functionCallingConfig": {
|
||||
"mode": "ANY",
|
||||
"allowedFunctionNames": ["ReadFile", "WriteFile", "Edit"]
|
||||
"allowedFunctionNames": ["read_file", "write_file", "replace"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +330,7 @@ Or simple exit codes:
|
||||
Or simple output (comma-separated tool names sets mode to ANY):
|
||||
|
||||
```bash
|
||||
echo "ReadFile,WriteFile,Edit"
|
||||
echo "read_file,write_file,replace"
|
||||
```
|
||||
|
||||
#### SessionStart
|
||||
@@ -410,13 +414,22 @@ precedence rules.
|
||||
|
||||
### Configuration layers
|
||||
|
||||
Hook configurations are applied in the following order of precedence (higher
|
||||
numbers override lower numbers):
|
||||
Hook configurations are applied in the following order of execution (lower
|
||||
numbers run first):
|
||||
|
||||
1. **System defaults:** Built-in default settings (lowest precedence)
|
||||
2. **User settings:** `~/.gemini/settings.json`
|
||||
3. **Project settings:** `.gemini/settings.json` in your project directory
|
||||
4. **System settings:** `/etc/gemini-cli/settings.json` (highest precedence)
|
||||
1. **Project settings:** `.gemini/settings.json` in your project directory
|
||||
(highest priority)
|
||||
2. **User settings:** `~/.gemini/settings.json`
|
||||
3. **System settings:** `/etc/gemini-cli/settings.json`
|
||||
4. **Extensions:** Internal hooks defined by installed extensions (lowest
|
||||
priority)
|
||||
|
||||
#### Deduplication and shadowing
|
||||
|
||||
If multiple hooks with the identical **name** and **command** are discovered
|
||||
across different configuration layers, Gemini CLI deduplicates them. The hook
|
||||
from the higher-priority layer (e.g., Project) will be kept, and others will be
|
||||
ignored.
|
||||
|
||||
Within each level, hooks run in the order they are declared in the
|
||||
configuration.
|
||||
@@ -446,8 +459,9 @@ configuration.
|
||||
|
||||
**Configuration properties:**
|
||||
|
||||
- **`name`** (string, required): Unique identifier for the hook used in
|
||||
`/hooks enable/disable` commands
|
||||
- **`name`** (string, recommended): Unique identifier for the hook used in
|
||||
`/hooks enable/disable` commands. If omitted, the `command` path is used as
|
||||
the identifier.
|
||||
- **`type`** (string, required): Hook type - currently only `"command"` is
|
||||
supported
|
||||
- **`command`** (string, required): Path to the script or command to execute
|
||||
@@ -494,6 +508,8 @@ You can temporarily enable or disable individual hooks using commands:
|
||||
|
||||
These commands allow you to control hook execution without editing configuration
|
||||
files. The hook name should match the `name` field in your hook configuration.
|
||||
Changes made via these commands are persisted to your global User settings
|
||||
(`~/.gemini/settings.json`).
|
||||
|
||||
### Disabled hooks configuration
|
||||
|
||||
@@ -524,7 +540,7 @@ This command:
|
||||
|
||||
- Reads `.claude/settings.json`
|
||||
- Converts event names (`PreToolUse` → `BeforeTool`, etc.)
|
||||
- Translates tool names (`Bash` → `RunShellCommand`, `Edit` → `Edit`)
|
||||
- Translates tool names (`Bash` → `run_shell_command`, `replace` → `replace`)
|
||||
- Updates matcher patterns
|
||||
- Writes to `.gemini/settings.json`
|
||||
|
||||
@@ -543,12 +559,101 @@ This command:
|
||||
|
||||
### Tool name mapping
|
||||
|
||||
| Claude Code | Gemini CLI |
|
||||
| ----------- | ----------------- |
|
||||
| `Bash` | `RunShellCommand` |
|
||||
| `Edit` | `Edit` |
|
||||
| `Read` | `ReadFile` |
|
||||
| `Write` | `WriteFile` |
|
||||
| Claude Code | Gemini CLI |
|
||||
| ----------- | --------------------- |
|
||||
| `Bash` | `run_shell_command` |
|
||||
| `Edit` | `replace` |
|
||||
| `Read` | `read_file` |
|
||||
| `Write` | `write_file` |
|
||||
| `Glob` | `glob` |
|
||||
| `Grep` | `search_file_content` |
|
||||
| `LS` | `list_directory` |
|
||||
|
||||
## Tool and Event Matchers Reference
|
||||
|
||||
### Available tool names for matchers
|
||||
|
||||
The following built-in tools can be used in `BeforeTool` and `AfterTool` hook
|
||||
matchers:
|
||||
|
||||
#### File operations
|
||||
|
||||
- `read_file` - Read a single file
|
||||
- `read_many_files` - Read multiple files at once
|
||||
- `write_file` - Create or overwrite a file
|
||||
- `replace` - Edit file content with find/replace
|
||||
|
||||
#### File system
|
||||
|
||||
- `list_directory` - List directory contents
|
||||
- `glob` - Find files matching a pattern
|
||||
- `search_file_content` - Search within file contents
|
||||
|
||||
#### Execution
|
||||
|
||||
- `run_shell_command` - Execute shell commands
|
||||
|
||||
#### Web and external
|
||||
|
||||
- `google_web_search` - Google Search with grounding
|
||||
- `web_fetch` - Fetch web page content
|
||||
|
||||
#### Agent features
|
||||
|
||||
- `write_todos` - Manage TODO items
|
||||
- `save_memory` - Save information to memory
|
||||
- `delegate_to_agent` - Delegate tasks to sub-agents
|
||||
|
||||
#### Example matchers
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "write_file|replace" // File editing tools
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "read_.*" // All read operations
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "run_shell_command" // Only shell commands
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "*" // All tools
|
||||
}
|
||||
```
|
||||
|
||||
### Event-specific matchers
|
||||
|
||||
#### SessionStart event matchers
|
||||
|
||||
- `startup` - Fresh session start
|
||||
- `resume` - Resuming a previous session
|
||||
- `clear` - Session cleared
|
||||
|
||||
#### SessionEnd event matchers
|
||||
|
||||
- `exit` - Normal exit
|
||||
- `clear` - Session cleared
|
||||
- `logout` - User logged out
|
||||
- `prompt_input_exit` - Exit from prompt input
|
||||
- `other` - Other reasons
|
||||
|
||||
#### PreCompress event matchers
|
||||
|
||||
- `manual` - Manually triggered compression
|
||||
- `auto` - Automatically triggered compression
|
||||
|
||||
#### Notification event matchers
|
||||
|
||||
- `ToolPermission` - Tool permission notifications
|
||||
|
||||
## Learn more
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# Hooks Reference
|
||||
|
||||
This document provides the technical specification for Gemini CLI hooks,
|
||||
including the JSON schemas for input and output, exit code behaviors, and the
|
||||
stable model API.
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
Hooks communicate with Gemini CLI via standard streams and exit codes:
|
||||
|
||||
- **Input**: Gemini CLI sends a JSON object to the hook's `stdin`.
|
||||
- **Output**: The hook sends a JSON object (or plain text) to `stdout`.
|
||||
- **Exit Codes**: Used to signal success or blocking errors.
|
||||
|
||||
### Exit Code Behavior
|
||||
|
||||
| Exit Code | Meaning | Behavior |
|
||||
| :-------- | :----------------- | :---------------------------------------------------------------------------------------------- |
|
||||
| `0` | **Success** | `stdout` is parsed as JSON. If parsing fails, it's treated as a `systemMessage`. |
|
||||
| `2` | **Blocking Error** | Interrupts the current operation. `stderr` is shown to the agent (for tool events) or the user. |
|
||||
| Other | **Warning** | Execution continues. `stderr` is logged as a non-blocking warning. |
|
||||
|
||||
---
|
||||
|
||||
## Input Schema (`stdin`)
|
||||
|
||||
Every hook receives a base JSON object. Extra fields are added depending on the
|
||||
specific event.
|
||||
|
||||
### Base Fields (All Events)
|
||||
|
||||
| Field | Type | Description |
|
||||
| :---------------- | :------- | :---------------------------------------------------- |
|
||||
| `session_id` | `string` | Unique identifier for the current CLI session. |
|
||||
| `transcript_path` | `string` | Path to the session's JSON transcript (if available). |
|
||||
| `cwd` | `string` | The current working directory. |
|
||||
| `hook_event_name` | `string` | The name of the firing event (e.g., `BeforeTool`). |
|
||||
| `timestamp` | `string` | ISO 8601 timestamp of the event. |
|
||||
|
||||
### Event-Specific Fields
|
||||
|
||||
#### Tool Events (`BeforeTool`, `AfterTool`)
|
||||
|
||||
- `tool_name`: (`string`) The internal name of the tool (e.g., `write_file`,
|
||||
`run_shell_command`).
|
||||
- `tool_input`: (`object`) The arguments passed to the tool.
|
||||
- `tool_response`: (`object`, **AfterTool only**) The raw output from the tool
|
||||
execution.
|
||||
|
||||
#### Agent Events (`BeforeAgent`, `AfterAgent`)
|
||||
|
||||
- `prompt`: (`string`) The user's submitted prompt.
|
||||
- `prompt_response`: (`string`, **AfterAgent only**) The final response text
|
||||
from the model.
|
||||
- `stop_hook_active`: (`boolean`, **AfterAgent only**) Indicates if a stop hook
|
||||
is already handling a continuation.
|
||||
|
||||
#### Model Events (`BeforeModel`, `AfterModel`, `BeforeToolSelection`)
|
||||
|
||||
- `llm_request`: (`LLMRequest`) A stable representation of the outgoing request.
|
||||
See [Stable Model API](#stable-model-api).
|
||||
- `llm_response`: (`LLMResponse`, **AfterModel only**) A stable representation
|
||||
of the incoming response.
|
||||
|
||||
#### Session & Notification Events
|
||||
|
||||
- `source`: (`startup` | `resume` | `clear`, **SessionStart only**) The trigger
|
||||
source.
|
||||
- `reason`: (`exit` | `clear` | `logout` | `prompt_input_exit` | `other`,
|
||||
**SessionEnd only**) The reason for session end.
|
||||
- `trigger`: (`manual` | `auto`, **PreCompress only**) What triggered the
|
||||
compression event.
|
||||
- `notification_type`: (`ToolPermission`, **Notification only**) The type of
|
||||
notification being fired.
|
||||
- `message`: (`string`, **Notification only**) The notification message.
|
||||
- `details`: (`object`, **Notification only**) Payload-specific details for the
|
||||
notification.
|
||||
|
||||
---
|
||||
|
||||
## Output Schema (`stdout`)
|
||||
|
||||
If the hook exits with `0`, the CLI attempts to parse `stdout` as JSON.
|
||||
|
||||
### Common Output Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------------------- | :-------- | :----------------------------------------------------------------------- |
|
||||
| `decision` | `string` | One of: `allow`, `deny`, `block`, `ask`, `approve`. |
|
||||
| `reason` | `string` | Explanation shown to the **agent** when a decision is `deny` or `block`. |
|
||||
| `systemMessage` | `string` | Message displayed to the **user** in the CLI terminal. |
|
||||
| `continue` | `boolean` | If `false`, immediately terminates the agent loop for this turn. |
|
||||
| `stopReason` | `string` | Message shown to the user when `continue` is `false`. |
|
||||
| `suppressOutput` | `boolean` | If `true`, the hook execution is hidden from the CLI transcript. |
|
||||
| `hookSpecificOutput` | `object` | Container for event-specific data (see below). |
|
||||
|
||||
### `hookSpecificOutput` Reference
|
||||
|
||||
| Field | Supported Events | Description |
|
||||
| :------------------ | :----------------------------------------- | :-------------------------------------------------------------------------------- |
|
||||
| `additionalContext` | `SessionStart`, `BeforeAgent`, `AfterTool` | Appends text directly to the agent's context. |
|
||||
| `llm_request` | `BeforeModel` | A `Partial<LLMRequest>` to override parameters of the outgoing call. |
|
||||
| `llm_response` | `BeforeModel` | A **full** `LLMResponse` to bypass the model and provide a synthetic result. |
|
||||
| `llm_response` | `AfterModel` | A `Partial<LLMResponse>` to modify the model's response before the agent sees it. |
|
||||
| `toolConfig` | `BeforeToolSelection` | Object containing `mode` (`AUTO`/`ANY`/`NONE`) and `allowedFunctionNames`. |
|
||||
|
||||
---
|
||||
|
||||
## Stable Model API
|
||||
|
||||
Gemini CLI uses a decoupled format for model interactions to ensure hooks remain
|
||||
stable even if the underlying Gemini SDK changes.
|
||||
|
||||
### `LLMRequest` Object
|
||||
|
||||
Used in `BeforeModel` and `BeforeToolSelection`.
|
||||
|
||||
> 💡 **Note**: In v1, model hooks are primarily text-focused. Non-text parts
|
||||
> (like images or function calls) provided in the `content` array will be
|
||||
> simplified to their string representation by the translator.
|
||||
|
||||
```typescript
|
||||
{
|
||||
"model": string,
|
||||
"messages": Array<{
|
||||
"role": "user" | "model" | "system",
|
||||
"content": string | Array<{ "type": string, [key: string]: any }>
|
||||
}>,
|
||||
"config"?: {
|
||||
"temperature"?: number,
|
||||
"maxOutputTokens"?: number,
|
||||
"topP"?: number,
|
||||
"topK"?: number
|
||||
},
|
||||
"toolConfig"?: {
|
||||
"mode"?: "AUTO" | "ANY" | "NONE",
|
||||
"allowedFunctionNames"?: string[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `LLMResponse` Object
|
||||
|
||||
Used in `AfterModel` and as a synthetic response in `BeforeModel`.
|
||||
|
||||
```typescript
|
||||
{
|
||||
"text"?: string,
|
||||
"candidates": Array<{
|
||||
"content": {
|
||||
"role": "model",
|
||||
"parts": string[]
|
||||
},
|
||||
"finishReason"?: "STOP" | "MAX_TOKENS" | "SAFETY" | "RECITATION" | "OTHER",
|
||||
"index"?: number,
|
||||
"safetyRatings"?: Array<{
|
||||
"category": string,
|
||||
"probability": string,
|
||||
"blocked"?: boolean
|
||||
}>
|
||||
}>,
|
||||
"usageMetadata"?: {
|
||||
"promptTokenCount"?: number,
|
||||
"candidatesTokenCount"?: number,
|
||||
"totalTokenCount"?: number
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -72,9 +72,9 @@ Run Gemini CLI and execute any command that uses tools:
|
||||
```
|
||||
> Read the README.md file
|
||||
|
||||
[Agent uses ReadFile tool]
|
||||
[Agent uses read_file tool]
|
||||
|
||||
Logged: ReadFile
|
||||
Logged: read_file
|
||||
```
|
||||
|
||||
Check `.gemini/tool-log.txt` to see the logged tool executions.
|
||||
@@ -110,7 +110,7 @@ exit 0
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "secret-scanner",
|
||||
@@ -167,7 +167,7 @@ exit 0
|
||||
"hooks": {
|
||||
"AfterTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "auto-test",
|
||||
@@ -379,7 +379,7 @@ chmod +x .gemini/hooks/*.js
|
||||
],
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "security-check",
|
||||
@@ -392,7 +392,7 @@ chmod +x .gemini/hooks/*.js
|
||||
],
|
||||
"AfterTool": [
|
||||
{
|
||||
"matcher": "WriteFile|Edit",
|
||||
"matcher": "write_file|replace",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "auto-test",
|
||||
@@ -601,7 +601,7 @@ async function main() {
|
||||
.map((k) => k.trim());
|
||||
|
||||
// Simple keyword-based filtering + core tools
|
||||
const coreTools = ['ReadFile', 'WriteFile', 'Edit', 'RunShellCommand'];
|
||||
const coreTools = ['read_file', 'write_file', 'replace', 'run_shell_command'];
|
||||
const filtered = candidateTools.filter((tool) => {
|
||||
if (coreTools.includes(tool)) return true;
|
||||
const toolLower = tool.toLowerCase();
|
||||
|
||||
@@ -128,8 +128,8 @@ editor.
|
||||
You can also **modify the suggested changes** directly in the diff view before
|
||||
accepting them.
|
||||
|
||||
If you select ‘Yes, allow always’ in the CLI, changes will no longer show up in
|
||||
the IDE as they will be auto-accepted.
|
||||
If you select ‘Allow for this session’ in the CLI, changes will no longer show
|
||||
up in the IDE as they will be auto-accepted.
|
||||
|
||||
## Using with sandboxing
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ This documentation is organized into the following sections:
|
||||
- **[Memport](./core/memport.md):** Using the Memory Import Processor.
|
||||
- **[Tools API](./core/tools-api.md):** Information on how the core manages and
|
||||
exposes tools.
|
||||
- **[System Prompt Override](./cli/system-prompt.md):** Replace built-in system
|
||||
instructions using `GEMINI_SYSTEM_MD`.
|
||||
|
||||
- **[Policy Engine](./core/policy-engine.md):** Use the Policy Engine for
|
||||
fine-grained control over tool execution.
|
||||
|
||||
|
||||
+9
-1
@@ -24,7 +24,7 @@
|
||||
"slug": "docs/get-started"
|
||||
},
|
||||
{
|
||||
"label": "Gemini 3 Pro on Gemini CLI",
|
||||
"label": "Gemini 3 on Gemini CLI",
|
||||
"slug": "docs/get-started/gemini-3"
|
||||
},
|
||||
{
|
||||
@@ -115,6 +115,10 @@
|
||||
{
|
||||
"label": "Uninstall",
|
||||
"slug": "docs/cli/uninstall"
|
||||
},
|
||||
{
|
||||
"label": "System prompt override",
|
||||
"slug": "docs/cli/system-prompt"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -204,6 +208,10 @@
|
||||
"label": "Writing hooks",
|
||||
"slug": "docs/hooks/writing-hooks"
|
||||
},
|
||||
{
|
||||
"label": "Hooks reference",
|
||||
"slug": "docs/hooks/reference"
|
||||
},
|
||||
{
|
||||
"label": "Best practices",
|
||||
"slug": "docs/hooks/best-practices"
|
||||
|
||||
+18
-10
@@ -12,7 +12,7 @@ import prettierConfig from 'eslint-config-prettier';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import vitest from '@vitest/eslint-plugin';
|
||||
import globals from 'globals';
|
||||
import licenseHeader from 'eslint-plugin-license-header';
|
||||
import headers from 'eslint-plugin-headers';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
@@ -165,6 +165,7 @@ export default tseslint.config(
|
||||
'prefer-const': ['error', { destructuring: 'all' }],
|
||||
radix: 'error',
|
||||
'default-case': 'error',
|
||||
'@typescript-eslint/await-thenable': ['error'],
|
||||
'@typescript-eslint/no-floating-promises': ['error'],
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': ['error'],
|
||||
},
|
||||
@@ -208,19 +209,26 @@ export default tseslint.config(
|
||||
{
|
||||
files: ['./**/*.{tsx,ts,js}'],
|
||||
plugins: {
|
||||
'license-header': licenseHeader,
|
||||
headers,
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
'license-header/header': [
|
||||
'headers/header-format': [
|
||||
'error',
|
||||
[
|
||||
'/**',
|
||||
' * @license',
|
||||
' * Copyright 2025 Google LLC',
|
||||
' * SPDX-License-Identifier: Apache-2.0',
|
||||
' */',
|
||||
],
|
||||
{
|
||||
source: 'string',
|
||||
content: [
|
||||
'@license',
|
||||
'Copyright (year) Google LLC',
|
||||
'SPDX-License-Identifier: Apache-2.0',
|
||||
].join('\n'),
|
||||
patterns: {
|
||||
year: {
|
||||
pattern: '202[5-6]',
|
||||
defaultValue: '2026',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'import/enforce-node-protocol-usage': ['error', 'always'],
|
||||
},
|
||||
|
||||
@@ -27,6 +27,10 @@ class MockConfig {
|
||||
getDebugMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getFileFilteringRespectGeminiIgnore() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ripgrep-real-direct', () => {
|
||||
|
||||
Generated
+837
-46
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.21.0-nightly.20251213.977248e09",
|
||||
"version": "0.23.0-preview.5",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.0-nightly.20251213.977248e09"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.23.0-preview.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
@@ -89,12 +89,13 @@
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"depcheck": "^1.4.7",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-wasm": "^1.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-headers": "^1.3.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-license-header": "^0.8.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"glob": "^12.0.0",
|
||||
@@ -113,6 +114,7 @@
|
||||
"react-devtools-core": "^6.1.2",
|
||||
"semver": "^7.7.2",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"ts-prune": "^0.10.3",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vitest": "^3.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.21.0-nightly.20251213.977248e09",
|
||||
"version": "0.23.0-preview.5",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.2",
|
||||
"@a2a-js/sdk": "^0.3.7",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"express": "^5.1.0",
|
||||
|
||||
@@ -123,7 +123,7 @@ export class Task {
|
||||
// process. This is not scoped to the individual task but reflects the global connection
|
||||
// state managed within the @gemini-cli/core module.
|
||||
async getMetadata(): Promise<TaskMetadata> {
|
||||
const toolRegistry = await this.config.getToolRegistry();
|
||||
const toolRegistry = this.config.getToolRegistry();
|
||||
const mcpServers = this.config.getMcpClientManager()?.getMcpServers() || {};
|
||||
const serverStatuses = getAllMCPServerStatuses();
|
||||
const servers = Object.keys(mcpServers).map((serverName) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.21.0-nightly.20251213.977248e09",
|
||||
"version": "0.23.0-preview.5",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,9 +26,10 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.0-nightly.20251213.977248e09"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.23.0-preview.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.11.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
|
||||
@@ -228,7 +228,9 @@ describe('extensions disable command', () => {
|
||||
_: [],
|
||||
$0: '',
|
||||
};
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
expect(mockExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: '/test/dir',
|
||||
|
||||
@@ -205,7 +205,9 @@ describe('extensions enable command', () => {
|
||||
_: [],
|
||||
$0: '',
|
||||
};
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockExtensionManager.prototype.enableExtension,
|
||||
|
||||
@@ -180,7 +180,9 @@ describe('extensions link command', () => {
|
||||
_: [],
|
||||
$0: '',
|
||||
};
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockExtensionManager.prototype.installOrUpdateExtension,
|
||||
|
||||
@@ -49,7 +49,7 @@ const setCommand: CommandModule<object, SetArgs> = {
|
||||
if (!extension || !extensionManager) {
|
||||
return;
|
||||
}
|
||||
const extensionConfig = extensionManager.loadExtensionConfig(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (!extensionConfig) {
|
||||
@@ -89,7 +89,7 @@ const listCommand: CommandModule<object, ListArgs> = {
|
||||
if (!extension || !extensionManager) {
|
||||
return;
|
||||
}
|
||||
const extensionConfig = extensionManager.loadExtensionConfig(
|
||||
const extensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -287,7 +287,9 @@ describe('extensions uninstall command', () => {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
const argv: TestArgv = { names: ['my-extension'], _: [], $0: '' };
|
||||
await (command.handler as unknown as (args: TestArgv) => void)(argv);
|
||||
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(mockUninstallExtension).toHaveBeenCalledWith(
|
||||
'my-extension',
|
||||
|
||||
@@ -41,7 +41,7 @@ async function validateExtension(args: ValidateArgs) {
|
||||
});
|
||||
const absoluteInputPath = path.resolve(args.path);
|
||||
const extensionConfig: ExtensionConfig =
|
||||
extensionManager.loadExtensionConfig(absoluteInputPath);
|
||||
await extensionManager.loadExtensionConfig(absoluteInputPath);
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, type Mock, type MockInstance } from 'vitest';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
type Mock,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
import yargs, { type Argv } from 'yargs';
|
||||
import { addCommand } from './add.js';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
|
||||
@@ -1283,7 +1283,7 @@ describe('loadCliConfig model selection', () => {
|
||||
argv,
|
||||
);
|
||||
|
||||
expect(config.getModel()).toBe('auto');
|
||||
expect(config.getModel()).toBe('auto-gemini-2.5');
|
||||
});
|
||||
|
||||
it('always prefers model from argv', async () => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
loadServerHierarchicalMemory,
|
||||
WEB_FETCH_TOOL_NAME,
|
||||
getVersion,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Settings } from './settings.js';
|
||||
|
||||
@@ -434,9 +435,15 @@ export async function loadCliConfig(
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const { memoryContent, fileCount, filePaths } =
|
||||
await loadServerHierarchicalMemory(
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
|
||||
let memoryContent = '';
|
||||
let fileCount = 0;
|
||||
let filePaths: string[] = [];
|
||||
|
||||
if (!experimentalJitContext) {
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
debugMode,
|
||||
@@ -447,6 +454,10 @@ export async function loadCliConfig(
|
||||
memoryFileFiltering,
|
||||
settings.context?.discoveryMaxDirs,
|
||||
);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
filePaths = result.filePaths;
|
||||
}
|
||||
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
|
||||
@@ -569,7 +580,9 @@ export async function loadCliConfig(
|
||||
extraExcludes.length > 0 ? extraExcludes : undefined,
|
||||
);
|
||||
|
||||
const defaultModel = DEFAULT_GEMINI_MODEL_AUTO;
|
||||
const defaultModel = settings.general?.previewFeatures
|
||||
? PREVIEW_GEMINI_MODEL_AUTO
|
||||
: DEFAULT_GEMINI_MODEL_AUTO;
|
||||
const resolvedModel: string =
|
||||
argv.model ||
|
||||
process.env['GEMINI_MODEL'] ||
|
||||
@@ -640,8 +653,6 @@ export async function loadCliConfig(
|
||||
extensionLoader: extensionManager,
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
enableModelAvailabilityService:
|
||||
settings.experimental?.isModelAvailabilityServiceEnabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
@@ -668,6 +679,8 @@ export async function loadCliConfig(
|
||||
enableMessageBusIntegration,
|
||||
codebaseInvestigatorSettings:
|
||||
settings.experimental?.codebaseInvestigatorSettings,
|
||||
introspectionAgentSettings:
|
||||
settings.experimental?.introspectionAgentSettings,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
|
||||
|
||||
@@ -220,7 +220,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
newExtensionConfig = this.loadExtensionConfig(localSourcePath);
|
||||
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
|
||||
|
||||
if (isUpdate && installMetadata.autoUpdate) {
|
||||
const oldSettings = new Set(
|
||||
@@ -364,7 +364,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
// to get the name and version for logging.
|
||||
if (!newExtensionConfig && localSourcePath) {
|
||||
try {
|
||||
newExtensionConfig = this.loadExtensionConfig(localSourcePath);
|
||||
newExtensionConfig = await this.loadExtensionConfig(localSourcePath);
|
||||
} catch {
|
||||
// Ignore error, this is just for logging.
|
||||
}
|
||||
@@ -491,7 +491,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
let config = this.loadExtensionConfig(effectiveExtensionPath);
|
||||
let config = await this.loadExtensionConfig(effectiveExtensionPath);
|
||||
if (
|
||||
this.getExtensions().find((extension) => extension.name === config.name)
|
||||
) {
|
||||
@@ -571,13 +571,13 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
return this.maybeStopExtension(extension);
|
||||
}
|
||||
|
||||
loadExtensionConfig(extensionDir: string): ExtensionConfig {
|
||||
async loadExtensionConfig(extensionDir: string): Promise<ExtensionConfig> {
|
||||
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
throw new Error(`Configuration file not found at ${configFilePath}`);
|
||||
}
|
||||
try {
|
||||
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const configContent = await fs.promises.readFile(configFilePath, 'utf-8');
|
||||
const rawConfig = JSON.parse(configContent) as ExtensionConfig;
|
||||
if (!rawConfig.name || !rawConfig.version) {
|
||||
throw new Error(
|
||||
@@ -797,7 +797,7 @@ function validateName(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getExtensionId(
|
||||
function getExtensionId(
|
||||
config: ExtensionConfig,
|
||||
installMetadata?: ExtensionInstallMetadata,
|
||||
): string {
|
||||
|
||||
@@ -1424,9 +1424,10 @@ This extension will run the following MCP servers:
|
||||
],
|
||||
});
|
||||
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-local-extension'),
|
||||
);
|
||||
const previousExtensionConfig =
|
||||
await extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-local-extension'),
|
||||
);
|
||||
mockPromptForSettings.mockResolvedValueOnce('new-setting-value');
|
||||
|
||||
// 3. Call installOrUpdateExtension to perform the update.
|
||||
@@ -1481,9 +1482,10 @@ This extension will run the following MCP servers:
|
||||
],
|
||||
});
|
||||
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-auto-update-ext'),
|
||||
);
|
||||
const previousExtensionConfig =
|
||||
await extensionManager.loadExtensionConfig(
|
||||
path.join(userExtensionsDir, 'my-auto-update-ext'),
|
||||
);
|
||||
|
||||
// 3. Attempt to update and assert it fails
|
||||
await expect(
|
||||
@@ -1967,7 +1969,7 @@ This extension will run the following MCP servers:
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
|
||||
await extensionManager.enableExtension('ext1', SettingScope.User);
|
||||
activeExtensions = await getActiveExtensions();
|
||||
activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(1);
|
||||
expect(activeExtensions[0].name).toBe('ext1');
|
||||
});
|
||||
@@ -1984,7 +1986,7 @@ This extension will run the following MCP servers:
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
|
||||
await extensionManager.enableExtension('ext1', SettingScope.Workspace);
|
||||
activeExtensions = await getActiveExtensions();
|
||||
activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(1);
|
||||
expect(activeExtensions[0].name).toBe('ext1');
|
||||
});
|
||||
|
||||
@@ -221,9 +221,11 @@ describe('github.ts', () => {
|
||||
});
|
||||
|
||||
it('should return NOT_UPDATABLE for non-git/non-release extensions', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
version: '1.0.0',
|
||||
} as unknown as ExtensionConfig);
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
version: '1.0.0',
|
||||
} as unknown as ExtensionConfig),
|
||||
);
|
||||
|
||||
const linkExt = {
|
||||
installMetadata: { type: 'link' },
|
||||
|
||||
@@ -175,7 +175,7 @@ export async function checkForExtensionUpdate(
|
||||
if (installMetadata?.type === 'local') {
|
||||
let latestConfig: ExtensionConfig | undefined;
|
||||
try {
|
||||
latestConfig = extensionManager.loadExtensionConfig(
|
||||
latestConfig = await extensionManager.loadExtensionConfig(
|
||||
installMetadata.source,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -145,10 +145,12 @@ describe('Extension Update Logic', () => {
|
||||
});
|
||||
|
||||
it('should successfully update extension and set state to UPDATED_NEEDS_RESTART by default', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockResolvedValue({
|
||||
@@ -183,10 +185,12 @@ describe('Extension Update Logic', () => {
|
||||
});
|
||||
|
||||
it('should set state to UPDATED if enableExtensionReloading is true', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockResolvedValue({
|
||||
@@ -212,10 +216,12 @@ describe('Extension Update Logic', () => {
|
||||
});
|
||||
|
||||
it('should rollback and set state to ERROR if installation fails', async () => {
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockRejectedValue(new Error('Install failed'));
|
||||
@@ -257,10 +263,12 @@ describe('Extension Update Logic', () => {
|
||||
['ext3', { status: ExtensionUpdateState.UPDATE_AVAILABLE }],
|
||||
]);
|
||||
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue({
|
||||
name: 'ext',
|
||||
version: '1.0.0',
|
||||
});
|
||||
vi.mocked(mockExtensionManager.loadExtensionConfig).mockReturnValue(
|
||||
Promise.resolve({
|
||||
name: 'ext',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
);
|
||||
vi.mocked(
|
||||
mockExtensionManager.installOrUpdateExtension,
|
||||
).mockResolvedValue({ ...mockExtension, version: '1.1.0' });
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function updateExtension(
|
||||
|
||||
const tempDir = await ExtensionStorage.createTmpDir();
|
||||
try {
|
||||
const previousExtensionConfig = extensionManager.loadExtensionConfig(
|
||||
const previousExtensionConfig = await extensionManager.loadExtensionConfig(
|
||||
extension.path,
|
||||
);
|
||||
let updatedExtension: GeminiCLIExtension;
|
||||
|
||||
@@ -192,7 +192,10 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
{ key: 'x', ctrl: true },
|
||||
{ sequence: '\x18', ctrl: true },
|
||||
],
|
||||
[Command.PASTE_CLIPBOARD]: [{ key: 'v', ctrl: true }],
|
||||
[Command.PASTE_CLIPBOARD]: [
|
||||
{ key: 'v', ctrl: true },
|
||||
{ key: 'v', command: true },
|
||||
],
|
||||
|
||||
// App level bindings
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
|
||||
|
||||
@@ -291,7 +291,7 @@ describe('Settings Loading and Merging', () => {
|
||||
theme: 'legacy-dark',
|
||||
vimMode: true,
|
||||
contextFileName: 'LEGACY_CONTEXT.md',
|
||||
model: 'gemini-pro',
|
||||
model: 'gemini-2.5-pro',
|
||||
mcpServers: {
|
||||
'legacy-server-1': {
|
||||
command: 'npm',
|
||||
@@ -329,7 +329,7 @@ describe('Settings Loading and Merging', () => {
|
||||
fileName: 'LEGACY_CONTEXT.md',
|
||||
},
|
||||
model: {
|
||||
name: 'gemini-pro',
|
||||
name: 'gemini-2.5-pro',
|
||||
},
|
||||
mcpServers: {
|
||||
'legacy-server-1': {
|
||||
@@ -1960,7 +1960,7 @@ describe('Settings Loading and Merging', () => {
|
||||
usageStatisticsEnabled: false,
|
||||
},
|
||||
model: {
|
||||
name: 'gemini-pro',
|
||||
name: 'gemini-2.5-pro',
|
||||
},
|
||||
context: {
|
||||
fileName: 'CONTEXT.md',
|
||||
@@ -1999,7 +1999,7 @@ describe('Settings Loading and Merging', () => {
|
||||
vimMode: true,
|
||||
theme: 'dark',
|
||||
usageStatisticsEnabled: false,
|
||||
model: 'gemini-pro',
|
||||
model: 'gemini-2.5-pro',
|
||||
contextFileName: 'CONTEXT.md',
|
||||
includeDirectories: ['/src'],
|
||||
sandbox: true,
|
||||
|
||||
@@ -352,22 +352,21 @@ describe('SettingsSchema', () => {
|
||||
expect(setting.default).toBe(false);
|
||||
expect(setting.requiresRestart).toBe(true);
|
||||
expect(setting.showInDialog).toBe(false);
|
||||
expect(setting.description).toBe('Enable local and remote subagents.');
|
||||
expect(setting.description).toBe(
|
||||
'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents',
|
||||
);
|
||||
});
|
||||
|
||||
it('should have isModelAvailabilityServiceEnabled setting in schema', () => {
|
||||
const setting =
|
||||
getSettingsSchema().experimental.properties
|
||||
.isModelAvailabilityServiceEnabled;
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting.type).toBe('boolean');
|
||||
expect(setting.category).toBe('Experimental');
|
||||
expect(setting.default).toBe(false);
|
||||
expect(setting.requiresRestart).toBe(true);
|
||||
expect(setting.showInDialog).toBe(false);
|
||||
expect(setting.description).toBe(
|
||||
'Enable model routing using new availability service.',
|
||||
);
|
||||
it('should have name and description in hook definitions', () => {
|
||||
const hookDef = SETTINGS_SCHEMA_DEFINITIONS['HookDefinitionArray'];
|
||||
expect(hookDef).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const hookItemProperties = (hookDef as any).items.properties.hooks.items
|
||||
.properties;
|
||||
expect(hookItemProperties.name).toBeDefined();
|
||||
expect(hookItemProperties.name.type).toBe('string');
|
||||
expect(hookItemProperties.description).toBeDefined();
|
||||
expect(hookItemProperties.description.type).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
DEFAULT_MODEL_CONFIGS,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||
import type { SessionRetentionSettings } from './settings.js';
|
||||
@@ -1164,6 +1164,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Disable YOLO mode, even if enabled by a flag.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enablePermanentToolApproval: {
|
||||
type: 'boolean',
|
||||
label: 'Allow Permanent Tool Approval',
|
||||
category: 'Security',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Enable the "Allow for all future sessions" option in tool confirmation dialogs.',
|
||||
showInDialog: true,
|
||||
},
|
||||
blockGitExtensions: {
|
||||
type: 'boolean',
|
||||
label: 'Blocks extensions from Git',
|
||||
@@ -1301,7 +1311,8 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable local and remote subagents.',
|
||||
description:
|
||||
'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents',
|
||||
showInDialog: false,
|
||||
},
|
||||
extensionManagement: {
|
||||
@@ -1323,15 +1334,6 @@ const SETTINGS_SCHEMA = {
|
||||
'Enables extension loading/unloading within the CLI session.',
|
||||
showInDialog: false,
|
||||
},
|
||||
isModelAvailabilityServiceEnabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Model Availability Service',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable model routing using new availability service.',
|
||||
showInDialog: false,
|
||||
},
|
||||
jitContext: {
|
||||
type: 'boolean',
|
||||
label: 'JIT Context Loading',
|
||||
@@ -1394,13 +1396,33 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Model',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: GEMINI_MODEL_ALIAS_PRO,
|
||||
default: GEMINI_MODEL_ALIAS_AUTO,
|
||||
description:
|
||||
'The model to use for the Codebase Investigator agent.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
introspectionAgentSettings: {
|
||||
type: 'object',
|
||||
label: 'Introspection Agent Settings',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Configuration for Introspection Agent.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Introspection Agent',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable the Introspection Agent.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1899,6 +1921,10 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
type: 'object',
|
||||
description: 'Individual hook configuration.',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Unique identifier for the hook.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description:
|
||||
@@ -1909,6 +1935,10 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
description:
|
||||
'Shell command to execute. Receives JSON input via stdin and returns JSON output via stdout.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'A description of the hook.',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Timeout in milliseconds for hook execution.',
|
||||
@@ -1938,9 +1968,3 @@ type InferSettings<T extends SettingsSchema> = {
|
||||
};
|
||||
|
||||
export type Settings = InferSettings<SettingsSchemaType>;
|
||||
|
||||
export interface FooterSettings {
|
||||
hideCWD?: boolean;
|
||||
hideSandboxStatus?: boolean;
|
||||
hideModelInfo?: boolean;
|
||||
}
|
||||
|
||||
@@ -424,3 +424,52 @@ describe('Trusted Folders Caching', () => {
|
||||
expect(readSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid trust levels', () => {
|
||||
const mockCwd = '/user/folder';
|
||||
const mockRules: Record<string, TrustLevel> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
resetTrustedFoldersForTesting();
|
||||
vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
|
||||
vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
|
||||
if (p === getTrustedFoldersPath()) {
|
||||
return JSON.stringify(mockRules);
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
vi.spyOn(fs, 'existsSync').mockImplementation(
|
||||
(p) => p === getTrustedFoldersPath(),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
// Clear the object
|
||||
Object.keys(mockRules).forEach((key) => delete mockRules[key]);
|
||||
});
|
||||
|
||||
it('should create a comprehensive error message for invalid trust level', () => {
|
||||
mockRules[mockCwd] = 'INVALID_TRUST_LEVEL' as TrustLevel;
|
||||
|
||||
const { errors } = loadTrustedFolders();
|
||||
const possibleValues = Object.values(TrustLevel).join(', ');
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].message).toBe(
|
||||
`Invalid trust level "INVALID_TRUST_LEVEL" for path "${mockCwd}". Possible values are: ${possibleValues}.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw a fatal error for invalid trust level', () => {
|
||||
const mockSettings: Settings = {
|
||||
security: {
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
mockRules[mockCwd] = 'INVALID_TRUST_LEVEL' as TrustLevel;
|
||||
|
||||
expect(() => isWorkspaceTrusted(mockSettings)).toThrow(FatalConfigError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,13 @@ export enum TrustLevel {
|
||||
DO_NOT_TRUST = 'DO_NOT_TRUST',
|
||||
}
|
||||
|
||||
export function isTrustLevel(value: unknown): value is TrustLevel {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
Object.values(TrustLevel).includes(value as TrustLevel)
|
||||
);
|
||||
}
|
||||
|
||||
export interface TrustRule {
|
||||
path: string;
|
||||
trustLevel: TrustLevel;
|
||||
@@ -151,7 +158,7 @@ export function loadTrustedFolders(): LoadedTrustedFolders {
|
||||
}
|
||||
|
||||
const errors: TrustedFoldersError[] = [];
|
||||
let userConfig: Record<string, TrustLevel> = {};
|
||||
const userConfig: Record<string, TrustLevel> = {};
|
||||
|
||||
const userPath = getTrustedFoldersPath();
|
||||
|
||||
@@ -171,7 +178,17 @@ export function loadTrustedFolders(): LoadedTrustedFolders {
|
||||
path: userPath,
|
||||
});
|
||||
} else {
|
||||
userConfig = parsed as Record<string, TrustLevel>;
|
||||
for (const [path, trustLevel] of Object.entries(parsed)) {
|
||||
if (isTrustLevel(trustLevel)) {
|
||||
userConfig[path] = trustLevel;
|
||||
} else {
|
||||
const possibleValues = Object.values(TrustLevel).join(', ');
|
||||
errors.push({
|
||||
message: `Invalid trust level "${trustLevel}" for path "${path}". Possible values are: ${possibleValues}.`,
|
||||
path: userPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -127,11 +127,19 @@ vi.mock('./config/settings.js', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./ui/utils/terminalCapabilityManager.js', () => ({
|
||||
terminalCapabilityManager: {
|
||||
detectCapabilities: vi.fn(),
|
||||
getTerminalBackgroundColor: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./config/config.js', () => ({
|
||||
loadCliConfig: vi.fn().mockResolvedValue({
|
||||
getSandbox: vi.fn(() => false),
|
||||
getQuestion: vi.fn(() => ''),
|
||||
isInteractive: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config),
|
||||
parseArguments: vi.fn().mockResolvedValue({}),
|
||||
isDebugMode: vi.fn(() => false),
|
||||
@@ -271,6 +279,7 @@ describe('gemini.tsx main function', () => {
|
||||
getOutputFormat: () => 'text',
|
||||
getExtensions: () => [],
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config;
|
||||
});
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
@@ -464,9 +473,9 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => {
|
||||
const { detectAndEnableKittyProtocol } = await import(
|
||||
'./ui/utils/kittyProtocolDetector.js'
|
||||
it('should call setRawMode and detectCapabilities when isInteractive is true', async () => {
|
||||
const { terminalCapabilityManager } = await import(
|
||||
'./ui/utils/terminalCapabilityManager.js'
|
||||
);
|
||||
const { loadCliConfig, parseArguments } = await import(
|
||||
'./config/config.js'
|
||||
@@ -504,6 +513,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getOutputFormat: () => 'text',
|
||||
getExtensions: () => [],
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
errors: [],
|
||||
@@ -546,7 +556,9 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
});
|
||||
|
||||
expect(setRawModeSpy).toHaveBeenCalledWith(true);
|
||||
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
|
||||
expect(terminalCapabilityManager.detectCapabilities).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -601,6 +613,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getScreenReader: () => false,
|
||||
getGeminiMdFileCount: () => 0,
|
||||
getProjectRoot: () => '/',
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(mockConfig);
|
||||
@@ -683,6 +696,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getGeminiMdFileCount: () => 0,
|
||||
getProjectRoot: () => '/',
|
||||
refreshAuth: vi.fn(),
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(mockConfig);
|
||||
@@ -762,6 +776,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getOutputFormat: () => 'text',
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
vi.spyOn(themeManager, 'setActiveTheme').mockReturnValue(false);
|
||||
@@ -844,6 +859,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getOutputFormat: () => 'text',
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
try {
|
||||
@@ -921,6 +937,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getOutputFormat: () => 'text',
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
// The mock is already set up at the top of the test
|
||||
@@ -993,6 +1010,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
getOutputFormat: () => 'text',
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
vi.mock('./utils/readStdin.js', () => ({
|
||||
@@ -1152,6 +1170,7 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
getOutputFormat: () => 'text',
|
||||
getExtensions: () => [],
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
merged: { security: { auth: {} }, ui: {} },
|
||||
@@ -1214,6 +1233,7 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
getOutputFormat: () => 'text',
|
||||
getExtensions: () => [],
|
||||
getUsageStatisticsEnabled: () => false,
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
merged: { security: { auth: {} }, ui: {} },
|
||||
@@ -1295,11 +1315,6 @@ describe('startInteractiveUI', () => {
|
||||
geminiMdFileCount: 0,
|
||||
};
|
||||
|
||||
vi.mock('./ui/utils/kittyProtocolDetector.js', () => ({
|
||||
detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve(true)),
|
||||
isKittyProtocolSupported: vi.fn(() => true),
|
||||
isKittyProtocolEnabled: vi.fn(() => true),
|
||||
}));
|
||||
vi.mock('./ui/utils/updateCheck.js', () => ({
|
||||
checkForUpdates: vi.fn(() => Promise.resolve(null)),
|
||||
}));
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
migrateDeprecatedSettings,
|
||||
SettingScope,
|
||||
} from './config/settings.js';
|
||||
import { themeManager } from './ui/themes/theme-manager.js';
|
||||
import { getStartupWarnings } from './utils/startupWarnings.js';
|
||||
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
|
||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||
@@ -73,7 +72,6 @@ import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
|
||||
import { runZedIntegration } from './zed-integration/zedIntegration.js';
|
||||
import { cleanupExpiredSessions } from './utils/sessionCleanup.js';
|
||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||
import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js';
|
||||
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
||||
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
@@ -98,6 +96,7 @@ import { requestConsentNonInteractive } from './config/extensions/consent.js';
|
||||
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
|
||||
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
|
||||
const SLOW_RENDER_MS = 200;
|
||||
@@ -360,19 +359,6 @@ export async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load custom themes from settings
|
||||
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
|
||||
|
||||
if (settings.merged.ui?.theme) {
|
||||
if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) {
|
||||
// If the theme is not found during initial load, log a warning and continue.
|
||||
// The useThemeCommand hook in AppContainer.tsx will handle opening the dialog.
|
||||
debugLogger.warn(
|
||||
`Warning: Theme "${settings.merged.ui?.theme}" not found.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// hop into sandbox if we are outside and sandboxing is enabled
|
||||
if (!process.env['SANDBOX']) {
|
||||
const memoryArgs = settings.merged.advanced?.autoConfigureMemory
|
||||
@@ -550,11 +536,10 @@ export async function main() {
|
||||
process.on('SIGINT', () => {
|
||||
process.stdin.setRawMode(wasRaw);
|
||||
});
|
||||
|
||||
// Detect and enable Kitty keyboard protocol once at startup.
|
||||
await detectAndEnableKittyProtocol();
|
||||
}
|
||||
|
||||
await setupTerminalAndTheme(config, settings);
|
||||
|
||||
setMaxSizedBoxDebugging(isDebugMode);
|
||||
const initAppHandle = startupProfiler.start('initialize_app');
|
||||
const initializationResult = await initializeApp(config, settings);
|
||||
|
||||
@@ -210,6 +210,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getOutputFormat: vi.fn(() => 'text'),
|
||||
getUsageStatisticsEnabled: vi.fn(() => false),
|
||||
setTerminalBackground: vi.fn(),
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
createWorkingStdio,
|
||||
recordToolCallInteractions,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import type { Content, Part } from '@google/genai';
|
||||
@@ -407,6 +408,8 @@ export async function runNonInteractive({
|
||||
geminiClient
|
||||
.getChat()
|
||||
.recordCompletedToolCalls(currentModel, completedToolCalls);
|
||||
|
||||
await recordToolCallInteractions(config, completedToolCalls);
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
`Error recording completed tool call information: ${error}`,
|
||||
|
||||
@@ -91,6 +91,7 @@ const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getEnableInteractiveShell: () => true,
|
||||
getPreviewFeatures: () => false,
|
||||
};
|
||||
|
||||
const configProxy = new Proxy(mockConfig, {
|
||||
@@ -133,6 +134,7 @@ const baseMockUiState = {
|
||||
mainAreaWidth: 100,
|
||||
terminalWidth: 120,
|
||||
currentModel: 'gemini-pro',
|
||||
terminalBackgroundColor: undefined,
|
||||
};
|
||||
|
||||
const mockUIActions: UIActions = {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { render } from '../test-utils/render.js';
|
||||
import { waitFor } from '../test-utils/async.js';
|
||||
import { cleanup } from 'ink-testing-library';
|
||||
import { act, useContext } from 'react';
|
||||
import { act, useContext, type ReactElement } from 'react';
|
||||
import { AppContainer } from './AppContainer.js';
|
||||
import { SettingsContext } from './contexts/SettingsContext.js';
|
||||
import {
|
||||
@@ -71,6 +71,14 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
})),
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn(),
|
||||
})),
|
||||
startupProfiler: {
|
||||
flush: vi.fn(),
|
||||
start: vi.fn(),
|
||||
end: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
@@ -344,7 +352,7 @@ describe('AppContainer State Management', () => {
|
||||
// Add other properties if AppContainer uses them
|
||||
});
|
||||
mockedUseLogger.mockReturnValue({
|
||||
getPreviousUserMessages: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
mockedUseInputHistoryStore.mockReturnValue({
|
||||
inputHistory: [],
|
||||
@@ -361,6 +369,8 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock config's getTargetDir to return consistent workspace directory
|
||||
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(mockConfig, 'getDebugMode').mockReturnValue(false);
|
||||
|
||||
mockExtensionManager = vi.mockObject({
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
@@ -403,17 +413,25 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
describe('Basic Rendering', () => {
|
||||
it('renders without crashing with minimal props', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('renders with startup warnings', async () => {
|
||||
const startupWarnings = ['Warning 1', 'Warning 2'];
|
||||
|
||||
const { unmount } = renderAppContainer({ startupWarnings });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ startupWarnings });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -424,11 +442,15 @@ describe('AppContainer State Management', () => {
|
||||
themeError: 'Failed to load theme',
|
||||
};
|
||||
|
||||
const { unmount } = renderAppContainer({
|
||||
initResult: initResultWithError,
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({
|
||||
initResult: initResultWithError,
|
||||
});
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('handles debug mode state', () => {
|
||||
@@ -443,29 +465,45 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
describe('Context Providers', () => {
|
||||
it('provides AppContext with correct values', async () => {
|
||||
const { unmount } = renderAppContainer({ version: '2.0.0' });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ version: '2.0.0' });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Should render and unmount cleanly
|
||||
expect(() => unmount()).not.toThrow();
|
||||
expect(() => unmount!()).not.toThrow();
|
||||
});
|
||||
|
||||
it('provides UIStateContext with state management', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('provides UIActionsContext with action handlers', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('provides ConfigContext with config object', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,9 +518,13 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const { unmount } = renderAppContainer({ settings: settingsAllHidden });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ settings: settingsAllHidden });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('handles settings with memory usage enabled', async () => {
|
||||
@@ -495,9 +537,13 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const { unmount } = renderAppContainer({ settings: settingsWithMemory });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ settings: settingsWithMemory });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -505,9 +551,13 @@ describe('AppContainer State Management', () => {
|
||||
it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])(
|
||||
'handles version format: %s',
|
||||
async (version) => {
|
||||
const { unmount } = renderAppContainer({ version });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ version });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -529,9 +579,13 @@ describe('AppContainer State Management', () => {
|
||||
merged: {},
|
||||
} as LoadedSettings;
|
||||
|
||||
const { unmount } = renderAppContainer({ settings: undefinedSettings });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ settings: undefinedSettings });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -860,12 +914,16 @@ describe('AppContainer State Management', () => {
|
||||
describe('Quota and Fallback Integration', () => {
|
||||
it('passes a null proQuotaRequest to UIStateContext by default', async () => {
|
||||
// The default mock from beforeEach already sets proQuotaRequest to null
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Assert that the context value is as expected
|
||||
expect(capturedUIState.proQuotaRequest).toBeNull();
|
||||
});
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', async () => {
|
||||
@@ -881,12 +939,16 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Assert: The mock request is correctly passed through the context
|
||||
expect(capturedUIState.proQuotaRequest).toEqual(mockRequest);
|
||||
});
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('passes the handleProQuotaChoice function to UIActionsContext', async () => {
|
||||
@@ -898,7 +960,11 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Assert: The action in the context is the mock handler we provided
|
||||
expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler);
|
||||
@@ -909,7 +975,7 @@ describe('AppContainer State Management', () => {
|
||||
capturedUIActions.handleProQuotaChoice('retry_later');
|
||||
});
|
||||
expect(mockHandler).toHaveBeenCalledWith('retry_later');
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1327,13 +1393,17 @@ describe('AppContainer State Management', () => {
|
||||
activePtyId: 'some-id',
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(resizePtySpy).toHaveBeenCalled());
|
||||
const lastCall =
|
||||
resizePtySpy.mock.calls[resizePtySpy.mock.calls.length - 1];
|
||||
// Check the height argument specifically
|
||||
expect(lastCall[2]).toBe(1);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1672,11 +1742,15 @@ describe('AppContainer State Management', () => {
|
||||
closeModelDialog: vi.fn(),
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
expect(capturedUIState.isModelDialogOpen).toBe(true);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should provide model dialog actions in the UIActionsContext', async () => {
|
||||
@@ -1688,7 +1762,11 @@ describe('AppContainer State Management', () => {
|
||||
closeModelDialog: mockCloseModelDialog,
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Verify that the actions are correctly passed through context
|
||||
@@ -1696,13 +1774,17 @@ describe('AppContainer State Management', () => {
|
||||
capturedUIActions.closeModelDialog();
|
||||
});
|
||||
expect(mockCloseModelDialog).toHaveBeenCalled();
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreEvents Integration', () => {
|
||||
it('subscribes to UserFeedback and drains backlog on mount', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
expect(mockCoreEvents.on).toHaveBeenCalledWith(
|
||||
@@ -1710,14 +1792,18 @@ describe('AppContainer State Management', () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCoreEvents.drainBacklogs).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('unsubscribes from UserFeedback on unmount', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
|
||||
expect(mockCoreEvents.off).toHaveBeenCalledWith(
|
||||
CoreEvent.UserFeedback,
|
||||
@@ -1726,7 +1812,11 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('adds history item when UserFeedback event is received', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Get the registered handler
|
||||
@@ -1751,14 +1841,18 @@ describe('AppContainer State Management', () => {
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('updates currentModel when ModelChanged event is received', async () => {
|
||||
// Arrange: Mock initial model
|
||||
vi.spyOn(mockConfig, 'getModel').mockReturnValue('initial-model');
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState?.currentModel).toBe('initial-model');
|
||||
});
|
||||
@@ -1770,13 +1864,15 @@ describe('AppContainer State Management', () => {
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
// Act: Simulate ModelChanged event
|
||||
// Update config mock to return new model since the handler reads from config
|
||||
vi.spyOn(mockConfig, 'getModel').mockReturnValue('new-model');
|
||||
act(() => {
|
||||
handler({ model: 'new-model' });
|
||||
});
|
||||
|
||||
// Assert: Verify model is updated
|
||||
expect(capturedUIState.currentModel).toBe('new-model');
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1799,10 +1895,14 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// The main assertion is that the render does not throw.
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => expect(resizePtySpy).toHaveBeenCalled());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
describe('Banner Text', () => {
|
||||
@@ -1812,10 +1912,14 @@ describe('AppContainer State Management', () => {
|
||||
authType: AuthType.USE_GEMINI,
|
||||
apiKey: 'fake-key',
|
||||
});
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.bannerData.defaultText).toBeDefined();
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1838,7 +1942,11 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('clears the prompt when onCancelSubmit is called with shouldRestorePrompt=false', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
const { onCancelSubmit } = extractUseGeminiStreamArgs(
|
||||
@@ -1851,7 +1959,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
expect(mockSetText).toHaveBeenCalledWith('');
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('restores the prompt when onCancelSubmit is called with shouldRestorePrompt=true (or undefined)', async () => {
|
||||
@@ -1862,7 +1970,11 @@ describe('AppContainer State Management', () => {
|
||||
initializeFromLogger: vi.fn(),
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(capturedUIState.userMessages).toContain('previous message'),
|
||||
);
|
||||
@@ -1877,7 +1989,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
expect(mockSetText).toHaveBeenCalledWith('previous message');
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('input history is independent from conversation history (survives /clear)', async () => {
|
||||
@@ -1890,7 +2002,13 @@ describe('AppContainer State Management', () => {
|
||||
initializeFromLogger: vi.fn(),
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let rerender: (tree: ReactElement) => void;
|
||||
let unmount;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
rerender = result.rerender;
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
// Verify userMessages is populated from inputHistory
|
||||
await waitFor(() =>
|
||||
@@ -1908,12 +2026,17 @@ describe('AppContainer State Management', () => {
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// Rerender to apply the new mock.
|
||||
rerender(getAppContainer());
|
||||
});
|
||||
|
||||
// Verify that userMessages still contains the input history
|
||||
// (it should not be affected by clearing conversation history)
|
||||
expect(capturedUIState.userMessages).toContain('first prompt');
|
||||
expect(capturedUIState.userMessages).toContain('second prompt');
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1928,7 +2051,11 @@ describe('AppContainer State Management', () => {
|
||||
// Clear previous calls
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let compUnmount: () => void = () => {};
|
||||
await act(async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
compUnmount = unmount;
|
||||
});
|
||||
|
||||
// Allow async effects to run
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
@@ -1944,7 +2071,7 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
|
||||
expect(clearTerminalCalls).toHaveLength(0);
|
||||
unmount();
|
||||
compUnmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
type IdeContext,
|
||||
type UserTierId,
|
||||
type UserFeedbackPayload,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
IdeClient,
|
||||
ideContextStore,
|
||||
getErrorMessage,
|
||||
@@ -50,7 +49,6 @@ import {
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
refreshServerHierarchicalMemory,
|
||||
type ModelChangedPayload,
|
||||
type MemoryChangedPayload,
|
||||
writeToStdout,
|
||||
disableMouseEvents,
|
||||
@@ -123,7 +121,7 @@ import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||
import { useSettings } from './contexts/SettingsContext.js';
|
||||
import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js';
|
||||
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
|
||||
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||
import { enableBracketedPaste } from './utils/bracketedPaste.js';
|
||||
import { useBanner } from './hooks/useBanner.js';
|
||||
@@ -256,12 +254,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
|
||||
// Helper to determine the effective model, considering the fallback state.
|
||||
const getEffectiveModel = useCallback(() => {
|
||||
if (config.isInFallbackMode()) {
|
||||
return DEFAULT_GEMINI_FLASH_MODEL;
|
||||
}
|
||||
return config.getModel();
|
||||
}, [config]);
|
||||
const getEffectiveModel = useCallback(() => config.getModel(), [config]);
|
||||
|
||||
const [currentModel, setCurrentModel] = useState(getEffectiveModel());
|
||||
|
||||
@@ -340,22 +333,15 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
// Subscribe to fallback mode and model changes from core
|
||||
useEffect(() => {
|
||||
const handleFallbackModeChanged = () => {
|
||||
const effectiveModel = getEffectiveModel();
|
||||
setCurrentModel(effectiveModel);
|
||||
const handleModelChanged = () => {
|
||||
setCurrentModel(config.getModel());
|
||||
};
|
||||
|
||||
const handleModelChanged = (payload: ModelChangedPayload) => {
|
||||
setCurrentModel(payload.model);
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
|
||||
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
|
||||
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
|
||||
};
|
||||
}, [getEffectiveModel]);
|
||||
}, [getEffectiveModel, config]);
|
||||
|
||||
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
|
||||
useConsoleMessages();
|
||||
@@ -412,7 +398,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
app.rerender();
|
||||
}
|
||||
enableBracketedPaste();
|
||||
enableSupportedProtocol();
|
||||
terminalCapabilityManager.enableKittyProtocol();
|
||||
refreshStatic();
|
||||
}, [refreshStatic, isAlternateBuffer, app, config]);
|
||||
|
||||
@@ -1438,7 +1424,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
authType === AuthType.USE_VERTEX_AI
|
||||
) {
|
||||
setDefaultBannerText(
|
||||
'Gemini 3 is now available.\nTo use Gemini 3, enable "Preview features" in /settings\nLearn more at https://goo.gle/enable-preview-features',
|
||||
'Gemini 3 Flash and Pro are now available. \nEnable "Preview features" in /settings. \nLearn more at https://goo.gle/enable-preview-features',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1543,6 +1529,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
warningMessage,
|
||||
bannerData,
|
||||
bannerVisible,
|
||||
terminalBackgroundColor: config.getTerminalBackground(),
|
||||
}),
|
||||
[
|
||||
isThemeDialogOpen,
|
||||
@@ -1634,6 +1621,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
warningMessage,
|
||||
bannerData,
|
||||
bannerVisible,
|
||||
config,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -4,19 +4,44 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { authCommand } from './authCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...actual,
|
||||
clearCachedCredentialFile: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
describe('authCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockCommandContext();
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getGeminiClient: vi.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
// Add setValue mock to settings
|
||||
mockContext.services.settings.setValue = vi.fn();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return a dialog action to open the auth dialog', () => {
|
||||
it('should have subcommands: login and logout', () => {
|
||||
expect(authCommand.subCommands).toBeDefined();
|
||||
expect(authCommand.subCommands).toHaveLength(2);
|
||||
expect(authCommand.subCommands?.[0]?.name).toBe('login');
|
||||
expect(authCommand.subCommands?.[1]?.name).toBe('logout');
|
||||
});
|
||||
|
||||
it('should return a dialog action to open the auth dialog when called with no args', () => {
|
||||
if (!authCommand.action) {
|
||||
throw new Error('The auth command must have an action.');
|
||||
}
|
||||
@@ -31,6 +56,76 @@ describe('authCommand', () => {
|
||||
|
||||
it('should have the correct name and description', () => {
|
||||
expect(authCommand.name).toBe('auth');
|
||||
expect(authCommand.description).toBe('Change the auth method');
|
||||
expect(authCommand.description).toBe('Manage authentication');
|
||||
});
|
||||
|
||||
describe('auth login subcommand', () => {
|
||||
it('should return auth dialog action', () => {
|
||||
const loginCommand = authCommand.subCommands?.[0];
|
||||
expect(loginCommand?.name).toBe('login');
|
||||
const result = loginCommand!.action!(mockContext, '');
|
||||
expect(result).toEqual({ type: 'dialog', dialog: 'auth' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth logout subcommand', () => {
|
||||
it('should clear cached credentials', async () => {
|
||||
const logoutCommand = authCommand.subCommands?.[1];
|
||||
expect(logoutCommand?.name).toBe('logout');
|
||||
|
||||
const { clearCachedCredentialFile } = await import(
|
||||
'@google/gemini-cli-core'
|
||||
);
|
||||
|
||||
await logoutCommand!.action!(mockContext, '');
|
||||
|
||||
expect(clearCachedCredentialFile).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should clear selectedAuthType setting', async () => {
|
||||
const logoutCommand = authCommand.subCommands?.[1];
|
||||
|
||||
await logoutCommand!.action!(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip thoughts from history', async () => {
|
||||
const logoutCommand = authCommand.subCommands?.[1];
|
||||
const mockStripThoughts = vi.fn();
|
||||
const mockClient = {
|
||||
stripThoughtsFromHistory: mockStripThoughts,
|
||||
} as unknown as ReturnType<
|
||||
NonNullable<typeof mockContext.services.config>['getGeminiClient']
|
||||
>;
|
||||
|
||||
if (mockContext.services.config) {
|
||||
mockContext.services.config.getGeminiClient = vi.fn(() => mockClient);
|
||||
}
|
||||
|
||||
await logoutCommand!.action!(mockContext, '');
|
||||
|
||||
expect(mockStripThoughts).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return logout action to signal explicit state change', async () => {
|
||||
const logoutCommand = authCommand.subCommands?.[1];
|
||||
const result = await logoutCommand!.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({ type: 'logout' });
|
||||
});
|
||||
|
||||
it('should handle missing config gracefully', async () => {
|
||||
const logoutCommand = authCommand.subCommands?.[1];
|
||||
mockContext.services.config = null;
|
||||
|
||||
const result = await logoutCommand!.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({ type: 'logout' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { OpenDialogActionReturn, SlashCommand } from './types.js';
|
||||
import type {
|
||||
OpenDialogActionReturn,
|
||||
SlashCommand,
|
||||
LogoutActionReturn,
|
||||
} from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
export const authCommand: SlashCommand = {
|
||||
name: 'auth',
|
||||
description: 'Change the auth method',
|
||||
const authLoginCommand: SlashCommand = {
|
||||
name: 'login',
|
||||
description: 'Login or change the auth method',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (_context, _args): OpenDialogActionReturn => ({
|
||||
@@ -17,3 +23,34 @@ export const authCommand: SlashCommand = {
|
||||
dialog: 'auth',
|
||||
}),
|
||||
};
|
||||
|
||||
const authLogoutCommand: SlashCommand = {
|
||||
name: 'logout',
|
||||
description: 'Log out and clear all cached credentials',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context, _args): Promise<LogoutActionReturn> => {
|
||||
await clearCachedCredentialFile();
|
||||
// Clear the selected auth type so user sees the auth selection menu
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
undefined,
|
||||
);
|
||||
// Strip thoughts from history instead of clearing completely
|
||||
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
|
||||
// Return logout action to signal explicit state change
|
||||
return {
|
||||
type: 'logout',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const authCommand: SlashCommand = {
|
||||
name: 'auth',
|
||||
description: 'Manage authentication',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [authLoginCommand, authLogoutCommand],
|
||||
action: (context, args) =>
|
||||
// Default to login if no subcommand is provided
|
||||
authLoginCommand.action!(context, args),
|
||||
};
|
||||
|
||||
@@ -39,6 +39,14 @@ vi.mock('node:process', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../utils/terminalCapabilityManager.js', () => ({
|
||||
terminalCapabilityManager: {
|
||||
getTerminalName: vi.fn().mockReturnValue('Test Terminal'),
|
||||
getTerminalBackgroundColor: vi.fn().mockReturnValue('#000000'),
|
||||
isKittyProtocolEnabled: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('bugCommand', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getVersion).mockResolvedValue('0.1.0');
|
||||
@@ -73,6 +81,9 @@ describe('bugCommand', () => {
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** gemini-pro
|
||||
* **Memory Usage:** 100 MB
|
||||
* **Terminal Name:** Test Terminal
|
||||
* **Terminal Background:** #000000
|
||||
* **Kitty Keyboard Protocol:** Supported
|
||||
* **IDE Client:** VSCode
|
||||
`;
|
||||
const expectedUrl =
|
||||
@@ -106,6 +117,9 @@ describe('bugCommand', () => {
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** gemini-pro
|
||||
* **Memory Usage:** 100 MB
|
||||
* **Terminal Name:** Test Terminal
|
||||
* **Terminal Background:** #000000
|
||||
* **Kitty Keyboard Protocol:** Supported
|
||||
* **IDE Client:** VSCode
|
||||
`;
|
||||
const expectedUrl = customTemplate
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MessageType } from '../types.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
import { formatMemoryUsage } from '../utils/formatters.js';
|
||||
import { IdeClient, sessionId, getVersion } from '@google/gemini-cli-core';
|
||||
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
|
||||
|
||||
export const bugCommand: SlashCommand = {
|
||||
name: 'bug',
|
||||
@@ -38,6 +39,13 @@ export const bugCommand: SlashCommand = {
|
||||
const cliVersion = await getVersion();
|
||||
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
|
||||
const ideClient = await getIdeClientName(context);
|
||||
const terminalName =
|
||||
terminalCapabilityManager.getTerminalName() || 'Unknown';
|
||||
const terminalBgColor =
|
||||
terminalCapabilityManager.getTerminalBackgroundColor() || 'Unknown';
|
||||
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
|
||||
? 'Supported'
|
||||
: 'Unsupported';
|
||||
|
||||
let info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
@@ -47,6 +55,9 @@ export const bugCommand: SlashCommand = {
|
||||
* **Sandbox Environment:** ${sandboxEnv}
|
||||
* **Model Version:** ${modelVersion}
|
||||
* **Memory Usage:** ${memoryUsage}
|
||||
* **Terminal Name:** ${terminalName}
|
||||
* **Terminal Background:** ${terminalBgColor}
|
||||
* **Kitty Keyboard Protocol:** ${kittyProtocol}
|
||||
`;
|
||||
if (ideClient) {
|
||||
info += `* **IDE Client:** ${ideClient}\n`;
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('chatCommand', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetHistory = vi.fn().mockReturnValue([]);
|
||||
mockGetChat = vi.fn().mockResolvedValue({
|
||||
mockGetChat = vi.fn().mockReturnValue({
|
||||
getHistory: mockGetHistory,
|
||||
});
|
||||
mockSaveCheckpoint = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -163,7 +163,6 @@ describe('chatCommand', () => {
|
||||
|
||||
mockGetHistory.mockReturnValue([
|
||||
{ role: 'user', parts: [{ text: 'context for our chat' }] },
|
||||
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
|
||||
]);
|
||||
result = await saveCommand?.action?.(mockContext, tag);
|
||||
expect(result).toEqual({
|
||||
@@ -208,9 +207,7 @@ describe('chatCommand', () => {
|
||||
it('should save the conversation if overwrite is confirmed', async () => {
|
||||
const history: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'context for our chat' }] },
|
||||
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
|
||||
{ role: 'user', parts: [{ text: 'hello' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi there!' }] },
|
||||
];
|
||||
mockGetHistory.mockReturnValue(history);
|
||||
mockContext.overwriteConfirmed = true;
|
||||
@@ -263,6 +260,7 @@ describe('chatCommand', () => {
|
||||
|
||||
it('should resume a conversation with matching authType', async () => {
|
||||
const conversation: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'system setup' }] },
|
||||
{ role: 'user', parts: [{ text: 'hello gemini' }] },
|
||||
{ role: 'model', parts: [{ text: 'hello world' }] },
|
||||
];
|
||||
@@ -285,6 +283,7 @@ describe('chatCommand', () => {
|
||||
|
||||
it('should block resuming a conversation with mismatched authType', async () => {
|
||||
const conversation: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'system setup' }] },
|
||||
{ role: 'user', parts: [{ text: 'hello gemini' }] },
|
||||
{ role: 'model', parts: [{ text: 'hello world' }] },
|
||||
];
|
||||
@@ -304,6 +303,7 @@ describe('chatCommand', () => {
|
||||
|
||||
it('should resume a legacy conversation without authType', async () => {
|
||||
const conversation: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'system setup' }] },
|
||||
{ role: 'user', parts: [{ text: 'hello gemini' }] },
|
||||
{ role: 'model', parts: [{ text: 'hello world' }] },
|
||||
];
|
||||
@@ -521,7 +521,6 @@ Hi there!`;
|
||||
it('should inform if there is no conversation to share', async () => {
|
||||
mockGetHistory.mockReturnValue([
|
||||
{ role: 'user', parts: [{ text: 'context' }] },
|
||||
{ role: 'model', parts: [{ text: 'context response' }] },
|
||||
]);
|
||||
const result = await shareCommand?.action?.(mockContext, 'my-chat.json');
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
||||
|
||||
@@ -17,6 +17,7 @@ import { CommandKind } from './types.js';
|
||||
import {
|
||||
decodeTagName,
|
||||
type MessageActionReturn,
|
||||
INITIAL_HISTORY_LENGTH,
|
||||
} from '@google/gemini-cli-core';
|
||||
import path from 'node:path';
|
||||
import type {
|
||||
@@ -121,7 +122,7 @@ const saveCommand: SlashCommand = {
|
||||
}
|
||||
}
|
||||
|
||||
const chat = await config?.getGeminiClient()?.getChat();
|
||||
const chat = config?.getGeminiClient()?.getChat();
|
||||
if (!chat) {
|
||||
return {
|
||||
type: 'message',
|
||||
@@ -131,7 +132,7 @@ const saveCommand: SlashCommand = {
|
||||
}
|
||||
|
||||
const history = chat.getHistory();
|
||||
if (history.length > 2) {
|
||||
if (history.length > INITIAL_HISTORY_LENGTH) {
|
||||
const authType = config?.getContentGeneratorConfig()?.authType;
|
||||
await logger.saveCheckpoint({ history, authType }, tag);
|
||||
return {
|
||||
@@ -200,11 +201,8 @@ const resumeCommand: SlashCommand = {
|
||||
};
|
||||
|
||||
const uiHistory: HistoryItemWithoutId[] = [];
|
||||
let hasSystemPrompt = false;
|
||||
let i = 0;
|
||||
|
||||
for (const item of conversation) {
|
||||
i += 1;
|
||||
for (const item of conversation.slice(INITIAL_HISTORY_LENGTH)) {
|
||||
const text =
|
||||
item.parts
|
||||
?.filter((m) => !!m.text)
|
||||
@@ -213,15 +211,11 @@ const resumeCommand: SlashCommand = {
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
if (i === 1 && text.match(/context for our chat/)) {
|
||||
hasSystemPrompt = true;
|
||||
}
|
||||
if (i > 2 || !hasSystemPrompt) {
|
||||
uiHistory.push({
|
||||
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
|
||||
text,
|
||||
} as HistoryItemWithoutId);
|
||||
}
|
||||
|
||||
uiHistory.push({
|
||||
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
|
||||
text,
|
||||
} as HistoryItemWithoutId);
|
||||
}
|
||||
return {
|
||||
type: 'load_history',
|
||||
@@ -332,7 +326,7 @@ const shareCommand: SlashCommand = {
|
||||
};
|
||||
}
|
||||
|
||||
const chat = await context.services.config?.getGeminiClient()?.getChat();
|
||||
const chat = context.services.config?.getGeminiClient()?.getChat();
|
||||
if (!chat) {
|
||||
return {
|
||||
type: 'message',
|
||||
@@ -343,10 +337,10 @@ const shareCommand: SlashCommand = {
|
||||
|
||||
const history = chat.getHistory();
|
||||
|
||||
// An empty conversation has two hidden messages that setup the context for
|
||||
// An empty conversation has a hidden message that sets up the context for
|
||||
// the chat. Thus, to check whether a conversation has been started, we
|
||||
// can't check for length 0.
|
||||
if (history.length <= 2) {
|
||||
if (history.length <= INITIAL_HISTORY_LENGTH) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
|
||||
@@ -15,7 +15,7 @@ export const copyCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
|
||||
const chat = await context.services.config?.getGeminiClient()?.getChat();
|
||||
const chat = context.services.config?.getGeminiClient()?.getChat();
|
||||
const history = chat?.getHistory();
|
||||
|
||||
// Get the last message from the AI (model role)
|
||||
|
||||
@@ -309,6 +309,24 @@ describe('hooksCommand', () => {
|
||||
content: 'Failed to enable hook: Failed to save settings',
|
||||
});
|
||||
});
|
||||
|
||||
it('should complete hook names using friendly names', () => {
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
)!;
|
||||
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
true,
|
||||
);
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue([hookEntry]);
|
||||
|
||||
const completions = enableCmd.completion!(mockContext, 'frie');
|
||||
expect(completions).toContain('friendly-name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable subcommand', () => {
|
||||
|
||||
@@ -213,7 +213,7 @@ function completeHookNames(
|
||||
* Get a display name for a hook
|
||||
*/
|
||||
function getHookDisplayName(hook: HookRegistryEntry): string {
|
||||
return hook.config.command || 'unknown-hook';
|
||||
return hook.config.name || hook.config.command || 'unknown-hook';
|
||||
}
|
||||
|
||||
const panelCommand: SlashCommand = {
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('mcpCommand', () => {
|
||||
}),
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
getBlockedMcpServers: vi.fn().mockReturnValue([]),
|
||||
getPromptRegistry: vi.fn().mockResolvedValue({
|
||||
getPromptRegistry: vi.fn().mockReturnValue({
|
||||
getAllPrompts: vi.fn().mockReturnValue([]),
|
||||
getPromptsByServer: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
|
||||
@@ -216,7 +216,7 @@ const listAction = async (
|
||||
const allTools = toolRegistry.getAllTools();
|
||||
const mcpTools = allTools.filter((tool) => tool instanceof DiscoveredMCPTool);
|
||||
|
||||
const promptRegistry = await config.getPromptRegistry();
|
||||
const promptRegistry = config.getPromptRegistry();
|
||||
const mcpPrompts = promptRegistry
|
||||
.getAllPrompts()
|
||||
.filter(
|
||||
|
||||
@@ -157,12 +157,14 @@ describe('memoryCommand', () => {
|
||||
let mockSetUserMemory: Mock;
|
||||
let mockSetGeminiMdFileCount: Mock;
|
||||
let mockSetGeminiMdFilePaths: Mock;
|
||||
let mockContextManagerRefresh: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
refreshCommand = getSubCommand('refresh');
|
||||
mockSetUserMemory = vi.fn();
|
||||
mockSetGeminiMdFileCount = vi.fn();
|
||||
mockSetGeminiMdFilePaths = vi.fn();
|
||||
mockContextManagerRefresh = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const mockConfig = {
|
||||
setUserMemory: mockSetUserMemory,
|
||||
@@ -185,6 +187,12 @@ describe('memoryCommand', () => {
|
||||
updateSystemInstructionIfInitialized: vi
|
||||
.fn()
|
||||
.mockResolvedValue(undefined),
|
||||
isJitContextEnabled: vi.fn().mockReturnValue(false),
|
||||
getContextManager: vi.fn().mockReturnValue({
|
||||
refresh: mockContextManagerRefresh,
|
||||
}),
|
||||
getUserMemory: vi.fn().mockReturnValue(''),
|
||||
getGeminiMdFileCount: vi.fn().mockReturnValue(0),
|
||||
};
|
||||
|
||||
mockContext = createMockCommandContext({
|
||||
@@ -203,7 +211,32 @@ describe('memoryCommand', () => {
|
||||
mockRefreshServerHierarchicalMemory.mockClear();
|
||||
});
|
||||
|
||||
it('should display success message when memory is refreshed with content', async () => {
|
||||
it('should use ContextManager.refresh when JIT is enabled', async () => {
|
||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||
|
||||
// Enable JIT in mock config
|
||||
const config = mockContext.services.config;
|
||||
if (!config) throw new Error('Config is undefined');
|
||||
|
||||
vi.mocked(config.isJitContextEnabled).mockReturnValue(true);
|
||||
vi.mocked(config.getUserMemory).mockReturnValue('JIT Memory Content');
|
||||
vi.mocked(config.getGeminiMdFileCount).mockReturnValue(3);
|
||||
|
||||
await refreshCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContextManagerRefresh).toHaveBeenCalledOnce();
|
||||
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Memory refreshed successfully. Loaded 18 characters from 3 file(s).',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should display success message when memory is refreshed with content (Legacy)', async () => {
|
||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const refreshResult: LoadServerHierarchicalMemoryResponse = {
|
||||
|
||||
@@ -85,10 +85,20 @@ export const memoryCommand: SlashCommand = {
|
||||
);
|
||||
|
||||
try {
|
||||
const config = await context.services.config;
|
||||
const config = context.services.config;
|
||||
if (config) {
|
||||
const { memoryContent, fileCount } =
|
||||
await refreshServerHierarchicalMemory(config);
|
||||
let memoryContent = '';
|
||||
let fileCount = 0;
|
||||
|
||||
if (config.isJitContextEnabled()) {
|
||||
await config.getContextManager()?.refresh();
|
||||
memoryContent = config.getUserMemory();
|
||||
fileCount = config.getGeminiMdFileCount();
|
||||
} else {
|
||||
const result = await refreshServerHierarchicalMemory(config);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
}
|
||||
|
||||
await config.updateSystemInstructionIfInitialized();
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { modelCommand } from './modelCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
describe('modelCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
@@ -29,6 +30,21 @@ describe('modelCommand', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should call refreshUserQuota if config is available', async () => {
|
||||
if (!modelCommand.action) {
|
||||
throw new Error('The model command must have an action.');
|
||||
}
|
||||
|
||||
const mockRefreshUserQuota = vi.fn();
|
||||
mockContext.services.config = {
|
||||
refreshUserQuota: mockRefreshUserQuota,
|
||||
} as unknown as Config;
|
||||
|
||||
await modelCommand.action(mockContext, '');
|
||||
|
||||
expect(mockRefreshUserQuota).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have the correct name and description', () => {
|
||||
expect(modelCommand.name).toBe('model');
|
||||
expect(modelCommand.description).toBe(
|
||||
|
||||
@@ -4,15 +4,24 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
import {
|
||||
type CommandContext,
|
||||
CommandKind,
|
||||
type SlashCommand,
|
||||
} from './types.js';
|
||||
|
||||
export const modelCommand: SlashCommand = {
|
||||
name: 'model',
|
||||
description: 'Opens a dialog to configure the model',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async () => ({
|
||||
type: 'dialog',
|
||||
dialog: 'model',
|
||||
}),
|
||||
action: async (context: CommandContext) => {
|
||||
if (context.services.config) {
|
||||
await context.services.config.refreshUserQuota();
|
||||
}
|
||||
return {
|
||||
type: 'dialog',
|
||||
dialog: 'model',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ async function restoreAction(
|
||||
} else if (action.type === 'load_history' && loadHistory) {
|
||||
loadHistory(action.history);
|
||||
if (action.clientHistory) {
|
||||
await config?.getGeminiClient()?.setHistory(action.clientHistory);
|
||||
config?.getGeminiClient()?.setHistory(action.clientHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
describe('statsCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
@@ -45,6 +46,26 @@ describe('statsCommand', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch and display quota if config is available', async () => {
|
||||
if (!statsCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const mockQuota = { buckets: [] };
|
||||
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
|
||||
mockContext.services.config = {
|
||||
refreshUserQuota: mockRefreshUserQuota,
|
||||
} as unknown as Config;
|
||||
|
||||
await statsCommand.action(mockContext, '');
|
||||
|
||||
expect(mockRefreshUserQuota).toHaveBeenCalled();
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
quotas: mockQuota,
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should display model stats when using the "model" subcommand', () => {
|
||||
const modelSubCommand = statsCommand.subCommands?.find(
|
||||
(sc) => sc.name === 'model',
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CodeAssistServer, getCodeAssistServer } from '@google/gemini-cli-core';
|
||||
import type { HistoryItemStats } from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
@@ -35,11 +34,8 @@ async function defaultSessionView(context: CommandContext) {
|
||||
};
|
||||
|
||||
if (context.services.config) {
|
||||
const server = getCodeAssistServer(context.services.config);
|
||||
if (server instanceof CodeAssistServer && server.projectId) {
|
||||
const quota = await server.retrieveUserQuota({
|
||||
project: server.projectId,
|
||||
});
|
||||
const quota = await context.services.config.refreshUserQuota();
|
||||
if (quota) {
|
||||
statsItem.quotas = quota;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,13 +142,22 @@ export interface OpenCustomDialogActionReturn {
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* The return type for a command action that specifically handles logout logic,
|
||||
* signaling the application to explicitly transition to an unauthenticated state.
|
||||
*/
|
||||
export interface LogoutActionReturn {
|
||||
type: 'logout';
|
||||
}
|
||||
|
||||
export type SlashCommandActionReturn =
|
||||
| CommandActionReturn<HistoryItemWithoutId[]>
|
||||
| QuitActionReturn
|
||||
| OpenDialogActionReturn
|
||||
| ConfirmShellCommandsActionReturn
|
||||
| ConfirmActionReturn
|
||||
| OpenCustomDialogActionReturn;
|
||||
| OpenCustomDialogActionReturn
|
||||
| LogoutActionReturn;
|
||||
|
||||
export enum CommandKind {
|
||||
BUILT_IN = 'built-in',
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('<AppHeader />', () => {
|
||||
it('should render the banner with default text', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
@@ -52,6 +53,7 @@ describe('<AppHeader />', () => {
|
||||
it('should render the banner with warning text', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: 'There are capacity issues',
|
||||
@@ -72,6 +74,7 @@ describe('<AppHeader />', () => {
|
||||
it('should not render the banner when no flags are set', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: '',
|
||||
warningText: '',
|
||||
@@ -91,6 +94,7 @@ describe('<AppHeader />', () => {
|
||||
it('should render the banner when previewFeatures is disabled', () => {
|
||||
const mockConfig = makeFakeConfig({ previewFeatures: false });
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
@@ -111,6 +115,7 @@ describe('<AppHeader />', () => {
|
||||
it('should not render the banner when previewFeatures is enabled', () => {
|
||||
const mockConfig = makeFakeConfig({ previewFeatures: true });
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
@@ -131,6 +136,7 @@ describe('<AppHeader />', () => {
|
||||
persistentStateMock.get.mockReturnValue(5);
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
@@ -151,6 +157,7 @@ describe('<AppHeader />', () => {
|
||||
persistentStateMock.get.mockReturnValue({});
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
@@ -177,6 +184,7 @@ describe('<AppHeader />', () => {
|
||||
it('should render banner text with unescaped newlines', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
bannerData: {
|
||||
defaultText: 'First line\\nSecond line',
|
||||
warningText: '',
|
||||
|
||||
@@ -173,7 +173,9 @@ export const Composer = () => {
|
||||
placeholder={
|
||||
vimEnabled
|
||||
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
|
||||
: ' Type your message or @path/to/file'
|
||||
: uiState.shellModeActive
|
||||
? ' Type your shell command'
|
||||
: ' Type your message or @path/to/file'
|
||||
}
|
||||
setQueueErrorMessage={uiActions.setQueueErrorMessage}
|
||||
streamingState={uiState.streamingState}
|
||||
|
||||
@@ -80,14 +80,14 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" width="100%">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.status.warning}
|
||||
padding={1}
|
||||
width="100%"
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { shortenPath, tildeifyPath } from '@google/gemini-cli-core';
|
||||
import {
|
||||
shortenPath,
|
||||
tildeifyPath,
|
||||
getDisplayString,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
@@ -145,7 +149,8 @@ export const Footer: React.FC = () => {
|
||||
<Box alignItems="center" justifyContent="flex-end">
|
||||
<Box alignItems="center">
|
||||
<Text color={theme.text.accent}>
|
||||
{model}
|
||||
{getDisplayString(model, config.getPreviewFeatures())}
|
||||
<Text color={theme.text.secondary}> /model</Text>
|
||||
{!hideContextPercentage && (
|
||||
<>
|
||||
{' '}
|
||||
|
||||
@@ -15,6 +15,9 @@ import { Text } from 'ink';
|
||||
import type React from 'react';
|
||||
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
vi.mock('../hooks/useSnowfall.js', () => ({
|
||||
useSnowfall: vi.fn((art) => art),
|
||||
}));
|
||||
vi.mock('../utils/terminalSetup.js', () => ({
|
||||
getTerminalProgram: vi.fn(),
|
||||
}));
|
||||
@@ -159,7 +162,6 @@ describe('<Header />', () => {
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).not.toHaveBeenCalled();
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
console.log(JSON.stringify(textCalls, null, 2));
|
||||
expect(textCalls.length).toBe(1);
|
||||
expect(textCalls[0][0]).toHaveProperty('color', singleColor);
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { getAsciiArtWidth } from '../utils/textUtils.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { getTerminalProgram } from '../utils/terminalSetup.js';
|
||||
import { useSnowfall } from '../hooks/useSnowfall.js';
|
||||
|
||||
interface HeaderProps {
|
||||
customAsciiArt?: string; // For user-defined ASCII art
|
||||
@@ -47,6 +48,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
}
|
||||
|
||||
const artWidth = getAsciiArtWidth(displayTitle);
|
||||
const title = useSnowfall(displayTitle);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -55,7 +57,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
flexShrink={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
<ThemedGradient>{displayTitle}</ThemedGradient>
|
||||
<ThemedGradient>{title}</ThemedGradient>
|
||||
{nightly && (
|
||||
<Box width="100%" flexDirection="row" justifyContent="flex-end">
|
||||
<ThemedGradient>v{version}</ThemedGradient>
|
||||
|
||||
@@ -768,6 +768,63 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should execute perfect match on Enter even if suggestions are showing, if at first suggestion', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'review', value: 'review' }, // Match is now at index 0
|
||||
{ label: 'review-frontend', value: 'review-frontend' },
|
||||
],
|
||||
activeSuggestionIndex: 0,
|
||||
isPerfectMatch: true,
|
||||
});
|
||||
props.buffer.text = '/review';
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('/review');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete and NOT execute on Enter if a DIFFERENT suggestion is selected even if perfect match', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'review', value: 'review' },
|
||||
{ label: 'review-frontend', value: 'review-frontend' },
|
||||
],
|
||||
activeSuggestionIndex: 1, // review-frontend selected (not the perfect match at 0)
|
||||
isPerfectMatch: true, // /review is a perfect match
|
||||
});
|
||||
props.buffer.text = '/review';
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should handle autocomplete for index 1
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
|
||||
// Should NOT submit
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should submit directly on Enter when a complete leaf command is typed', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
|
||||
@@ -589,7 +589,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
// If the command is a perfect match, pressing enter should execute it.
|
||||
if (completion.isPerfectMatch && keyMatchers[Command.RETURN](key)) {
|
||||
// We prioritize execution unless the user is explicitly selecting a different suggestion.
|
||||
if (
|
||||
completion.isPerfectMatch &&
|
||||
keyMatchers[Command.RETURN](key) &&
|
||||
(!completion.showSuggestions || completion.activeSuggestionIndex <= 0)
|
||||
) {
|
||||
handleSubmit(buffer.text);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
LogoutConfirmationDialog,
|
||||
LogoutChoice,
|
||||
} from './LogoutConfirmationDialog.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
|
||||
vi.mock('./shared/RadioButtonSelect.js', () => ({
|
||||
RadioButtonSelect: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
describe('LogoutConfirmationDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the dialog with title, description, and hint', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('You are now logged out.');
|
||||
expect(lastFrame()).toContain(
|
||||
'Login again to continue using Gemini CLI, or exit the application.',
|
||||
);
|
||||
expect(lastFrame()).toContain('(Use Enter to select, Esc to close)');
|
||||
});
|
||||
|
||||
it('should render RadioButtonSelect with Login and Exit options', () => {
|
||||
renderWithProviders(<LogoutConfirmationDialog onSelect={vi.fn()} />);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalled();
|
||||
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
|
||||
expect(mockCall.items).toEqual([
|
||||
{ label: 'Login', value: LogoutChoice.LOGIN, key: 'login' },
|
||||
{ label: 'Exit', value: LogoutChoice.EXIT, key: 'exit' },
|
||||
]);
|
||||
expect(mockCall.isFocused).toBe(true);
|
||||
});
|
||||
|
||||
it('should call onSelect with LOGIN when Login is selected', () => {
|
||||
const onSelect = vi.fn();
|
||||
renderWithProviders(<LogoutConfirmationDialog onSelect={onSelect} />);
|
||||
|
||||
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
|
||||
mockCall.onSelect(LogoutChoice.LOGIN);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(LogoutChoice.LOGIN);
|
||||
});
|
||||
|
||||
it('should call onSelect with EXIT when Exit is selected', () => {
|
||||
const onSelect = vi.fn();
|
||||
renderWithProviders(<LogoutConfirmationDialog onSelect={onSelect} />);
|
||||
|
||||
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
|
||||
mockCall.onSelect(LogoutChoice.EXIT);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(LogoutChoice.EXIT);
|
||||
});
|
||||
|
||||
it('should call onSelect with EXIT when escape key is pressed', () => {
|
||||
const onSelect = vi.fn();
|
||||
const { stdin } = renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={onSelect} />,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
// Send kitty escape key sequence
|
||||
stdin.write('\u001b[27u');
|
||||
});
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(LogoutChoice.EXIT);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
export enum LogoutChoice {
|
||||
LOGIN = 'login',
|
||||
EXIT = 'exit',
|
||||
}
|
||||
|
||||
interface LogoutConfirmationDialogProps {
|
||||
onSelect: (choice: LogoutChoice) => void;
|
||||
}
|
||||
|
||||
export const LogoutConfirmationDialog: React.FC<
|
||||
LogoutConfirmationDialogProps
|
||||
> = ({ onSelect }) => {
|
||||
// Handle escape key to exit (consistent with other dialogs)
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onSelect(LogoutChoice.EXIT);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const options: Array<RadioSelectItem<LogoutChoice>> = [
|
||||
{
|
||||
label: 'Login',
|
||||
value: LogoutChoice.LOGIN,
|
||||
key: 'login',
|
||||
},
|
||||
{
|
||||
label: 'Exit',
|
||||
value: LogoutChoice.EXIT,
|
||||
key: 'exit',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" width="100%">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.focused}
|
||||
padding={1}
|
||||
flexGrow={1}
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
You are now logged out.
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Login again to continue using Gemini CLI, or exit the application.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<RadioButtonSelect items={options} onSelect={onSelect} isFocused />
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
(Use Enter to select, Esc to close)
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -4,239 +4,240 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { cleanup } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ModelDialog } from './ModelDialog.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
const mockedUseKeypress = vi.mocked(useKeypress);
|
||||
|
||||
vi.mock('./shared/DescriptiveRadioButtonSelect.js', () => ({
|
||||
DescriptiveRadioButtonSelect: vi.fn(() => null),
|
||||
}));
|
||||
const mockedSelect = vi.mocked(DescriptiveRadioButtonSelect);
|
||||
|
||||
const renderComponent = (
|
||||
props: Partial<React.ComponentProps<typeof ModelDialog>> = {},
|
||||
contextValue: Partial<Config> | undefined = undefined,
|
||||
) => {
|
||||
const defaultProps = {
|
||||
onClose: vi.fn(),
|
||||
};
|
||||
const combinedProps = { ...defaultProps, ...props };
|
||||
|
||||
const mockConfig = contextValue
|
||||
? ({
|
||||
// --- Functions used by ModelDialog ---
|
||||
getModel: vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO),
|
||||
setModel: vi.fn(),
|
||||
getPreviewFeatures: vi.fn(() => false),
|
||||
|
||||
// --- Functions used by ClearcutLogger ---
|
||||
getUsageStatisticsEnabled: vi.fn(() => true),
|
||||
getSessionId: vi.fn(() => 'mock-session-id'),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getContentGeneratorConfig: vi.fn(() => ({ authType: 'mock' })),
|
||||
getUseSmartEdit: vi.fn(() => false),
|
||||
getProxy: vi.fn(() => undefined),
|
||||
isInteractive: vi.fn(() => false),
|
||||
getExperiments: () => {},
|
||||
|
||||
// --- Spread test-specific overrides ---
|
||||
...contextValue,
|
||||
} as Config)
|
||||
: undefined;
|
||||
|
||||
const renderResult = render(
|
||||
<ConfigContext.Provider value={mockConfig}>
|
||||
<ModelDialog {...combinedProps} />
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
// Mock dependencies
|
||||
const mockGetDisplayString = vi.fn();
|
||||
const mockLogModelSlashCommand = vi.fn();
|
||||
const mockModelSlashCommandEvent = vi.fn();
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...renderResult,
|
||||
props: combinedProps,
|
||||
mockConfig,
|
||||
...actual,
|
||||
getDisplayString: (val: string) => mockGetDisplayString(val),
|
||||
logModelSlashCommand: (config: Config, event: ModelSlashCommandEvent) =>
|
||||
mockLogModelSlashCommand(config, event),
|
||||
ModelSlashCommandEvent: class {
|
||||
constructor(model: string) {
|
||||
mockModelSlashCommandEvent(model);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
describe('<ModelDialog />', () => {
|
||||
const mockSetModel = vi.fn();
|
||||
const mockGetModel = vi.fn();
|
||||
const mockGetPreviewFeatures = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
const mockGetHasAccessToPreviewModel = vi.fn();
|
||||
|
||||
interface MockConfig extends Partial<Config> {
|
||||
setModel: (model: string) => void;
|
||||
getModel: () => string;
|
||||
getPreviewFeatures: () => boolean;
|
||||
getHasAccessToPreviewModel: () => boolean;
|
||||
}
|
||||
|
||||
const mockConfig: MockConfig = {
|
||||
setModel: mockSetModel,
|
||||
getModel: mockGetModel,
|
||||
getPreviewFeatures: mockGetPreviewFeatures,
|
||||
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
mockGetPreviewFeatures.mockReturnValue(false);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||
|
||||
// Default implementation for getDisplayString
|
||||
mockGetDisplayString.mockImplementation((val: string) => {
|
||||
if (val === 'auto-gemini-2.5') return 'Auto (Gemini 2.5)';
|
||||
if (val === 'auto-gemini-3') return 'Auto (Preview)';
|
||||
return val;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
const renderComponent = (contextValue = mockConfig as Config) =>
|
||||
render(
|
||||
<KeypressProvider>
|
||||
<ConfigContext.Provider value={contextValue}>
|
||||
<ModelDialog onClose={mockOnClose} />
|
||||
</ConfigContext.Provider>
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
it('renders the title and help text', () => {
|
||||
const { lastFrame, unmount } = renderComponent();
|
||||
const waitForUpdate = () =>
|
||||
new Promise((resolve) => setTimeout(resolve, 150));
|
||||
|
||||
it('renders the initial "main" view correctly', () => {
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).toContain('Select Model');
|
||||
expect(lastFrame()).toContain('(Press Esc to close)');
|
||||
expect(lastFrame()).toContain(
|
||||
'To use a specific Gemini model on startup, use the --model flag.',
|
||||
);
|
||||
unmount();
|
||||
expect(lastFrame()).toContain('Auto');
|
||||
expect(lastFrame()).toContain('Manual');
|
||||
});
|
||||
|
||||
it('passes all model options to DescriptiveRadioButtonSelect', () => {
|
||||
const { unmount } = renderComponent();
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
|
||||
const props = mockedSelect.mock.calls[0][0];
|
||||
expect(props.items).toHaveLength(4);
|
||||
expect(props.items[0].value).toBe(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
expect(props.items[1].value).toBe(GEMINI_MODEL_ALIAS_PRO);
|
||||
expect(props.items[2].value).toBe(GEMINI_MODEL_ALIAS_FLASH);
|
||||
expect(props.items[3].value).toBe(GEMINI_MODEL_ALIAS_FLASH_LITE);
|
||||
expect(props.showNumbers).toBe(true);
|
||||
unmount();
|
||||
it('renders "main" view with preview options when preview features are enabled', () => {
|
||||
mockGetPreviewFeatures.mockReturnValue(true);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true); // Must have access
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).toContain('Auto (Preview)');
|
||||
});
|
||||
|
||||
it('initializes with the model from ConfigContext', () => {
|
||||
const mockGetModel = vi.fn(() => GEMINI_MODEL_ALIAS_FLASH);
|
||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
||||
it('switches to "manual" view when "Manual" is selected', async () => {
|
||||
const { lastFrame, stdin } = renderComponent();
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialIndex: 2,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
// Select "Manual" (index 1)
|
||||
// Press down arrow to move to "Manual"
|
||||
stdin.write('\u001B[B'); // Arrow Down
|
||||
await waitForUpdate();
|
||||
|
||||
// Press enter to select
|
||||
stdin.write('\r');
|
||||
await waitForUpdate();
|
||||
|
||||
// Should now show manual options
|
||||
expect(lastFrame()).toContain(DEFAULT_GEMINI_MODEL);
|
||||
expect(lastFrame()).toContain(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(lastFrame()).toContain(DEFAULT_GEMINI_FLASH_LITE_MODEL);
|
||||
});
|
||||
|
||||
it('initializes with "auto" model if context is not provided', () => {
|
||||
const { unmount } = renderComponent({}, undefined);
|
||||
it('renders "manual" view with preview options when preview features are enabled', async () => {
|
||||
mockGetPreviewFeatures.mockReturnValue(true);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true); // Must have access
|
||||
mockGetModel.mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||
const { lastFrame, stdin } = renderComponent();
|
||||
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialIndex: 0,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
// Select "Manual" (index 2 because Preview Auto is first, then Auto (Gemini 2.5))
|
||||
// Press down enough times to ensure we reach the bottom (Manual)
|
||||
stdin.write('\u001B[B'); // Arrow Down
|
||||
await waitForUpdate();
|
||||
stdin.write('\u001B[B'); // Arrow Down
|
||||
await waitForUpdate();
|
||||
|
||||
// Press enter to select Manual
|
||||
stdin.write('\r');
|
||||
await waitForUpdate();
|
||||
|
||||
expect(lastFrame()).toContain(PREVIEW_GEMINI_MODEL);
|
||||
});
|
||||
|
||||
it('initializes with "auto" model if getModel returns undefined', () => {
|
||||
const mockGetModel = vi.fn(() => undefined);
|
||||
// @ts-expect-error This test validates component robustness when getModel
|
||||
// returns an unexpected undefined value.
|
||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
||||
it('sets model and closes when a model is selected in "main" view', async () => {
|
||||
const { stdin } = renderComponent();
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
// Select "Auto" (index 0)
|
||||
stdin.write('\r');
|
||||
await waitForUpdate();
|
||||
|
||||
// When getModel returns undefined, preferredModel falls back to DEFAULT_GEMINI_MODEL_AUTO
|
||||
// which has index 0, so initialIndex should be 0
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialIndex: 0,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls config.setModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', () => {
|
||||
const { props, mockConfig, unmount } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
it('sets model and closes when a model is selected in "manual" view', async () => {
|
||||
const { stdin } = renderComponent();
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
expect(childOnSelect).toBeDefined();
|
||||
// Navigate to Manual (index 1) and select
|
||||
stdin.write('\u001B[B');
|
||||
await waitForUpdate();
|
||||
stdin.write('\r');
|
||||
await waitForUpdate();
|
||||
|
||||
childOnSelect(GEMINI_MODEL_ALIAS_PRO);
|
||||
// Now in manual view. Default selection is first item (DEFAULT_GEMINI_MODEL)
|
||||
stdin.write('\r');
|
||||
await waitForUpdate();
|
||||
|
||||
// Assert against the default mock provided by renderComponent
|
||||
expect(mockConfig?.setModel).toHaveBeenCalledWith(GEMINI_MODEL_ALIAS_PRO);
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => {
|
||||
const { unmount } = renderComponent();
|
||||
it('closes dialog on escape in "main" view', async () => {
|
||||
const { stdin } = renderComponent();
|
||||
|
||||
const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight;
|
||||
expect(childOnHighlight).toBeUndefined();
|
||||
unmount();
|
||||
stdin.write('\u001B'); // Escape
|
||||
await waitForUpdate();
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClose prop when "escape" key is pressed', () => {
|
||||
const { props, unmount } = renderComponent();
|
||||
it('goes back to "main" view on escape in "manual" view', async () => {
|
||||
const { lastFrame, stdin } = renderComponent();
|
||||
|
||||
expect(mockedUseKeypress).toHaveBeenCalled();
|
||||
// Go to manual view
|
||||
stdin.write('\u001B[B');
|
||||
await waitForUpdate();
|
||||
stdin.write('\r');
|
||||
await waitForUpdate();
|
||||
|
||||
const keyPressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
const options = mockedUseKeypress.mock.calls[0][1];
|
||||
expect(lastFrame()).toContain(DEFAULT_GEMINI_MODEL);
|
||||
|
||||
expect(options).toEqual({ isActive: true });
|
||||
// Press Escape
|
||||
stdin.write('\u001B');
|
||||
await waitForUpdate();
|
||||
|
||||
keyPressHandler({
|
||||
name: 'escape',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
insertable: false,
|
||||
sequence: '',
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
// Should be back to main view (Manual option visible)
|
||||
expect(lastFrame()).toContain('Manual');
|
||||
});
|
||||
|
||||
describe('Preview Logic', () => {
|
||||
it('should NOT show preview options if user has no access', () => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||
mockGetPreviewFeatures.mockReturnValue(true); // Even if enabled
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).not.toContain('Auto (Preview)');
|
||||
});
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
keyPressHandler({
|
||||
name: 'a',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
insertable: true,
|
||||
sequence: '',
|
||||
it('should NOT show preview options if user has access but preview features are disabled', () => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetPreviewFeatures.mockReturnValue(false);
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).not.toContain('Auto (Preview)');
|
||||
});
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('updates initialIndex when config context changes', () => {
|
||||
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO);
|
||||
const oldMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getPreviewFeatures: vi.fn(() => false),
|
||||
} as unknown as Config;
|
||||
const { rerender, unmount } = render(
|
||||
<ConfigContext.Provider value={oldMockConfig}>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
it('should show preview options if user has access AND preview features are enabled', () => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetPreviewFeatures.mockReturnValue(true);
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).toContain('Auto (Preview)');
|
||||
});
|
||||
|
||||
expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0);
|
||||
it('should show "Gemini 3 is now available" header if user has access but preview features disabled', () => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetPreviewFeatures.mockReturnValue(false);
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).toContain('Gemini 3 is now available.');
|
||||
expect(lastFrame()).toContain('Enable "Preview features" in /settings');
|
||||
});
|
||||
|
||||
mockGetModel.mockReturnValue(GEMINI_MODEL_ALIAS_FLASH_LITE);
|
||||
const newMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getPreviewFeatures: vi.fn(() => false),
|
||||
} as unknown as Config;
|
||||
it('should show "Gemini 3 is coming soon" header if user has no access', () => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||
mockGetPreviewFeatures.mockReturnValue(false);
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).toContain('Gemini 3 is coming soon.');
|
||||
});
|
||||
|
||||
rerender(
|
||||
<ConfigContext.Provider value={newMockConfig}>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
|
||||
// Should be called at least twice: initial render + re-render after context change
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(2);
|
||||
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(3);
|
||||
unmount();
|
||||
it('should NOT show header/subheader if preview options are shown', () => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetPreviewFeatures.mockReturnValue(true);
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).not.toContain('Gemini 3 is now available.');
|
||||
expect(lastFrame()).not.toContain('Gemini 3 is coming soon.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
getDisplayString,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
@@ -31,61 +31,131 @@ interface ModelDialogProps {
|
||||
|
||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
const [view, setView] = useState<'main' | 'manual'>('main');
|
||||
|
||||
// Determine the Preferred Model (read once when the dialog opens).
|
||||
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
||||
|
||||
const shouldShowPreviewModels =
|
||||
config?.getPreviewFeatures() && config.getHasAccessToPreviewModel();
|
||||
|
||||
const manualModelSelected = useMemo(() => {
|
||||
const manualModels = [
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
];
|
||||
if (manualModels.includes(preferredModel)) {
|
||||
return preferredModel;
|
||||
}
|
||||
return '';
|
||||
}, [preferredModel]);
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onClose();
|
||||
if (view === 'manual') {
|
||||
setView('main');
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
const mainOptions = useMemo(() => {
|
||||
const list = [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
title: 'Auto',
|
||||
description: 'Let the system choose the best model for your task.',
|
||||
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
|
||||
description:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
|
||||
key: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
},
|
||||
{
|
||||
value: GEMINI_MODEL_ALIAS_PRO,
|
||||
title: config?.getPreviewFeatures()
|
||||
? `Pro (${PREVIEW_GEMINI_MODEL}, ${DEFAULT_GEMINI_MODEL})`
|
||||
: `Pro (${DEFAULT_GEMINI_MODEL})`,
|
||||
value: 'Manual',
|
||||
title: manualModelSelected
|
||||
? `Manual (${manualModelSelected})`
|
||||
: 'Manual',
|
||||
description: 'Manually select a model',
|
||||
key: 'Manual',
|
||||
},
|
||||
];
|
||||
|
||||
if (shouldShowPreviewModels) {
|
||||
list.unshift({
|
||||
value: PREVIEW_GEMINI_MODEL_AUTO,
|
||||
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
|
||||
description:
|
||||
'For complex tasks that require deep reasoning and creativity',
|
||||
key: GEMINI_MODEL_ALIAS_PRO,
|
||||
'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
||||
key: PREVIEW_GEMINI_MODEL_AUTO,
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}, [shouldShowPreviewModels, manualModelSelected]);
|
||||
|
||||
const manualOptions = useMemo(() => {
|
||||
const list = [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL,
|
||||
title: DEFAULT_GEMINI_MODEL,
|
||||
key: DEFAULT_GEMINI_MODEL,
|
||||
},
|
||||
{
|
||||
value: GEMINI_MODEL_ALIAS_FLASH,
|
||||
title: `Flash (${DEFAULT_GEMINI_FLASH_MODEL})`,
|
||||
description: 'For tasks that need a balance of speed and reasoning',
|
||||
key: GEMINI_MODEL_ALIAS_FLASH,
|
||||
value: DEFAULT_GEMINI_FLASH_MODEL,
|
||||
title: DEFAULT_GEMINI_FLASH_MODEL,
|
||||
key: DEFAULT_GEMINI_FLASH_MODEL,
|
||||
},
|
||||
{
|
||||
value: GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
title: `Flash-Lite (${DEFAULT_GEMINI_FLASH_LITE_MODEL})`,
|
||||
description: 'For simple tasks that need to be done quickly',
|
||||
key: GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
title: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
key: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
},
|
||||
],
|
||||
[config],
|
||||
);
|
||||
];
|
||||
|
||||
if (shouldShowPreviewModels) {
|
||||
list.unshift(
|
||||
{
|
||||
value: PREVIEW_GEMINI_MODEL,
|
||||
title: PREVIEW_GEMINI_MODEL,
|
||||
key: PREVIEW_GEMINI_MODEL,
|
||||
},
|
||||
{
|
||||
value: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
title: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
key: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
},
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [shouldShowPreviewModels]);
|
||||
|
||||
const options = view === 'main' ? mainOptions : manualOptions;
|
||||
|
||||
// Calculate the initial index based on the preferred model.
|
||||
const initialIndex = useMemo(
|
||||
() => options.findIndex((option) => option.value === preferredModel),
|
||||
[preferredModel, options],
|
||||
);
|
||||
const initialIndex = useMemo(() => {
|
||||
const idx = options.findIndex((option) => option.value === preferredModel);
|
||||
if (idx !== -1) {
|
||||
return idx;
|
||||
}
|
||||
if (view === 'main') {
|
||||
const manualIdx = options.findIndex((o) => o.value === 'Manual');
|
||||
return manualIdx !== -1 ? manualIdx : 0;
|
||||
}
|
||||
return 0;
|
||||
}, [preferredModel, options, view]);
|
||||
|
||||
// Handle selection internally (Autonomous Dialog).
|
||||
const handleSelect = useCallback(
|
||||
(model: string) => {
|
||||
if (model === 'Manual') {
|
||||
setView('manual');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config) {
|
||||
config.setModel(model);
|
||||
const event = new ModelSlashCommandEvent(model);
|
||||
@@ -96,13 +166,23 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
[config, onClose],
|
||||
);
|
||||
|
||||
const header = config?.getPreviewFeatures()
|
||||
? 'Gemini 3 is now enabled.'
|
||||
: 'Gemini 3 is now available.';
|
||||
let header;
|
||||
let subheader;
|
||||
|
||||
const subheader = config?.getPreviewFeatures()
|
||||
? `To disable Gemini 3, disable "Preview features" in /settings.\nLearn more at https://goo.gle/enable-preview-features\n\nWhen you select Auto or Pro, Gemini CLI will attempt to use ${PREVIEW_GEMINI_MODEL} first, before falling back to ${DEFAULT_GEMINI_MODEL}.`
|
||||
: `To use Gemini 3, enable "Preview features" in /settings.\nLearn more at https://goo.gle/enable-preview-features`;
|
||||
// Do not show any header or subheader since it's already showing preview model
|
||||
// options
|
||||
if (shouldShowPreviewModels) {
|
||||
header = undefined;
|
||||
subheader = undefined;
|
||||
// When a user has the access but has not enabled the preview features.
|
||||
} else if (config?.getHasAccessToPreviewModel()) {
|
||||
header = 'Gemini 3 is now available.';
|
||||
subheader =
|
||||
'Enable "Preview features" in /settings.\nLearn more at https://goo.gle/enable-preview-features';
|
||||
} else {
|
||||
header = 'Gemini 3 is coming soon.';
|
||||
subheader = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -114,11 +194,15 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
>
|
||||
<Text bold>Select Model</Text>
|
||||
|
||||
<Box marginTop={1} marginBottom={1} flexDirection="column">
|
||||
<ThemedGradient>
|
||||
<Text>{header}</Text>
|
||||
</ThemedGradient>
|
||||
<Text>{subheader}</Text>
|
||||
<Box flexDirection="column">
|
||||
{header && (
|
||||
<Box marginTop={1}>
|
||||
<ThemedGradient>
|
||||
<Text>{header}</Text>
|
||||
</ThemedGradient>
|
||||
</Box>
|
||||
)}
|
||||
{subheader && <Text>{subheader}</Text>}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -22,7 +22,7 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
|
||||
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
|
||||
const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session',
|
||||
@@ -36,7 +36,7 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
return render(<ModelStatsDisplay />);
|
||||
return render(<ModelStatsDisplay />, width);
|
||||
};
|
||||
|
||||
describe('<ModelStatsDisplay />', () => {
|
||||
@@ -312,4 +312,60 @@ describe('<ModelStatsDisplay />', () => {
|
||||
expect(output).not.toContain('gemini-2.5-flash');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle models with long names (gemini-3-*-preview) without layout breaking', () => {
|
||||
const { lastFrame } = renderWithMockedStats(
|
||||
{
|
||||
models: {
|
||||
'gemini-3-pro-preview': {
|
||||
api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 2000 },
|
||||
tokens: {
|
||||
input: 1000,
|
||||
prompt: 2000,
|
||||
candidates: 4000,
|
||||
total: 6000,
|
||||
cached: 500,
|
||||
thoughts: 100,
|
||||
tool: 50,
|
||||
},
|
||||
},
|
||||
'gemini-3-flash-preview': {
|
||||
api: { totalRequests: 20, totalErrors: 0, totalLatencyMs: 1000 },
|
||||
tokens: {
|
||||
input: 2000,
|
||||
prompt: 4000,
|
||||
candidates: 8000,
|
||||
total: 12000,
|
||||
cached: 1000,
|
||||
thoughts: 200,
|
||||
tool: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
},
|
||||
80,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('gemini-3-pro-');
|
||||
expect(output).toContain('gemini-3-flash-');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,42 +13,17 @@ import {
|
||||
calculateCacheHitRate,
|
||||
calculateErrorRate,
|
||||
} from '../utils/computeStats.js';
|
||||
import type { ModelMetrics } from '../contexts/SessionContext.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { Table, type Column } from './Table.js';
|
||||
|
||||
const METRIC_COL_WIDTH = 28;
|
||||
const MODEL_COL_WIDTH = 22;
|
||||
|
||||
interface StatRowProps {
|
||||
title: string;
|
||||
values: Array<string | React.ReactElement>;
|
||||
isSubtle?: boolean;
|
||||
interface StatRowData {
|
||||
metric: string;
|
||||
isSection?: boolean;
|
||||
isSubtle?: boolean;
|
||||
// Dynamic keys for model values
|
||||
[key: string]: string | React.ReactNode | boolean | undefined;
|
||||
}
|
||||
|
||||
const StatRow: React.FC<StatRowProps> = ({
|
||||
title,
|
||||
values,
|
||||
isSubtle = false,
|
||||
isSection = false,
|
||||
}) => (
|
||||
<Box>
|
||||
<Box width={METRIC_COL_WIDTH}>
|
||||
<Text
|
||||
bold={isSection}
|
||||
color={isSection ? theme.text.primary : theme.text.link}
|
||||
>
|
||||
{isSubtle ? ` ↳ ${title}` : title}
|
||||
</Text>
|
||||
</Box>
|
||||
{values.map((value, index) => (
|
||||
<Box width={MODEL_COL_WIDTH} key={index}>
|
||||
<Text color={theme.text.primary}>{value}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const ModelStatsDisplay: React.FC = () => {
|
||||
const { stats } = useSessionStats();
|
||||
const { models } = stats.metrics;
|
||||
@@ -73,10 +48,6 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
|
||||
const modelNames = activeModels.map(([name]) => name);
|
||||
|
||||
const getModelValues = (
|
||||
getter: (metrics: ModelMetrics) => string | React.ReactElement,
|
||||
) => activeModels.map(([, metrics]) => getter(metrics));
|
||||
|
||||
const hasThoughts = activeModels.some(
|
||||
([, metrics]) => metrics.tokens.thoughts > 0,
|
||||
);
|
||||
@@ -85,6 +56,152 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
([, metrics]) => metrics.tokens.cached > 0,
|
||||
);
|
||||
|
||||
// Helper to create a row with values for each model
|
||||
const createRow = (
|
||||
metric: string,
|
||||
getValue: (
|
||||
metrics: (typeof activeModels)[0][1],
|
||||
) => string | React.ReactNode,
|
||||
options: { isSection?: boolean; isSubtle?: boolean } = {},
|
||||
): StatRowData => {
|
||||
const row: StatRowData = {
|
||||
metric,
|
||||
isSection: options.isSection,
|
||||
isSubtle: options.isSubtle,
|
||||
};
|
||||
activeModels.forEach(([name, metrics]) => {
|
||||
row[name] = getValue(metrics);
|
||||
});
|
||||
return row;
|
||||
};
|
||||
|
||||
const rows: StatRowData[] = [
|
||||
// API Section
|
||||
{ metric: 'API', isSection: true },
|
||||
createRow('Requests', (m) => m.api.totalRequests.toLocaleString()),
|
||||
createRow('Errors', (m) => {
|
||||
const errorRate = calculateErrorRate(m);
|
||||
return (
|
||||
<Text
|
||||
color={
|
||||
m.api.totalErrors > 0 ? theme.status.error : theme.text.primary
|
||||
}
|
||||
>
|
||||
{m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%)
|
||||
</Text>
|
||||
);
|
||||
}),
|
||||
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
|
||||
|
||||
// Spacer
|
||||
{ metric: '' },
|
||||
|
||||
// Tokens Section
|
||||
{ metric: 'Tokens', isSection: true },
|
||||
createRow('Total', (m) => (
|
||||
<Text color={theme.text.secondary}>
|
||||
{m.tokens.total.toLocaleString()}
|
||||
</Text>
|
||||
)),
|
||||
createRow(
|
||||
'Input',
|
||||
(m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.input.toLocaleString()}
|
||||
</Text>
|
||||
),
|
||||
{ isSubtle: true },
|
||||
),
|
||||
];
|
||||
|
||||
if (hasCached) {
|
||||
rows.push(
|
||||
createRow(
|
||||
'Cache Reads',
|
||||
(m) => {
|
||||
const cacheHitRate = calculateCacheHitRate(m);
|
||||
return (
|
||||
<Text color={theme.text.secondary}>
|
||||
{m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%)
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
{ isSubtle: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasThoughts) {
|
||||
rows.push(
|
||||
createRow(
|
||||
'Thoughts',
|
||||
(m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.thoughts.toLocaleString()}
|
||||
</Text>
|
||||
),
|
||||
{ isSubtle: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasTool) {
|
||||
rows.push(
|
||||
createRow(
|
||||
'Tool',
|
||||
(m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.tool.toLocaleString()}
|
||||
</Text>
|
||||
),
|
||||
{ isSubtle: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
rows.push(
|
||||
createRow(
|
||||
'Output',
|
||||
(m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.candidates.toLocaleString()}
|
||||
</Text>
|
||||
),
|
||||
{ isSubtle: true },
|
||||
),
|
||||
);
|
||||
|
||||
const columns: Array<Column<StatRowData>> = [
|
||||
{
|
||||
key: 'metric',
|
||||
header: 'Metric',
|
||||
width: 28,
|
||||
renderCell: (row) => (
|
||||
<Text
|
||||
bold={row.isSection}
|
||||
color={row.isSection ? theme.text.primary : theme.text.link}
|
||||
>
|
||||
{row.isSubtle ? ` ↳ ${row.metric}` : row.metric}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
...modelNames.map((name) => ({
|
||||
key: name,
|
||||
header: name,
|
||||
flexGrow: 1,
|
||||
renderCell: (row: StatRowData) => {
|
||||
// Don't render anything for section headers in model columns
|
||||
if (row.isSection) return null;
|
||||
const val = row[name];
|
||||
if (val === undefined || val === null) return null;
|
||||
if (typeof val === 'string' || typeof val === 'number') {
|
||||
return <Text color={theme.text.primary}>{val}</Text>;
|
||||
}
|
||||
return val as React.ReactNode;
|
||||
},
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -97,128 +214,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
Model Stats For Nerds
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
|
||||
{/* Header */}
|
||||
<Box>
|
||||
<Box width={METRIC_COL_WIDTH}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Metric
|
||||
</Text>
|
||||
</Box>
|
||||
{modelNames.map((name) => (
|
||||
<Box width={MODEL_COL_WIDTH} key={name}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{name}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderBottom={true}
|
||||
borderTop={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
/>
|
||||
|
||||
{/* API Section */}
|
||||
<StatRow title="API" values={[]} isSection />
|
||||
<StatRow
|
||||
title="Requests"
|
||||
values={getModelValues((m) => m.api.totalRequests.toLocaleString())}
|
||||
/>
|
||||
<StatRow
|
||||
title="Errors"
|
||||
values={getModelValues((m) => {
|
||||
const errorRate = calculateErrorRate(m);
|
||||
return (
|
||||
<Text
|
||||
color={
|
||||
m.api.totalErrors > 0 ? theme.status.error : theme.text.primary
|
||||
}
|
||||
>
|
||||
{m.api.totalErrors.toLocaleString()} ({errorRate.toFixed(1)}%)
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
<StatRow
|
||||
title="Avg Latency"
|
||||
values={getModelValues((m) => {
|
||||
const avgLatency = calculateAverageLatency(m);
|
||||
return formatDuration(avgLatency);
|
||||
})}
|
||||
/>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Tokens Section */}
|
||||
<StatRow title="Tokens" values={[]} isSection />
|
||||
<StatRow
|
||||
title="Total"
|
||||
values={getModelValues((m) => (
|
||||
<Text color={theme.text.secondary}>
|
||||
{m.tokens.total.toLocaleString()}
|
||||
</Text>
|
||||
))}
|
||||
/>
|
||||
<StatRow
|
||||
title="Input"
|
||||
isSubtle
|
||||
values={getModelValues((m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.input.toLocaleString()}
|
||||
</Text>
|
||||
))}
|
||||
/>
|
||||
{hasCached && (
|
||||
<StatRow
|
||||
title="Cache Reads"
|
||||
isSubtle
|
||||
values={getModelValues((m) => {
|
||||
const cacheHitRate = calculateCacheHitRate(m);
|
||||
return (
|
||||
<Text color={theme.text.secondary}>
|
||||
{m.tokens.cached.toLocaleString()} ({cacheHitRate.toFixed(1)}%)
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{hasThoughts && (
|
||||
<StatRow
|
||||
title="Thoughts"
|
||||
isSubtle
|
||||
values={getModelValues((m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.thoughts.toLocaleString()}
|
||||
</Text>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
{hasTool && (
|
||||
<StatRow
|
||||
title="Tool"
|
||||
isSubtle
|
||||
values={getModelValues((m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.tool.toLocaleString()}
|
||||
</Text>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
<StatRow
|
||||
title="Output"
|
||||
isSubtle
|
||||
values={getModelValues((m) => (
|
||||
<Text color={theme.text.primary}>
|
||||
{m.tokens.candidates.toLocaleString()}
|
||||
</Text>
|
||||
))}
|
||||
/>
|
||||
<Table data={rows} columns={columns} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
const keypressCallback = mockedUseKeypress.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
await keypressCallback({
|
||||
keypressCallback({
|
||||
name: 'escape',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
@@ -117,7 +117,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
await onSelect(MultiFolderTrustChoice.NO);
|
||||
onSelect(MultiFolderTrustChoice.NO);
|
||||
});
|
||||
|
||||
expect(mockFinishAddingDirectories).toHaveBeenCalledWith(
|
||||
@@ -145,7 +145,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
await onSelect(MultiFolderTrustChoice.YES);
|
||||
onSelect(MultiFolderTrustChoice.YES);
|
||||
});
|
||||
|
||||
expect(mockAddDirectory).toHaveBeenCalledWith('/path/to/folder1');
|
||||
@@ -166,7 +166,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
await onSelect(MultiFolderTrustChoice.YES_AND_REMEMBER);
|
||||
onSelect(MultiFolderTrustChoice.YES_AND_REMEMBER);
|
||||
});
|
||||
|
||||
expect(mockAddDirectory).toHaveBeenCalledWith('/path/to/folder1');
|
||||
@@ -192,7 +192,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
await onSelect(MultiFolderTrustChoice.NO);
|
||||
onSelect(MultiFolderTrustChoice.NO);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Applying trust settings...');
|
||||
@@ -210,7 +210,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
await onSelect(MultiFolderTrustChoice.YES);
|
||||
onSelect(MultiFolderTrustChoice.YES);
|
||||
});
|
||||
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
@@ -243,7 +243,7 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
await onSelect(MultiFolderTrustChoice.YES);
|
||||
onSelect(MultiFolderTrustChoice.YES);
|
||||
});
|
||||
|
||||
expect(mockAddDirectory).toHaveBeenCalledWith('/path/to/good');
|
||||
|
||||
@@ -138,14 +138,14 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" width="100%">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.status.warning}
|
||||
padding={1}
|
||||
width="100%"
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('ProQuotaDialog', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel={DEFAULT_GEMINI_FLASH_MODEL}
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
fallbackModel={DEFAULT_GEMINI_FLASH_MODEL}
|
||||
message="flash error"
|
||||
isTerminalQuotaError={true} // should not matter
|
||||
onChoice={mockOnChoice}
|
||||
@@ -97,6 +97,38 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render "Keep trying" and "Stop" options when failed model and fallback model are the same', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel={PREVIEW_GEMINI_MODEL}
|
||||
fallbackModel={PREVIEW_GEMINI_MODEL}
|
||||
message="flash error"
|
||||
isTerminalQuotaError={true}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Keep trying',
|
||||
value: 'retry_once',
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render switch, upgrade, and stop options for free tier', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
@@ -137,7 +169,7 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
describe('when it is a capacity error', () => {
|
||||
it('should render keep trying, switch, and stop options', () => {
|
||||
it('should render keep trying and stop options', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
@@ -158,11 +190,6 @@ describe('ProQuotaDialog', () => {
|
||||
value: 'retry_once',
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: 'Switch to gemini-2.5-flash',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{ label: 'Stop', value: 'retry_later', key: 'retry_later' },
|
||||
],
|
||||
}),
|
||||
@@ -263,44 +290,4 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('footer note', () => {
|
||||
it('should show a special note for PREVIEW_GEMINI_MODEL', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel={PREVIEW_GEMINI_MODEL}
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
message=""
|
||||
isTerminalQuotaError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(
|
||||
'Note: We will periodically retry Preview Model to see if congestion has cleared.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show the default note for other models', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message=""
|
||||
isTerminalQuotaError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(
|
||||
'Note: You can always use /model to select a different option.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,7 @@ import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { UserTierId } from '@google/gemini-cli-core';
|
||||
|
||||
interface ProQuotaDialogProps {
|
||||
failedModel: string;
|
||||
@@ -41,11 +36,8 @@ export function ProQuotaDialog({
|
||||
const isPaidTier =
|
||||
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
|
||||
let items;
|
||||
// flash and flash lite don't have options to switch or upgrade.
|
||||
if (
|
||||
failedModel === DEFAULT_GEMINI_FLASH_MODEL ||
|
||||
failedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL
|
||||
) {
|
||||
// Do not provide a fallback option if failed model and fallbackmodel are same.
|
||||
if (failedModel === fallbackModel) {
|
||||
items = [
|
||||
{
|
||||
label: 'Keep trying',
|
||||
@@ -99,11 +91,6 @@ export function ProQuotaDialog({
|
||||
value: 'retry_once' as const,
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: `Switch to ${fallbackModel}`,
|
||||
value: 'retry_always' as const,
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later' as const,
|
||||
@@ -118,19 +105,31 @@ export function ProQuotaDialog({
|
||||
onChoice(choice);
|
||||
};
|
||||
|
||||
// Helper to highlight simple slash commands in the message
|
||||
const renderMessage = (msg: string) => {
|
||||
const parts = msg.split(/(\s+)/);
|
||||
return (
|
||||
<Text>
|
||||
{parts.map((part, index) => {
|
||||
if (part.startsWith('/')) {
|
||||
return (
|
||||
<Text key={index} bold color={theme.text.accent}>
|
||||
{part}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return <Text key={index}>{part}</Text>;
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box borderStyle="round" flexDirection="column" padding={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text>{message}</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>{renderMessage(message)}</Box>
|
||||
<Box marginTop={1} marginBottom={1}>
|
||||
<RadioButtonSelect items={items} onSelect={handleSelect} />
|
||||
</Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{fallbackModel === DEFAULT_GEMINI_MODEL && !isModelNotFoundError
|
||||
? 'Note: We will periodically retry Preview Model to see if congestion has cleared.'
|
||||
: 'Note: You can always use /model to select a different option.'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ export function SettingsDialog({
|
||||
};
|
||||
|
||||
// Height constraint calculations similar to ThemeDialog
|
||||
const DIALOG_PADDING = 4;
|
||||
const DIALOG_PADDING = 5;
|
||||
const SETTINGS_TITLE_HEIGHT = 2; // "Settings" title + spacing
|
||||
const SCROLL_ARROWS_HEIGHT = 2; // Up and down arrows
|
||||
const SPACING_HEIGHT = 1; // Space between settings list and scope
|
||||
|
||||
@@ -24,23 +24,23 @@ describe('ShellConfirmationDialog', () => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls onConfirm with ProceedOnce when "Yes, allow once" is selected', () => {
|
||||
it('calls onConfirm with ProceedOnce when "Allow once" is selected', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ShellConfirmationDialog request={request} />,
|
||||
);
|
||||
const select = lastFrame()!.toString();
|
||||
// Simulate selecting the first option
|
||||
// This is a simplified way to test the selection
|
||||
expect(select).toContain('Yes, allow once');
|
||||
expect(select).toContain('Allow once');
|
||||
});
|
||||
|
||||
it('calls onConfirm with ProceedAlways when "Yes, allow always for this session" is selected', () => {
|
||||
it('calls onConfirm with ProceedAlways when "Allow for this session" is selected', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ShellConfirmationDialog request={request} />,
|
||||
);
|
||||
const select = lastFrame()!.toString();
|
||||
// Simulate selecting the second option
|
||||
expect(select).toContain('Yes, allow always for this session');
|
||||
expect(select).toContain('Allow for this session');
|
||||
});
|
||||
|
||||
it('calls onConfirm with Cancel when "No (esc)" is selected', () => {
|
||||
|
||||
@@ -51,14 +51,14 @@ export const ShellConfirmationDialog: React.FC<
|
||||
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [
|
||||
{
|
||||
label: 'Yes, allow once',
|
||||
label: 'Allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
key: 'Yes, allow once',
|
||||
key: 'Allow once',
|
||||
},
|
||||
{
|
||||
label: 'Yes, allow always for this session',
|
||||
label: 'Allow for this session',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
key: 'Yes, allow always for this session',
|
||||
key: 'Allow for this session',
|
||||
},
|
||||
{
|
||||
label: 'No (esc)',
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Table } from './Table.js';
|
||||
import { Text } from 'ink';
|
||||
|
||||
describe('Table', () => {
|
||||
it('should render headers and data correctly', () => {
|
||||
const columns = [
|
||||
{ key: 'id', header: 'ID', width: 5 },
|
||||
{ key: 'name', header: 'Name', flexGrow: 1 },
|
||||
];
|
||||
const data = [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
];
|
||||
|
||||
const { lastFrame } = render(<Table columns={columns} data={data} />, 100);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('ID');
|
||||
expect(output).toContain('Name');
|
||||
expect(output).toContain('1');
|
||||
expect(output).toContain('Alice');
|
||||
expect(output).toContain('2');
|
||||
expect(output).toContain('Bob');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should support custom cell rendering', () => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'value',
|
||||
header: 'Value',
|
||||
flexGrow: 1,
|
||||
renderCell: (item: { value: number }) => (
|
||||
<Text color="green">{item.value * 2}</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
const data = [{ value: 10 }];
|
||||
|
||||
const { lastFrame } = render(<Table columns={columns} data={data} />, 100);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('20');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle undefined values gracefully', () => {
|
||||
const columns = [{ key: 'name', header: 'Name', flexGrow: 1 }];
|
||||
const data: Array<{ name: string | undefined }> = [{ name: undefined }];
|
||||
const { lastFrame } = render(<Table columns={columns} data={data} />, 100);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('undefined');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
export interface Column<T> {
|
||||
key: string;
|
||||
header: React.ReactNode;
|
||||
width?: number;
|
||||
flexGrow?: number;
|
||||
flexShrink?: number;
|
||||
flexBasis?: number | string;
|
||||
renderCell?: (item: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface TableProps<T> {
|
||||
data: T[];
|
||||
columns: Array<Column<T>>;
|
||||
}
|
||||
|
||||
export function Table<T>({ data, columns }: TableProps<T>) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* Header */}
|
||||
<Box flexDirection="row">
|
||||
{columns.map((col, index) => (
|
||||
<Box
|
||||
key={`header-${index}`}
|
||||
width={col.width}
|
||||
flexGrow={col.flexGrow}
|
||||
flexShrink={col.flexShrink}
|
||||
flexBasis={col.flexBasis ?? (col.width ? undefined : 0)}
|
||||
paddingRight={1}
|
||||
>
|
||||
{typeof col.header === 'string' ? (
|
||||
<Text bold color={theme.text.primary}>
|
||||
{col.header}
|
||||
</Text>
|
||||
) : (
|
||||
col.header
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderBottom={true}
|
||||
borderTop={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
marginBottom={0}
|
||||
/>
|
||||
|
||||
{/* Rows */}
|
||||
{data.map((item, rowIndex) => (
|
||||
<Box key={`row-${rowIndex}`} flexDirection="row">
|
||||
{columns.map((col, colIndex) => (
|
||||
<Box
|
||||
key={`cell-${rowIndex}-${colIndex}`}
|
||||
width={col.width}
|
||||
flexGrow={col.flexGrow}
|
||||
flexShrink={col.flexShrink}
|
||||
flexBasis={col.flexBasis ?? (col.width ? undefined : 0)}
|
||||
paddingRight={1}
|
||||
>
|
||||
{col.renderCell ? (
|
||||
col.renderCell(item)
|
||||
) : (
|
||||
<Text color={theme.text.primary}>
|
||||
{String((item as Record<string, unknown>)[col.key])}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user