mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 16:50:59 -07:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1f3228d29 | |||
| eb17d94e91 | |||
| 9f1623688d | |||
| 7f066fd3a7 | |||
| 6d36219e55 | |||
| 5cedd28733 | |||
| ba3e1ed1c7 | |||
| 7c2cdb225a | |||
| a91f4e692a | |||
| 9bb175a506 | |||
| 0b169e9867 | |||
| 553ae3ad69 | |||
| b2265cfde4 | |||
| bee1267e2a | |||
| 30065c51fb | |||
| 065e69a12b | |||
| 519a0e617a | |||
| 0465de303a | |||
| beaa134f0e | |||
| 47f4a3e50e | |||
| 3787c71d15 | |||
| 9e09db1ddb | |||
| 25ae1a1b54 | |||
| 65accca296 | |||
| 72f0733d98 | |||
| 4d4e16cc8c | |||
| 370b165769 | |||
| 899e8a9edc | |||
| 4dca5c27c6 | |||
| 06c7dc561e | |||
| f54eaa1713 | |||
| 6807a2fa28 | |||
| 9aeeb69ab0 | |||
| 0a77999d73 | |||
| 9035ca32bc | |||
| ef3146eed6 | |||
| ffbecd4eb4 | |||
| 2c09785748 | |||
| 130b596c80 |
@@ -0,0 +1,22 @@
|
||||
description = "Analyze agent behavior and suggest high-level improvements to system prompts."
|
||||
prompt = """
|
||||
# Prompt Engineering Analysis
|
||||
|
||||
You are a world-class prompt engineer and an expert AI engineer at the top of your class. Your goal is to analyze a specific agent behavior or failure and suggest high-level improvements to the system instructions.
|
||||
|
||||
**Observed Behavior / Issue:**
|
||||
{{args}}
|
||||
|
||||
**Reference Context:**
|
||||
- System Prompt Logic: @packages/core/src/core/prompts.ts
|
||||
|
||||
### Task
|
||||
1. **Analyze the Failure:** Review the provided behavior and identify the underlying instructional causes. Use the `/introspect` command output if provided by the user.
|
||||
2. **Strategic Insights:** Share your technical view of the issue. Focus on the "why" and identify any instructional inertia or ambiguity.
|
||||
3. **Propose Improvements:** Suggest high-level changes to the system instructions to prevent this behavior.
|
||||
|
||||
### Principles
|
||||
- **Avoid Hyper-scoping:** Do not create narrow solutions for specific scenarios; aim for generalized improvements that handle classes of behavior.
|
||||
- **Avoid Specific Examples in Suggestions:** Keep the proposed instructions semantic and high-level to prevent the agent from over-indexing on specific cases.
|
||||
- **Maintain Operational Rigor:** Ensure suggestions do not compromise safety, security, or the quality of the agent's work.
|
||||
"""
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"experimental": {
|
||||
"skills": true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
---
|
||||
name: docs-writer
|
||||
description:
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
a natural tone.
|
||||
- **Simple vocabulary:** Use common words. Define technical terms when
|
||||
necessary.
|
||||
- **Conciseness:** Keep sentences short and focused.
|
||||
- **Conciseness:** Keep sentences short and focused, but don't omit helpful
|
||||
information.
|
||||
- **"Please":** Avoid using the word "please."
|
||||
|
||||
## IV. Procedures and steps
|
||||
|
||||
@@ -28,7 +28,7 @@ body:
|
||||
id: 'info'
|
||||
attributes:
|
||||
label: 'Client information'
|
||||
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux).'
|
||||
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux). Note that this output contains your email address. Consider removing it before submitting.'
|
||||
value: |-
|
||||
<details>
|
||||
<summary>Client Information</summary>
|
||||
|
||||
@@ -72,7 +72,8 @@ powerful tool for developers.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Always use the `docs-writer` skill when you are asked to write, edit, or
|
||||
review any documentation.
|
||||
- Documentation is located in the `docs/` directory.
|
||||
- Suggest documentation updates when code changes render existing documentation
|
||||
obsolete or incomplete.
|
||||
- Located in the `docs/` directory.
|
||||
- Use the `docs-writer` skill.
|
||||
|
||||
@@ -18,6 +18,23 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.26.0 - 2026-01-27
|
||||
|
||||
- **Agents and Skills:** We've introduced a new `skill-creator` skill
|
||||
([#16394](https://github.com/google-gemini/gemini-cli/pull/16394) by
|
||||
@NTaylorMullen), enabled agent skills by default, and added a generalist agent
|
||||
to improve task routing
|
||||
([#16638](https://github.com/google-gemini/gemini-cli/pull/16638) by
|
||||
@joshualitt).
|
||||
- **UI/UX Improvements:** You can now "Rewind" through your conversation history
|
||||
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by @Adib234)
|
||||
and use a new `/introspect` command for debugging.
|
||||
- **Core and Scheduler Refactoring:** The core scheduler has been significantly
|
||||
refactored to improve performance and reliability
|
||||
([#16895](https://github.com/google-gemini/gemini-cli/pull/16895) by
|
||||
@abhipatel12), and numerous performance and stability fixes have been
|
||||
included.
|
||||
|
||||
## Announcements: v0.25.0 - 2026-01-20
|
||||
|
||||
- **Skills and Agents Improvements:** We've enhanced the `activate_skill` tool,
|
||||
|
||||
+315
-347
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.25.0
|
||||
# Latest stable release: v0.26.0
|
||||
|
||||
Released: January 20, 2026
|
||||
Released: January 27, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,360 +11,328 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Skills and Agents Improvements:** Enhanced `activate_skill` tool, new
|
||||
`pr-creator` skill, default enablement of skills, improved `cli_help` agent,
|
||||
and a new `/agents refresh` command.
|
||||
- **UI/UX Refinements:** Transparent feedback for skills, ability to switch
|
||||
focus between shell and input with Tab, and dynamic terminal tab titles.
|
||||
- **Core Functionality & Performance:** Support for built-in agent skills,
|
||||
refined Gemini 3 system instructions, caching ignore instances for
|
||||
performance, and improved retry mechanisms.
|
||||
- **Bug Fixes and Stability:** Numerous bug fixes across the CLI, core, and
|
||||
workflows, including issues with subagent delegation, unicode character
|
||||
crashes, and sticky header regressions.
|
||||
- **Enhanced Agent and Skill Capabilities:** This release introduces the new
|
||||
`skill-creator` built-in skill, enables Agent Skills by default, and adds a
|
||||
generalist agent to improve task routing. Security for skill installation has
|
||||
also been enhanced with new consent prompts.
|
||||
- **Improved UI and UX:** A new "Rewind" feature lets you walk back through
|
||||
conversation history. We've also added an `/introspect` command for debugging
|
||||
and unified various shell confirmation dialogs for a more consistent user
|
||||
experience.
|
||||
- **Core Stability and Performance:** This release includes significant
|
||||
performance improvements, including a fix for PDF token estimation,
|
||||
optimizations for large inputs, and prevention of OOM crashes. Key memory
|
||||
management components like `LRUCache` have also been updated.
|
||||
- **Scheduler and Policy Refactoring:** The core tool scheduler has been
|
||||
decoupled into distinct orchestration, policy, and confirmation components,
|
||||
and we've added an experimental event-driven scheduler to improve performance
|
||||
and reliability.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- feat(core): improve activate_skill tool and use lowercase XML tags by
|
||||
@NTaylorMullen in
|
||||
[#16009](https://github.com/google-gemini/gemini-cli/pull/16009)
|
||||
- Add initiation method telemetry property by @gundermanc in
|
||||
[#15818](https://github.com/google-gemini/gemini-cli/pull/15818)
|
||||
- chore(release): bump version to 0.25.0-nightly.20260107.59a18e710 by
|
||||
- fix: PDF token estimation (#16494) by @korade-krushna in
|
||||
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
|
||||
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
|
||||
@gemini-cli-robot in
|
||||
[#16048](https://github.com/google-gemini/gemini-cli/pull/16048)
|
||||
- Hx support by @kevinfjiang in
|
||||
[#16032](https://github.com/google-gemini/gemini-cli/pull/16032)
|
||||
- [Skills] Foundation: Centralize management logic and feedback rendering by
|
||||
@NTaylorMullen in
|
||||
[#15952](https://github.com/google-gemini/gemini-cli/pull/15952)
|
||||
- Introduce GEMINI_CLI_HOME for strict test isolation by @NTaylorMullen in
|
||||
[#15907](https://github.com/google-gemini/gemini-cli/pull/15907)
|
||||
- [Skills] Multi-scope skill enablement and shadowing fix by @NTaylorMullen in
|
||||
[#15953](https://github.com/google-gemini/gemini-cli/pull/15953)
|
||||
- policy: extract legacy policy from core tool scheduler to policy engine by
|
||||
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
|
||||
- docs: clarify F12 to open debug console by @jackwotherspoon in
|
||||
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
|
||||
- docs: Remove .md extension from internal links in architecture.md by
|
||||
@medic-code in
|
||||
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
|
||||
- Add an experimental setting for extension config by @chrstnb in
|
||||
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
|
||||
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
|
||||
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
|
||||
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
|
||||
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
|
||||
- prefactor: add rootCommands as array so it can be used for policy parsing by
|
||||
@abhipatel12 in
|
||||
[#15902](https://github.com/google-gemini/gemini-cli/pull/15902)
|
||||
- Enhance TestRig with process management and timeouts by @NTaylorMullen in
|
||||
[#15908](https://github.com/google-gemini/gemini-cli/pull/15908)
|
||||
- Update troubleshooting doc for UNABLE_TO_GET_ISSUER_CERT_LOCALLY by @sehoon38
|
||||
in [#16069](https://github.com/google-gemini/gemini-cli/pull/16069)
|
||||
- Add keytar to dependencies by @chrstnb in
|
||||
[#15928](https://github.com/google-gemini/gemini-cli/pull/15928)
|
||||
- Simplify extension settings command by @chrstnb in
|
||||
[#16001](https://github.com/google-gemini/gemini-cli/pull/16001)
|
||||
- feat(admin): implement extensions disabled by @skeshive in
|
||||
[#16024](https://github.com/google-gemini/gemini-cli/pull/16024)
|
||||
- Core data structure updates for Rewind functionality by @Adib234 in
|
||||
[#15714](https://github.com/google-gemini/gemini-cli/pull/15714)
|
||||
- feat(hooks): simplify hook firing with HookSystem wrapper methods by @ved015
|
||||
in [#15982](https://github.com/google-gemini/gemini-cli/pull/15982)
|
||||
- Add exp.gws_experiment field to LogEventEntry by @gsquared94 in
|
||||
[#16062](https://github.com/google-gemini/gemini-cli/pull/16062)
|
||||
- Revert "feat(admin): implement extensions disabled" by @chrstnb in
|
||||
[#16082](https://github.com/google-gemini/gemini-cli/pull/16082)
|
||||
- feat(core): Decouple enabling hooks UI from subsystem. by @joshualitt in
|
||||
[#16074](https://github.com/google-gemini/gemini-cli/pull/16074)
|
||||
- docs: add docs for hooks + extensions by @abhipatel12 in
|
||||
[#16073](https://github.com/google-gemini/gemini-cli/pull/16073)
|
||||
- feat(core): Preliminary changes for subagent model routing. by @joshualitt in
|
||||
[#16035](https://github.com/google-gemini/gemini-cli/pull/16035)
|
||||
- Optimize CI workflow: Parallelize jobs and cache linters by @NTaylorMullen in
|
||||
[#16054](https://github.com/google-gemini/gemini-cli/pull/16054)
|
||||
- Add option to fallback for capacity errors in ProQuotaDi… by @sehoon38 in
|
||||
[#16050](https://github.com/google-gemini/gemini-cli/pull/16050)
|
||||
- feat: add confirmation details support + jsonrpc vs http rest support by
|
||||
@adamfweidman in
|
||||
[#16079](https://github.com/google-gemini/gemini-cli/pull/16079)
|
||||
- fix(workflows): fix and limit labels for pr-triage.sh script by @jacob314 in
|
||||
[#16096](https://github.com/google-gemini/gemini-cli/pull/16096)
|
||||
- Fix and rename introspection agent -> cli help agent by @scidomino in
|
||||
[#16097](https://github.com/google-gemini/gemini-cli/pull/16097)
|
||||
- Docs: Changelogs update 20260105 by @jkcinouye in
|
||||
[#15937](https://github.com/google-gemini/gemini-cli/pull/15937)
|
||||
- enable cli_help agent by default by @scidomino in
|
||||
[#16100](https://github.com/google-gemini/gemini-cli/pull/16100)
|
||||
- Optimize json-output tests with mock responses by @NTaylorMullen in
|
||||
[#16102](https://github.com/google-gemini/gemini-cli/pull/16102)
|
||||
- Fix CI for forks by @scidomino in
|
||||
[#16113](https://github.com/google-gemini/gemini-cli/pull/16113)
|
||||
- Reduce nags about PRs that reference issues but don't fix them. by @jacob314
|
||||
in [#16112](https://github.com/google-gemini/gemini-cli/pull/16112)
|
||||
- feat(cli): add filepath autosuggestion after slash commands by @jasmeetsb in
|
||||
[#14738](https://github.com/google-gemini/gemini-cli/pull/14738)
|
||||
- Add upgrade option for paid users by @cayden-google in
|
||||
[#15978](https://github.com/google-gemini/gemini-cli/pull/15978)
|
||||
- [Skills] UX Polishing: Transparent feedback and CLI refinements by
|
||||
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
|
||||
- remove unnecessary \x7f key bindings by @scidomino in
|
||||
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
|
||||
- docs(skills): use body-file in pr-creator skill for better reliability by
|
||||
@abhipatel12 in
|
||||
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
|
||||
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
|
||||
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
|
||||
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
|
||||
@NTaylorMullen in
|
||||
[#15954](https://github.com/google-gemini/gemini-cli/pull/15954)
|
||||
- Polish: Move 'Failed to load skills' warning to debug logs by @NTaylorMullen
|
||||
in [#16142](https://github.com/google-gemini/gemini-cli/pull/16142)
|
||||
- feat(cli): export chat history in /bug and prefill GitHub issue by
|
||||
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
|
||||
- chore(automation): remove automated PR size and complexity labeler by
|
||||
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
|
||||
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
|
||||
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
|
||||
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
|
||||
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
|
||||
- Simplify paste handling by @scidomino in
|
||||
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
|
||||
- chore(automation): improve scheduled issue triage discovery and throughput by
|
||||
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
|
||||
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
|
||||
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
|
||||
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
|
||||
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
|
||||
- Remove unused rewind key binding by @scidomino in
|
||||
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
|
||||
- Remove sequence binding by @scidomino in
|
||||
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
|
||||
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
|
||||
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
|
||||
- chore: update dependabot configuration by @cosmopax in
|
||||
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
|
||||
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
|
||||
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
|
||||
- Enable & disable agents by @sehoon38 in
|
||||
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
|
||||
- cleanup: Improve keybindings by @scidomino in
|
||||
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
|
||||
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
|
||||
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
|
||||
- feat(plan): add experimental plan flag by @jerop in
|
||||
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
|
||||
- feat(cli): add security consent prompts for skill installation by
|
||||
@NTaylorMullen in
|
||||
[#16115](https://github.com/google-gemini/gemini-cli/pull/16115)
|
||||
- bug(core): fix issue with overrides to bases. by @joshualitt in
|
||||
[#15255](https://github.com/google-gemini/gemini-cli/pull/15255)
|
||||
- enableInteractiveShell for external tooling relying on a2a server by
|
||||
@DavidAPierce in
|
||||
[#16080](https://github.com/google-gemini/gemini-cli/pull/16080)
|
||||
- Reapply "feat(admin): implement extensions disabled" (#16082) by @skeshive in
|
||||
[#16109](https://github.com/google-gemini/gemini-cli/pull/16109)
|
||||
- bug(core): Fix spewie getter in hookTranslator.ts by @joshualitt in
|
||||
[#16108](https://github.com/google-gemini/gemini-cli/pull/16108)
|
||||
- feat(hooks): add mcp_context to BeforeTool and AfterTool hook inputs by @vrv
|
||||
in [#15656](https://github.com/google-gemini/gemini-cli/pull/15656)
|
||||
- Add extension linking capabilities in cli by @kevinjwang1 in
|
||||
[#16040](https://github.com/google-gemini/gemini-cli/pull/16040)
|
||||
- Update the page's title to be consistent and show in site. by @kschaab in
|
||||
[#16174](https://github.com/google-gemini/gemini-cli/pull/16174)
|
||||
- docs: correct typo in bufferFastReturn JSDoc ("accomodate" → "accommodate") by
|
||||
@minglu7 in [#16056](https://github.com/google-gemini/gemini-cli/pull/16056)
|
||||
- fix: typo in MCP servers settings description by @alphanota in
|
||||
[#15929](https://github.com/google-gemini/gemini-cli/pull/15929)
|
||||
- fix: yolo should auto allow redirection by @abhipatel12 in
|
||||
[#16183](https://github.com/google-gemini/gemini-cli/pull/16183)
|
||||
- fix(cli): disableYoloMode shouldn't enforce default approval mode against args
|
||||
by @psinha40898 in
|
||||
[#16155](https://github.com/google-gemini/gemini-cli/pull/16155)
|
||||
- feat: add native Sublime Text support to IDE detection by @phreakocious in
|
||||
[#16083](https://github.com/google-gemini/gemini-cli/pull/16083)
|
||||
- refactor(core): extract ToolModificationHandler from scheduler by @abhipatel12
|
||||
in [#16118](https://github.com/google-gemini/gemini-cli/pull/16118)
|
||||
- Add support for Antigravity terminal in terminal setup utility by @raky291 in
|
||||
[#16051](https://github.com/google-gemini/gemini-cli/pull/16051)
|
||||
- feat(core): Wire up model routing to subagents. by @joshualitt in
|
||||
[#16043](https://github.com/google-gemini/gemini-cli/pull/16043)
|
||||
- feat(cli): add /agents slash command to list available agents by @adamfweidman
|
||||
in [#16182](https://github.com/google-gemini/gemini-cli/pull/16182)
|
||||
- docs(cli): fix includeDirectories nesting in configuration.md by @maru0804 in
|
||||
[#15067](https://github.com/google-gemini/gemini-cli/pull/15067)
|
||||
- feat: implement file system reversion utilities for rewind by @Adib234 in
|
||||
[#15715](https://github.com/google-gemini/gemini-cli/pull/15715)
|
||||
- Always enable redaction in GitHub actions. by @gundermanc in
|
||||
[#16200](https://github.com/google-gemini/gemini-cli/pull/16200)
|
||||
- fix: remove unsupported 'enabled' key from workflow config by @Han5991 in
|
||||
[#15611](https://github.com/google-gemini/gemini-cli/pull/15611)
|
||||
- docs: Remove redundant and duplicate documentation files by @liqzheng in
|
||||
[#14699](https://github.com/google-gemini/gemini-cli/pull/14699)
|
||||
- docs: shorten run command and use published version by @dsherret in
|
||||
[#16172](https://github.com/google-gemini/gemini-cli/pull/16172)
|
||||
- test(command-registry): increase initialization test timeout by @wszqkzqk in
|
||||
[#15979](https://github.com/google-gemini/gemini-cli/pull/15979)
|
||||
- Ensure TERM is set to xterm-256color by @falouu in
|
||||
[#15828](https://github.com/google-gemini/gemini-cli/pull/15828)
|
||||
- The telemetry.js script should handle paths that contain spaces by @JohnJAS in
|
||||
[#12078](https://github.com/google-gemini/gemini-cli/pull/12078)
|
||||
- ci: guard links workflow from running on forks by @wtanaka in
|
||||
[#15461](https://github.com/google-gemini/gemini-cli/pull/15461)
|
||||
- ci: guard nightly release workflow from running on forks by @wtanaka in
|
||||
[#15463](https://github.com/google-gemini/gemini-cli/pull/15463)
|
||||
- Support @ suggestions for subagenets by @sehoon38 in
|
||||
[#16201](https://github.com/google-gemini/gemini-cli/pull/16201)
|
||||
- feat(hooks): Support explicit stop and block execution control in model hooks
|
||||
by @SandyTao520 in
|
||||
[#15947](https://github.com/google-gemini/gemini-cli/pull/15947)
|
||||
- Refine Gemini 3 system instructions to reduce model verbosity by
|
||||
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
|
||||
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
|
||||
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
|
||||
- chore(automation): ensure status/need-triage is applied and never cleared
|
||||
automatically by @bdmorgan in
|
||||
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
|
||||
- fix: Handle colons in skill description frontmatter by @maru0804 in
|
||||
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
|
||||
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
|
||||
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
|
||||
- feat(skills): add conflict detection and warnings for skill overrides by
|
||||
@NTaylorMullen in
|
||||
[#16139](https://github.com/google-gemini/gemini-cli/pull/16139)
|
||||
- chore: clean up unused models and use consts by @sehoon38 in
|
||||
[#16246](https://github.com/google-gemini/gemini-cli/pull/16246)
|
||||
- Always enable bracketed paste by @scidomino in
|
||||
[#16179](https://github.com/google-gemini/gemini-cli/pull/16179)
|
||||
- refactor: migrate clearCommand hook calls to HookSystem by @ved015 in
|
||||
[#16157](https://github.com/google-gemini/gemini-cli/pull/16157)
|
||||
- refactor: migrate app containter hook calls to hook system by @ishaanxgupta in
|
||||
[#16161](https://github.com/google-gemini/gemini-cli/pull/16161)
|
||||
- Show settings source in extensions lists by @chrstnb in
|
||||
[#16207](https://github.com/google-gemini/gemini-cli/pull/16207)
|
||||
- feat(skills): add pr-creator skill and enable skills by @NTaylorMullen in
|
||||
[#16232](https://github.com/google-gemini/gemini-cli/pull/16232)
|
||||
- fix: handle Shift+Space in Kitty keyboard protocol terminals by @tt-a1i in
|
||||
[#15767](https://github.com/google-gemini/gemini-cli/pull/15767)
|
||||
- feat(core, ui): Add /agents refresh command. by @joshualitt in
|
||||
[#16204](https://github.com/google-gemini/gemini-cli/pull/16204)
|
||||
- feat(core): add local experiments override via GEMINI_EXP by @kevin-ramdass in
|
||||
[#16181](https://github.com/google-gemini/gemini-cli/pull/16181)
|
||||
- feat(ui): reduce home directory warning noise and add opt-out setting by
|
||||
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
|
||||
- feat(scheduler): add SchedulerStateManager for reactive tool state by
|
||||
@abhipatel12 in
|
||||
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
|
||||
- chore(automation): enforce 'help wanted' label permissions and update
|
||||
guidelines by @bdmorgan in
|
||||
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
|
||||
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
|
||||
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
|
||||
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
|
||||
@gemini-cli-robot in
|
||||
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
|
||||
- fix(automation): correct status/need-issue label matching wildcard by
|
||||
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
|
||||
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
|
||||
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
|
||||
- Add links to supported locations and minor fixes by @g-samroberts in
|
||||
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
|
||||
- feat(policy): add source tracking to policy rules by @allenhutchison in
|
||||
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
|
||||
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
|
||||
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
|
||||
- Make merged settings non-nullable and fix all lints related to that. by
|
||||
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
|
||||
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
|
||||
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
|
||||
- Replace relative paths to fix website build by @chrstnb in
|
||||
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
|
||||
- Restricting to localhost by @cocosheng-g in
|
||||
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
|
||||
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
|
||||
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
|
||||
- fix(automation): robust label enforcement with permission checks by @bdmorgan
|
||||
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
|
||||
- fix(cli): prevent OOM crash by limiting file search traversal and adding
|
||||
timeout by @galz10 in
|
||||
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
|
||||
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
|
||||
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
|
||||
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
|
||||
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
|
||||
- Add support for running available commands prior to MCP servers loading by
|
||||
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
|
||||
- feat(plan): add experimental 'plan' approval mode by @jerop in
|
||||
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
|
||||
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
|
||||
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
|
||||
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
|
||||
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
|
||||
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
|
||||
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
|
||||
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
|
||||
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
|
||||
- skip simple-mcp-server.test.ts by @scidomino in
|
||||
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
|
||||
- Steer outer agent to use expert subagents when present by @gundermanc in
|
||||
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
|
||||
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
|
||||
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
|
||||
- cleanup: Organize key bindings by @scidomino in
|
||||
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
|
||||
- feat(core): Add generalist agent. by @joshualitt in
|
||||
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
|
||||
- perf(ui): optimize text buffer and highlighting for large inputs by
|
||||
@NTaylorMullen in
|
||||
[#16229](https://github.com/google-gemini/gemini-cli/pull/16229)
|
||||
- refactor: migrate chatCompressionService to use HookSystem by @ved015 in
|
||||
[#16259](https://github.com/google-gemini/gemini-cli/pull/16259)
|
||||
- fix: properly use systemMessage for hooks in UI by @jackwotherspoon in
|
||||
[#16250](https://github.com/google-gemini/gemini-cli/pull/16250)
|
||||
- Infer modifyOtherKeys support by @scidomino in
|
||||
[#16270](https://github.com/google-gemini/gemini-cli/pull/16270)
|
||||
- feat(core): Cache ignore instances for performance by @EricRahm in
|
||||
[#16185](https://github.com/google-gemini/gemini-cli/pull/16185)
|
||||
- feat: apply remote admin settings (no-op) by @skeshive in
|
||||
[#16106](https://github.com/google-gemini/gemini-cli/pull/16106)
|
||||
- Autogenerate docs/cli/settings.md docs/getting-started/configuration.md was
|
||||
already autogenerated but settings.md was not. by @jacob314 in
|
||||
[#14408](https://github.com/google-gemini/gemini-cli/pull/14408)
|
||||
- refactor(config): remove legacy V1 settings migration logic by @galz10 in
|
||||
[#16252](https://github.com/google-gemini/gemini-cli/pull/16252)
|
||||
- Fix an issue where the agent stops prematurely by @gundermanc in
|
||||
[#16269](https://github.com/google-gemini/gemini-cli/pull/16269)
|
||||
- Update system prompt to prefer non-interactive commands by @NTaylorMullen in
|
||||
[#16117](https://github.com/google-gemini/gemini-cli/pull/16117)
|
||||
- Update ink version to 6.4.7 by @jacob314 in
|
||||
[#16284](https://github.com/google-gemini/gemini-cli/pull/16284)
|
||||
- Support for Built-in Agent Skills by @NTaylorMullen in
|
||||
[#16045](https://github.com/google-gemini/gemini-cli/pull/16045)
|
||||
- fix(skills): remove "Restart required" message from non-interactive commands
|
||||
by @NTaylorMullen in
|
||||
[#16307](https://github.com/google-gemini/gemini-cli/pull/16307)
|
||||
- remove unused sessionHookTriggers and exports by @ved015 in
|
||||
[#16324](https://github.com/google-gemini/gemini-cli/pull/16324)
|
||||
- Triage action cleanup by @bdmorgan in
|
||||
[#16319](https://github.com/google-gemini/gemini-cli/pull/16319)
|
||||
- fix: Add event-driven trigger to issue triage workflow by @bdmorgan in
|
||||
[#16334](https://github.com/google-gemini/gemini-cli/pull/16334)
|
||||
- fix(workflows): resolve triage workflow failures and actionlint errors by
|
||||
@bdmorgan in [#16338](https://github.com/google-gemini/gemini-cli/pull/16338)
|
||||
- docs: add note about experimental hooks by @abhipatel12 in
|
||||
[#16337](https://github.com/google-gemini/gemini-cli/pull/16337)
|
||||
- feat(cli): implement passive activity logger for session analysis by
|
||||
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
|
||||
- fix(core): fix PTY descriptor shell leak by @galz10 in
|
||||
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
|
||||
- feat(plan): enforce strict read-only policy and halt execution on violation by
|
||||
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
|
||||
- remove need-triage label from bug_report template by @sehoon38 in
|
||||
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
|
||||
- fix(core): truncate large telemetry log entries by @sehoon38 in
|
||||
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
|
||||
- docs(extensions): add Agent Skills support and mark feature as experimental by
|
||||
@NTaylorMullen in
|
||||
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
|
||||
- fix(core): surface warnings for invalid hook event names in configuration
|
||||
(#16788) by @sehoon38 in
|
||||
[#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
|
||||
- feat(plan): remove read_many_files from approval mode policies by @jerop in
|
||||
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
|
||||
- feat(admin): implement admin controls polling and restart prompt by @skeshive
|
||||
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
|
||||
- Remove LRUCache class migrating to mnemoist by @jacob314 in
|
||||
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
|
||||
- feat(settings): rename negative settings to positive naming (disable* ->
|
||||
enable*) by @afarber in
|
||||
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
|
||||
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
|
||||
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
|
||||
- feat(agent): enable agent skills by default by @NTaylorMullen in
|
||||
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
|
||||
- refactor(core): foundational truncation refactoring and token estimation
|
||||
optimization by @NTaylorMullen in
|
||||
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
|
||||
- fix(hooks): enable /hooks disable to reliably stop single hooks by
|
||||
@abhipatel12 in
|
||||
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
|
||||
- Don't commit unless user asks us to. by @gundermanc in
|
||||
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
|
||||
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
|
||||
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
|
||||
- fix: Show experiment values in settings UI for compressionThreshold by
|
||||
@ishaanxgupta in
|
||||
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
|
||||
- feat(cli): replace relative keyboard shortcuts link with web URL by
|
||||
@imaliabbas in
|
||||
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
|
||||
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
|
||||
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
|
||||
- Delete rewind documentation for now by @Adib234 in
|
||||
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
|
||||
- Stabilize skill-creator CI and package format by @NTaylorMullen in
|
||||
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
|
||||
- Stabilize the git evals by @gundermanc in
|
||||
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
|
||||
- fix(core): attempt compression before context overflow check by @NTaylorMullen
|
||||
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
|
||||
- Fix inverted logic. by @gundermanc in
|
||||
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
|
||||
- chore(scripts): add duplicate issue closer script and fix lint errors by
|
||||
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
|
||||
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
|
||||
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
|
||||
- fix(cli): correct Homebrew installation detection by @kij in
|
||||
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
|
||||
- Demote git evals to nightly run. by @gundermanc in
|
||||
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
|
||||
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
|
||||
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
|
||||
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
|
||||
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
|
||||
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
|
||||
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
|
||||
- refactor(core): decouple scheduler into orchestration, policy, and
|
||||
confirmation by @abhipatel12 in
|
||||
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
|
||||
- feat: add /introspect slash command by @NTaylorMullen in
|
||||
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
|
||||
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
|
||||
@abhipatel12 in
|
||||
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
|
||||
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
|
||||
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
|
||||
- fix(workflows): use author_association for maintainer check by @bdmorgan in
|
||||
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
|
||||
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
|
||||
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
|
||||
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
|
||||
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
|
||||
- feat(core): improve shell redirection transparency and security by
|
||||
@NTaylorMullen in
|
||||
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
|
||||
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
|
||||
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
|
||||
- docs(themes): remove unsupported DiffModified color key by @jw409 in
|
||||
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
|
||||
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
|
||||
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
|
||||
- feat(core): enhanced anchored iterative context compression with
|
||||
self-verification by @rmedranollamas in
|
||||
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
|
||||
- Fix mcp instructions by @chrstnb in
|
||||
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
|
||||
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
|
||||
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
|
||||
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
|
||||
by @skeshive in
|
||||
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
|
||||
- Test coverage for hook exit code cases by @gundermanc in
|
||||
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
|
||||
- Revert "Revert "Update extension examples"" by @chrstnb in
|
||||
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
|
||||
- fix(core): Provide compact, actionable errors for agent delegation failures by
|
||||
@SandyTao520 in
|
||||
[#15829](https://github.com/google-gemini/gemini-cli/pull/15829)
|
||||
- feat(cli): add /chat debug command for nightly builds by @abhipatel12 in
|
||||
[#16339](https://github.com/google-gemini/gemini-cli/pull/16339)
|
||||
- style: format pr-creator skill by @NTaylorMullen in
|
||||
[#16381](https://github.com/google-gemini/gemini-cli/pull/16381)
|
||||
- feat(cli): Hooks enable-all/disable-all feature with dynamic status by
|
||||
@AbdulTawabJuly in
|
||||
[#15552](https://github.com/google-gemini/gemini-cli/pull/15552)
|
||||
- fix(core): ensure silent local subagent delegation while allowing remote
|
||||
confirmation by @adamfweidman in
|
||||
[#16395](https://github.com/google-gemini/gemini-cli/pull/16395)
|
||||
- Markdown w/ Frontmatter Agent Parser by @sehoon38 in
|
||||
[#16094](https://github.com/google-gemini/gemini-cli/pull/16094)
|
||||
- Fix crash on unicode character by @chrstnb in
|
||||
[#16420](https://github.com/google-gemini/gemini-cli/pull/16420)
|
||||
- Attempt to resolve OOM w/ useMemo on history items by @chrstnb in
|
||||
[#16424](https://github.com/google-gemini/gemini-cli/pull/16424)
|
||||
- fix(core): ensure sub-agent schema and prompt refresh during runtime by
|
||||
@adamfweidman in
|
||||
[#16409](https://github.com/google-gemini/gemini-cli/pull/16409)
|
||||
- Update extension examples by @chrstnb in
|
||||
[#16274](https://github.com/google-gemini/gemini-cli/pull/16274)
|
||||
- revert the change that was recently added from a fix by @sehoon38 in
|
||||
[#16390](https://github.com/google-gemini/gemini-cli/pull/16390)
|
||||
- Add other hook wrapper methods to hooksystem by @ved015 in
|
||||
[#16361](https://github.com/google-gemini/gemini-cli/pull/16361)
|
||||
- feat: introduce useRewindLogic hook for conversation history navigation by
|
||||
@Adib234 in [#15716](https://github.com/google-gemini/gemini-cli/pull/15716)
|
||||
- docs: Fix formatting issue in memport documentation by @wanglc02 in
|
||||
[#14774](https://github.com/google-gemini/gemini-cli/pull/14774)
|
||||
- fix(policy): enhance shell command safety and parsing by @allenhutchison in
|
||||
[#15034](https://github.com/google-gemini/gemini-cli/pull/15034)
|
||||
- fix(core): avoid 'activate_skill' re-registration warning by @NTaylorMullen in
|
||||
[#16398](https://github.com/google-gemini/gemini-cli/pull/16398)
|
||||
- perf(workflows): optimize PR triage script for faster execution by @bdmorgan
|
||||
in [#16355](https://github.com/google-gemini/gemini-cli/pull/16355)
|
||||
- feat(admin): prompt user to restart the CLI if they change auth to oauth
|
||||
mid-session or don't have auth type selected at start of session by @skeshive
|
||||
in [#16426](https://github.com/google-gemini/gemini-cli/pull/16426)
|
||||
- Update cli-help agent's system prompt in sub-agents section by @sehoon38 in
|
||||
[#16441](https://github.com/google-gemini/gemini-cli/pull/16441)
|
||||
- Revert "Update extension examples" by @chrstnb in
|
||||
[#16442](https://github.com/google-gemini/gemini-cli/pull/16442)
|
||||
- Fix: add back fastreturn support by @scidomino in
|
||||
[#16440](https://github.com/google-gemini/gemini-cli/pull/16440)
|
||||
- feat(a2a): Introduce /memory command for a2a server by @cocosheng-g in
|
||||
[#14456](https://github.com/google-gemini/gemini-cli/pull/14456)
|
||||
- docs: fix broken internal link by using relative path by @Gong-Mi in
|
||||
[#15371](https://github.com/google-gemini/gemini-cli/pull/15371)
|
||||
- migrate yolo/auto-edit keybindings by @scidomino in
|
||||
[#16457](https://github.com/google-gemini/gemini-cli/pull/16457)
|
||||
- feat(cli): add install and uninstall commands for skills by @NTaylorMullen in
|
||||
[#16377](https://github.com/google-gemini/gemini-cli/pull/16377)
|
||||
- feat(ui): use Tab to switch focus between shell and input by @jacob314 in
|
||||
[#14332](https://github.com/google-gemini/gemini-cli/pull/14332)
|
||||
- feat(core): support shipping built-in skills with the CLI by @NTaylorMullen in
|
||||
[#16300](https://github.com/google-gemini/gemini-cli/pull/16300)
|
||||
- Collect hardware details telemetry. by @gundermanc in
|
||||
[#16119](https://github.com/google-gemini/gemini-cli/pull/16119)
|
||||
- feat(agents): improve UI feedback and parser reliability by @NTaylorMullen in
|
||||
[#16459](https://github.com/google-gemini/gemini-cli/pull/16459)
|
||||
- Migrate keybindings by @scidomino in
|
||||
[#16460](https://github.com/google-gemini/gemini-cli/pull/16460)
|
||||
- feat(cli): cleanup activity logs alongside session files by @SandyTao520 in
|
||||
[#16399](https://github.com/google-gemini/gemini-cli/pull/16399)
|
||||
- feat(cli): implement dynamic terminal tab titles for CLI status by
|
||||
@NTaylorMullen in
|
||||
[#16378](https://github.com/google-gemini/gemini-cli/pull/16378)
|
||||
- feat(core): add disableLLMCorrection setting to skip auto-correction in edit
|
||||
tools by @SandyTao520 in
|
||||
[#16000](https://github.com/google-gemini/gemini-cli/pull/16000)
|
||||
- fix: Set both tab and window title instead of just window title by
|
||||
@NTaylorMullen in
|
||||
[#16464](https://github.com/google-gemini/gemini-cli/pull/16464)
|
||||
- fix(policy): ensure MCP policies match unqualified names in non-interactive
|
||||
mode by @NTaylorMullen in
|
||||
[#16490](https://github.com/google-gemini/gemini-cli/pull/16490)
|
||||
- fix(cli): refine 'Action Required' indicator and focus hints by @NTaylorMullen
|
||||
in [#16497](https://github.com/google-gemini/gemini-cli/pull/16497)
|
||||
- Refactor beforeAgent and afterAgent hookEvents to follow desired output by
|
||||
@ved015 in [#16495](https://github.com/google-gemini/gemini-cli/pull/16495)
|
||||
- feat(agents): clarify mandatory YAML frontmatter for sub-agents by
|
||||
@NTaylorMullen in
|
||||
[#16515](https://github.com/google-gemini/gemini-cli/pull/16515)
|
||||
- docs(telemetry): add Google Cloud Monitoring dashboard documentation by @jerop
|
||||
in [#16520](https://github.com/google-gemini/gemini-cli/pull/16520)
|
||||
- Implement support for subagents as extensions. by @gundermanc in
|
||||
[#16473](https://github.com/google-gemini/gemini-cli/pull/16473)
|
||||
- refactor: make baseTimestamp optional in addItem and remove redundant calls by
|
||||
@sehoon38 in [#16471](https://github.com/google-gemini/gemini-cli/pull/16471)
|
||||
- Improve key binding names and descriptions by @scidomino in
|
||||
[#16529](https://github.com/google-gemini/gemini-cli/pull/16529)
|
||||
- feat(core, cli): Add support for agents in settings.json. by @joshualitt in
|
||||
[#16433](https://github.com/google-gemini/gemini-cli/pull/16433)
|
||||
- fix(cli): fix 'gemini skills install' unknown argument error by @NTaylorMullen
|
||||
in [#16537](https://github.com/google-gemini/gemini-cli/pull/16537)
|
||||
- chore(ui): optimize AgentsStatus layout with dense list style and group
|
||||
separation by @adamfweidman in
|
||||
[#16545](https://github.com/google-gemini/gemini-cli/pull/16545)
|
||||
- fix(cli): allow @ file selector on slash command lines by @galz10 in
|
||||
[#16370](https://github.com/google-gemini/gemini-cli/pull/16370)
|
||||
- fix(ui): resolve sticky header regression in tool messages by @jacob314 in
|
||||
[#16514](https://github.com/google-gemini/gemini-cli/pull/16514)
|
||||
- feat(core): Align internal agent settings with configs exposed through
|
||||
settings.json by @joshualitt in
|
||||
[#16458](https://github.com/google-gemini/gemini-cli/pull/16458)
|
||||
- fix(cli): copy uses OSC52 only in SSH/WSL by @assagman in
|
||||
[#16554](https://github.com/google-gemini/gemini-cli/pull/16554)
|
||||
- docs(skills): clarify skill directory structure and file location by
|
||||
@NTaylorMullen in
|
||||
[#16532](https://github.com/google-gemini/gemini-cli/pull/16532)
|
||||
- Fix: make ctrl+x use preferred editor by @scidomino in
|
||||
[#16556](https://github.com/google-gemini/gemini-cli/pull/16556)
|
||||
- fix(core): Resolve race condition in tool response reporting by @abhipatel12
|
||||
in [#16557](https://github.com/google-gemini/gemini-cli/pull/16557)
|
||||
- feat(ui): highlight persist mode status in ModelDialog by @sehoon38 in
|
||||
[#16483](https://github.com/google-gemini/gemini-cli/pull/16483)
|
||||
- refactor: clean up A2A task output for users and LLMs by @adamfweidman in
|
||||
[#16561](https://github.com/google-gemini/gemini-cli/pull/16561)
|
||||
- feat(core/ui): enhance retry mechanism and UX by @sehoon38 in
|
||||
[#16489](https://github.com/google-gemini/gemini-cli/pull/16489)
|
||||
- Modernize MaxSizedBox to use and ResizeObservers by @jacob314 in
|
||||
[#16565](https://github.com/google-gemini/gemini-cli/pull/16565)
|
||||
- Behavioral evals framework. by @gundermanc in
|
||||
[#16047](https://github.com/google-gemini/gemini-cli/pull/16047)
|
||||
- Aggregate test results. by @gundermanc in
|
||||
[#16581](https://github.com/google-gemini/gemini-cli/pull/16581)
|
||||
- feat(admin): support admin-enforced settings for Agent Skills by
|
||||
@NTaylorMullen in
|
||||
[#16406](https://github.com/google-gemini/gemini-cli/pull/16406)
|
||||
- fix(patch): cherry-pick cfdc4cf to release/v0.25.0-preview.0-pr-16759 to patch
|
||||
version v0.25.0-preview.0 and create version 0.25.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#16866](https://github.com/google-gemini/gemini-cli/pull/16866)
|
||||
- Patch #16730 into v0.25.0 preview by @chrstnb in
|
||||
[#16882](https://github.com/google-gemini/gemini-cli/pull/16882)
|
||||
- fix(patch): cherry-pick 3b55581 to release/v0.25.0-preview.2-pr-16506 to patch
|
||||
version v0.25.0-preview.2 and create version 0.25.0-preview.3 by
|
||||
@gemini-cli-robot in
|
||||
[#17098](https://github.com/google-gemini/gemini-cli/pull/17098)
|
||||
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
|
||||
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
|
||||
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
|
||||
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
|
||||
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
|
||||
- fix(core): update telemetry token count after session resume by @psinha40898
|
||||
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
|
||||
- Demote the subagent test to nightly by @gundermanc in
|
||||
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
|
||||
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
|
||||
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
|
||||
- feat: Add flash lite utility fallback chain by @adamfweidman in
|
||||
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
|
||||
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
|
||||
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
|
||||
- feat(core): Add initial eval for generalist agent. by @joshualitt in
|
||||
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
|
||||
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
|
||||
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
|
||||
- fix(core): resolve auto model in default strategy by @sehoon38 in
|
||||
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
|
||||
- docs: update project context and pr-creator workflow by @NTaylorMullen in
|
||||
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
|
||||
- fix(cli): send gemini-cli version as mcp client version by @dsp in
|
||||
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
|
||||
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
|
||||
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
|
||||
- Remove missing sidebar item by @chrstnb in
|
||||
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
|
||||
- feat(core): Ensure all properties in hooks object are event names. by
|
||||
@joshualitt in
|
||||
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
|
||||
- fix(cli): fix newline support broken in previous PR by @scidomino in
|
||||
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
|
||||
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
|
||||
by @gsquared94 in
|
||||
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
|
||||
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
|
||||
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
|
||||
- Avoid spurious warnings about unexpected renders triggered by appEvents and
|
||||
coreEvents. by @jacob314 in
|
||||
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
|
||||
- fix(cli): resolve home/end keybinding conflict by @scidomino in
|
||||
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
|
||||
- fix(cli): display 'http' type on mcp list by @pamanta in
|
||||
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
|
||||
- fix bad fallback logic external editor logic by @scidomino in
|
||||
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
|
||||
- Fix bug where System scopes weren't migrated. by @jacob314 in
|
||||
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
|
||||
- Fix mcp tool lookup in tool registry by @werdnum in
|
||||
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.24.5...v0.25.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.25.2...v0.26.0
|
||||
|
||||
+415
-310
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.26.0-preview.0
|
||||
# Preview release: Release v0.27.0-preview.0
|
||||
|
||||
Released: January 21, 2026
|
||||
Released: January 27, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -13,320 +13,425 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Skills and Agents:** Improvements to the `activate_skill` tool and skill
|
||||
management. Experimental Agent Skills support.
|
||||
- **UI/UX:** Addition of a Rewind Confirmation dialog and Viewer component.
|
||||
- **Extensions:** Experimental setting for extension configuration.
|
||||
- **Bug Fixes and Stability:** PDF token estimation fix and improvements to
|
||||
scheduled issue triage.
|
||||
- **Event-Driven Architecture:** The tool execution scheduler is now
|
||||
event-driven, improving performance and reliability.
|
||||
- **System Prompt Override:** Now supports dynamic variable substitution.
|
||||
- **Rewind Command:** The `/rewind` command has been implemented.
|
||||
- **Linux Clipboard:** Image pasting capabilities for Wayland and X11 on Linux.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix: PDF token estimation
|
||||
([#16494](https://github.com/google-gemini/gemini-cli/pull/16494)) by
|
||||
@korade-krushna in
|
||||
[#16527](https://github.com/google-gemini/gemini-cli/pull/16527)
|
||||
- chore(release): bump version to 0.26.0-nightly.20260114.bb6c57414 by
|
||||
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
|
||||
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
|
||||
- Remove unused modelHooks and toolHooks by @ved015 in
|
||||
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
|
||||
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
|
||||
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
|
||||
- Update Attempt text to Retry when showing the retry happening to the … by
|
||||
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
|
||||
- chore(skills): update pr-creator skill workflow by @sehoon38 in
|
||||
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
|
||||
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
|
||||
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
|
||||
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
|
||||
@gemini-cli-robot in
|
||||
[#16604](https://github.com/google-gemini/gemini-cli/pull/16604)
|
||||
- docs: clarify F12 to open debug console by @jackwotherspoon in
|
||||
[#16570](https://github.com/google-gemini/gemini-cli/pull/16570)
|
||||
- docs: Remove .md extension from internal links in architecture.md by
|
||||
@medic-code in
|
||||
[#12899](https://github.com/google-gemini/gemini-cli/pull/12899)
|
||||
- Add an experimental setting for extension config by @chrstnb in
|
||||
[#16506](https://github.com/google-gemini/gemini-cli/pull/16506)
|
||||
- feat: add Rewind Confirmation dialog and Rewind Viewer component by @Adib234
|
||||
in [#15717](https://github.com/google-gemini/gemini-cli/pull/15717)
|
||||
- fix(a2a): Don't throw errors for GeminiEventType Retry and InvalidStream. by
|
||||
@ehedlund in [#16541](https://github.com/google-gemini/gemini-cli/pull/16541)
|
||||
- prefactor: add rootCommands as array so it can be used for policy parsing by
|
||||
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
|
||||
- Remove other rewind reference in docs by @chrstnb in
|
||||
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
|
||||
- feat(skills): add code-reviewer skill by @sehoon38 in
|
||||
[#17187](httpshttps://github.com/google-gemini/gemini-cli/pull/17187)
|
||||
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
|
||||
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
|
||||
- feat(plan): refactor TestRig and eval helper to support configurable approval
|
||||
modes by @jerop in
|
||||
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
|
||||
- feat(workflows): support recursive workstream labeling and new IDs by
|
||||
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
|
||||
- Run evals for all models. by @gundermanc in
|
||||
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123)
|
||||
- fix(github): improve label-workstream-rollup efficiency with GraphQL by
|
||||
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217)
|
||||
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by
|
||||
@g-samroberts in
|
||||
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215)
|
||||
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in
|
||||
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204)
|
||||
- fix(github): improve label-workstream-rollup efficiency and fix bugs by
|
||||
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219)
|
||||
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in
|
||||
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192)
|
||||
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in
|
||||
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218)
|
||||
- feat(core): support dynamic variable substitution in system prompt override by
|
||||
@NTaylorMullen in
|
||||
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042)
|
||||
- fix(core,cli): enable recursive directory access for by @galz10 in
|
||||
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094)
|
||||
- Docs: Marking for experimental features by @jkcinouye in
|
||||
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760)
|
||||
- Support command/ctrl/alt backspace correctly by @scidomino in
|
||||
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175)
|
||||
- feat(plan): add approval mode instructions to system prompt by @jerop in
|
||||
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151)
|
||||
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in
|
||||
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223)
|
||||
- Remove unused slug from sidebar by @chrstnb in
|
||||
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229)
|
||||
- drain stdin on exit by @scidomino in
|
||||
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241)
|
||||
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by
|
||||
@abhipatel12 in
|
||||
[#16640](https://github.com/google-gemini/gemini-cli/pull/16640)
|
||||
- remove unnecessary \x7f key bindings by @scidomino in
|
||||
[#16646](https://github.com/google-gemini/gemini-cli/pull/16646)
|
||||
- docs(skills): use body-file in pr-creator skill for better reliability by
|
||||
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
|
||||
- fix(core): update token count and telemetry on /chat resume history load by
|
||||
@psinha40898 in
|
||||
[#16279](https://github.com/google-gemini/gemini-cli/pull/16279)
|
||||
- fix: /policy to display policies according to mode by @ishaanxgupta in
|
||||
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772)
|
||||
- fix(core): simplify replace tool error message by @SandyTao520 in
|
||||
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246)
|
||||
- feat(cli): consolidate shell inactivity and redirection monitoring by
|
||||
@NTaylorMullen in
|
||||
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086)
|
||||
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by
|
||||
@abhipatel12 in
|
||||
[#16642](https://github.com/google-gemini/gemini-cli/pull/16642)
|
||||
- chore(automation): recursive labeling for workstream descendants by @bdmorgan
|
||||
in [#16609](https://github.com/google-gemini/gemini-cli/pull/16609)
|
||||
- feat: introduce 'skill-creator' built-in skill and CJS management tools by
|
||||
@NTaylorMullen in
|
||||
[#16394](https://github.com/google-gemini/gemini-cli/pull/16394)
|
||||
- chore(automation): remove automated PR size and complexity labeler by
|
||||
@bdmorgan in [#16648](https://github.com/google-gemini/gemini-cli/pull/16648)
|
||||
- refactor(skills): replace 'project' with 'workspace' scope by @NTaylorMullen
|
||||
in [#16380](https://github.com/google-gemini/gemini-cli/pull/16380)
|
||||
- Docs: Update release notes for 1/13/2026 by @jkcinouye in
|
||||
[#16583](https://github.com/google-gemini/gemini-cli/pull/16583)
|
||||
- Simplify paste handling by @scidomino in
|
||||
[#16654](https://github.com/google-gemini/gemini-cli/pull/16654)
|
||||
- chore(automation): improve scheduled issue triage discovery and throughput by
|
||||
@bdmorgan in [#16652](https://github.com/google-gemini/gemini-cli/pull/16652)
|
||||
- fix(acp): run exit cleanup when stdin closes by @codefromthecrypt in
|
||||
[#14953](https://github.com/google-gemini/gemini-cli/pull/14953)
|
||||
- feat(scheduler): add types needed for event driven scheduler by @abhipatel12
|
||||
in [#16641](https://github.com/google-gemini/gemini-cli/pull/16641)
|
||||
- Remove unused rewind key binding by @scidomino in
|
||||
[#16659](https://github.com/google-gemini/gemini-cli/pull/16659)
|
||||
- Remove sequence binding by @scidomino in
|
||||
[#16664](https://github.com/google-gemini/gemini-cli/pull/16664)
|
||||
- feat(cli): undeprecate the --prompt flag by @alexaustin007 in
|
||||
[#13981](https://github.com/google-gemini/gemini-cli/pull/13981)
|
||||
- chore: update dependabot configuration by @cosmopax in
|
||||
[#13507](https://github.com/google-gemini/gemini-cli/pull/13507)
|
||||
- feat(config): add 'auto' alias for default model selection by @sehoon38 in
|
||||
[#16661](https://github.com/google-gemini/gemini-cli/pull/16661)
|
||||
- Enable & disable agents by @sehoon38 in
|
||||
[#16225](https://github.com/google-gemini/gemini-cli/pull/16225)
|
||||
- cleanup: Improve keybindings by @scidomino in
|
||||
[#16672](https://github.com/google-gemini/gemini-cli/pull/16672)
|
||||
- Add timeout for shell-utils to prevent hangs. by @jacob314 in
|
||||
[#16667](https://github.com/google-gemini/gemini-cli/pull/16667)
|
||||
- feat(plan): add experimental plan flag by @jerop in
|
||||
[#16650](https://github.com/google-gemini/gemini-cli/pull/16650)
|
||||
- feat(cli): add security consent prompts for skill installation by
|
||||
@NTaylorMullen in
|
||||
[#16549](https://github.com/google-gemini/gemini-cli/pull/16549)
|
||||
- fix: replace 3 consecutive periods with ellipsis character by @Vist233 in
|
||||
[#16587](https://github.com/google-gemini/gemini-cli/pull/16587)
|
||||
- chore(automation): ensure status/need-triage is applied and never cleared
|
||||
automatically by @bdmorgan in
|
||||
[#16657](https://github.com/google-gemini/gemini-cli/pull/16657)
|
||||
- fix: Handle colons in skill description frontmatter by @maru0804 in
|
||||
[#16345](https://github.com/google-gemini/gemini-cli/pull/16345)
|
||||
- refactor(core): harden skill frontmatter parsing by @NTaylorMullen in
|
||||
[#16705](https://github.com/google-gemini/gemini-cli/pull/16705)
|
||||
- feat(skills): add conflict detection and warnings for skill overrides by
|
||||
@NTaylorMullen in
|
||||
[#16709](https://github.com/google-gemini/gemini-cli/pull/16709)
|
||||
- feat(scheduler): add SchedulerStateManager for reactive tool state by
|
||||
@abhipatel12 in
|
||||
[#16651](https://github.com/google-gemini/gemini-cli/pull/16651)
|
||||
- chore(automation): enforce 'help wanted' label permissions and update
|
||||
guidelines by @bdmorgan in
|
||||
[#16707](https://github.com/google-gemini/gemini-cli/pull/16707)
|
||||
- fix(core): resolve circular dependency via tsconfig paths by @sehoon38 in
|
||||
[#16730](https://github.com/google-gemini/gemini-cli/pull/16730)
|
||||
- chore/release: bump version to 0.26.0-nightly.20260115.6cb3ae4e0 by
|
||||
@gemini-cli-robot in
|
||||
[#16738](https://github.com/google-gemini/gemini-cli/pull/16738)
|
||||
- fix(automation): correct status/need-issue label matching wildcard by
|
||||
@bdmorgan in [#16727](https://github.com/google-gemini/gemini-cli/pull/16727)
|
||||
- fix(automation): prevent label-enforcer loop by ignoring all bots by @bdmorgan
|
||||
in [#16746](https://github.com/google-gemini/gemini-cli/pull/16746)
|
||||
- Add links to supported locations and minor fixes by @g-samroberts in
|
||||
[#16476](https://github.com/google-gemini/gemini-cli/pull/16476)
|
||||
- feat(policy): add source tracking to policy rules by @allenhutchison in
|
||||
[#16670](https://github.com/google-gemini/gemini-cli/pull/16670)
|
||||
- feat(automation): enforce '🔒 maintainer only' and fix bot loop by @bdmorgan
|
||||
in [#16751](https://github.com/google-gemini/gemini-cli/pull/16751)
|
||||
- Make merged settings non-nullable and fix all lints related to that. by
|
||||
@jacob314 in [#16647](https://github.com/google-gemini/gemini-cli/pull/16647)
|
||||
- fix(core): prevent ModelInfo event emission on aborted signal by @sehoon38 in
|
||||
[#16752](https://github.com/google-gemini/gemini-cli/pull/16752)
|
||||
- Replace relative paths to fix website build by @chrstnb in
|
||||
[#16755](https://github.com/google-gemini/gemini-cli/pull/16755)
|
||||
- Restricting to localhost by @cocosheng-g in
|
||||
[#16548](https://github.com/google-gemini/gemini-cli/pull/16548)
|
||||
- fix(cli): add explicit dependency on color-convert by @sehoon38 in
|
||||
[#16757](https://github.com/google-gemini/gemini-cli/pull/16757)
|
||||
- fix(automation): robust label enforcement with permission checks by @bdmorgan
|
||||
in [#16762](https://github.com/google-gemini/gemini-cli/pull/16762)
|
||||
- fix(cli): prevent OOM crash by limiting file search traversal and adding
|
||||
timeout by @galz10 in
|
||||
[#16696](https://github.com/google-gemini/gemini-cli/pull/16696)
|
||||
- fix(cli): safely handle /dev/tty access on macOS by @korade-krushna in
|
||||
[#16531](https://github.com/google-gemini/gemini-cli/pull/16531)
|
||||
- docs: clarify workspace test execution in GEMINI.md by @mattKorwel in
|
||||
[#16764](https://github.com/google-gemini/gemini-cli/pull/16764)
|
||||
- Add support for running available commands prior to MCP servers loading by
|
||||
@Adib234 in [#15596](https://github.com/google-gemini/gemini-cli/pull/15596)
|
||||
- feat(plan): add experimental 'plan' approval mode by @jerop in
|
||||
[#16753](https://github.com/google-gemini/gemini-cli/pull/16753)
|
||||
- feat(scheduler): add functional awaitConfirmation utility by @abhipatel12 in
|
||||
[#16721](https://github.com/google-gemini/gemini-cli/pull/16721)
|
||||
- fix(infra): update maintainer rollup label to 'workstream-rollup' by @bdmorgan
|
||||
in [#16809](https://github.com/google-gemini/gemini-cli/pull/16809)
|
||||
- fix(infra): use GraphQL to detect direct parents in rollup workflow by
|
||||
@bdmorgan in [#16811](https://github.com/google-gemini/gemini-cli/pull/16811)
|
||||
- chore(workflows): rename label-workstream-rollup workflow by @bdmorgan in
|
||||
[#16818](https://github.com/google-gemini/gemini-cli/pull/16818)
|
||||
- skip simple-mcp-server.test.ts by @scidomino in
|
||||
[#16842](https://github.com/google-gemini/gemini-cli/pull/16842)
|
||||
- Steer outer agent to use expert subagents when present by @gundermanc in
|
||||
[#16763](https://github.com/google-gemini/gemini-cli/pull/16763)
|
||||
- Fix race condition by awaiting scheduleToolCalls by @chrstnb in
|
||||
[#16759](https://github.com/google-gemini/gemini-cli/pull/16759)
|
||||
- cleanup: Organize key bindings by @scidomino in
|
||||
[#16798](https://github.com/google-gemini/gemini-cli/pull/16798)
|
||||
- feat(core): Add generalist agent. by @joshualitt in
|
||||
[#16638](https://github.com/google-gemini/gemini-cli/pull/16638)
|
||||
- perf(ui): optimize text buffer and highlighting for large inputs by
|
||||
@NTaylorMullen in
|
||||
[#16782](https://github.com/google-gemini/gemini-cli/pull/16782)
|
||||
- fix(core): fix PTY descriptor shell leak by @galz10 in
|
||||
[#16773](https://github.com/google-gemini/gemini-cli/pull/16773)
|
||||
- feat(plan): enforce strict read-only policy and halt execution on violation by
|
||||
@jerop in [#16849](https://github.com/google-gemini/gemini-cli/pull/16849)
|
||||
- remove need-triage label from bug_report template by @sehoon38 in
|
||||
[#16864](https://github.com/google-gemini/gemini-cli/pull/16864)
|
||||
- fix(core): truncate large telemetry log entries by @sehoon38 in
|
||||
[#16769](https://github.com/google-gemini/gemini-cli/pull/16769)
|
||||
- docs(extensions): add Agent Skills support and mark feature as experimental by
|
||||
@NTaylorMullen in
|
||||
[#16859](https://github.com/google-gemini/gemini-cli/pull/16859)
|
||||
- fix(core): surface warnings for invalid hook event names in configuration
|
||||
([#16788](https://github.com/google-gemini/gemini-cli/pull/16788)) by
|
||||
@sehoon38 in [#16873](https://github.com/google-gemini/gemini-cli/pull/16873)
|
||||
- feat(plan): remove read_many_files from approval mode policies by @jerop in
|
||||
[#16876](https://github.com/google-gemini/gemini-cli/pull/16876)
|
||||
- feat(admin): implement admin controls polling and restart prompt by @skeshive
|
||||
in [#16627](https://github.com/google-gemini/gemini-cli/pull/16627)
|
||||
- Remove LRUCache class migrating to mnemoist by @jacob314 in
|
||||
[#16872](https://github.com/google-gemini/gemini-cli/pull/16872)
|
||||
- feat(settings): rename negative settings to positive naming (disable* ->
|
||||
enable*) by @afarber in
|
||||
[#14142](https://github.com/google-gemini/gemini-cli/pull/14142)
|
||||
- refactor(cli): unify shell confirmation dialogs by @NTaylorMullen in
|
||||
[#16828](https://github.com/google-gemini/gemini-cli/pull/16828)
|
||||
- feat(agent): enable agent skills by default by @NTaylorMullen in
|
||||
[#16736](https://github.com/google-gemini/gemini-cli/pull/16736)
|
||||
- refactor(core): foundational truncation refactoring and token estimation
|
||||
optimization by @NTaylorMullen in
|
||||
[#16824](https://github.com/google-gemini/gemini-cli/pull/16824)
|
||||
- fix(hooks): enable /hooks disable to reliably stop single hooks by
|
||||
@abhipatel12 in
|
||||
[#16804](https://github.com/google-gemini/gemini-cli/pull/16804)
|
||||
- Don't commit unless user asks us to. by @gundermanc in
|
||||
[#16902](https://github.com/google-gemini/gemini-cli/pull/16902)
|
||||
- chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 by @adamfweidman in
|
||||
[#16800](https://github.com/google-gemini/gemini-cli/pull/16800)
|
||||
- fix: Show experiment values in settings UI for compressionThreshold by
|
||||
@ishaanxgupta in
|
||||
[#16267](https://github.com/google-gemini/gemini-cli/pull/16267)
|
||||
- feat(cli): replace relative keyboard shortcuts link with web URL by
|
||||
@imaliabbas in
|
||||
[#16479](https://github.com/google-gemini/gemini-cli/pull/16479)
|
||||
- fix(core): resolve PKCE length issue and stabilize OAuth redirect port by
|
||||
@sehoon38 in [#16815](https://github.com/google-gemini/gemini-cli/pull/16815)
|
||||
- Delete rewind documentation for now by @Adib234 in
|
||||
[#16932](https://github.com/google-gemini/gemini-cli/pull/16932)
|
||||
- Stabilize skill-creator CI and package format by @NTaylorMullen in
|
||||
[#17001](https://github.com/google-gemini/gemini-cli/pull/17001)
|
||||
- Stabilize the git evals by @gundermanc in
|
||||
[#16989](https://github.com/google-gemini/gemini-cli/pull/16989)
|
||||
- fix(core): attempt compression before context overflow check by @NTaylorMullen
|
||||
in [#16914](https://github.com/google-gemini/gemini-cli/pull/16914)
|
||||
- Fix inverted logic. by @gundermanc in
|
||||
[#17007](https://github.com/google-gemini/gemini-cli/pull/17007)
|
||||
- chore(scripts): add duplicate issue closer script and fix lint errors by
|
||||
@bdmorgan in [#16997](https://github.com/google-gemini/gemini-cli/pull/16997)
|
||||
- docs: update README and config guide to reference Gemini 3 by @JayadityaGit in
|
||||
[#15806](https://github.com/google-gemini/gemini-cli/pull/15806)
|
||||
- fix(cli): correct Homebrew installation detection by @kij in
|
||||
[#14727](https://github.com/google-gemini/gemini-cli/pull/14727)
|
||||
- Demote git evals to nightly run. by @gundermanc in
|
||||
[#17030](https://github.com/google-gemini/gemini-cli/pull/17030)
|
||||
- fix(cli): use OSC-52 clipboard copy in Windows Terminal by @Thomas-Shephard in
|
||||
[#16920](https://github.com/google-gemini/gemini-cli/pull/16920)
|
||||
- Fix: Process all parts in response chunks when thought is first by @pyrytakala
|
||||
in [#13539](https://github.com/google-gemini/gemini-cli/pull/13539)
|
||||
- fix(automation): fix jq quoting error in pr-triage.sh by @Kimsoo0119 in
|
||||
[#16958](https://github.com/google-gemini/gemini-cli/pull/16958)
|
||||
- refactor(core): decouple scheduler into orchestration, policy, and
|
||||
confirmation by @abhipatel12 in
|
||||
[#16895](https://github.com/google-gemini/gemini-cli/pull/16895)
|
||||
- feat: add /introspect slash command by @NTaylorMullen in
|
||||
[#17048](https://github.com/google-gemini/gemini-cli/pull/17048)
|
||||
- refactor(cli): centralize tool mapping and decouple legacy scheduler by
|
||||
@abhipatel12 in
|
||||
[#17044](https://github.com/google-gemini/gemini-cli/pull/17044)
|
||||
- fix(ui): ensure rationale renders before tool calls by @NTaylorMullen in
|
||||
[#17043](https://github.com/google-gemini/gemini-cli/pull/17043)
|
||||
- fix(workflows): use author_association for maintainer check by @bdmorgan in
|
||||
[#17060](https://github.com/google-gemini/gemini-cli/pull/17060)
|
||||
- fix return type of fireSessionStartEvent to defaultHookOutput by @ved015 in
|
||||
[#16833](https://github.com/google-gemini/gemini-cli/pull/16833)
|
||||
- feat(cli): add experiment gate for event-driven scheduler by @abhipatel12 in
|
||||
[#17055](https://github.com/google-gemini/gemini-cli/pull/17055)
|
||||
- feat(core): improve shell redirection transparency and security by
|
||||
@NTaylorMullen in
|
||||
[#16486](https://github.com/google-gemini/gemini-cli/pull/16486)
|
||||
- fix(core): deduplicate ModelInfo emission in GeminiClient by @NTaylorMullen in
|
||||
[#17075](https://github.com/google-gemini/gemini-cli/pull/17075)
|
||||
- docs(themes): remove unsupported DiffModified color key by @jw409 in
|
||||
[#17073](https://github.com/google-gemini/gemini-cli/pull/17073)
|
||||
- fix: update currentSequenceModel when modelChanged by @adamfweidman in
|
||||
[#17051](https://github.com/google-gemini/gemini-cli/pull/17051)
|
||||
- feat(core): enhanced anchored iterative context compression with
|
||||
self-verification by @rmedranollamas in
|
||||
[#15710](https://github.com/google-gemini/gemini-cli/pull/15710)
|
||||
- Fix mcp instructions by @chrstnb in
|
||||
[#16439](https://github.com/google-gemini/gemini-cli/pull/16439)
|
||||
- [A2A] Disable checkpointing if git is not installed by @cocosheng-g in
|
||||
[#16896](https://github.com/google-gemini/gemini-cli/pull/16896)
|
||||
- feat(admin): set admin.skills.enabled based on advancedFeaturesEnabled setting
|
||||
by @skeshive in
|
||||
[#17095](https://github.com/google-gemini/gemini-cli/pull/17095)
|
||||
- Test coverage for hook exit code cases by @gundermanc in
|
||||
[#17041](https://github.com/google-gemini/gemini-cli/pull/17041)
|
||||
- Revert "Revert "Update extension examples"" by @chrstnb in
|
||||
[#16445](https://github.com/google-gemini/gemini-cli/pull/16445)
|
||||
- fix(core): Provide compact, actionable errors for agent delegation failures by
|
||||
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
|
||||
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
|
||||
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
|
||||
- feat(hooks): enable hooks system by default by @abhipatel12 in
|
||||
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
|
||||
- feat(core): Enable AgentRegistry to track all discovered subagents by
|
||||
@SandyTao520 in
|
||||
[#16493](https://github.com/google-gemini/gemini-cli/pull/16493)
|
||||
- fix: migrate BeforeModel and AfterModel hooks to HookSystem by @ved015 in
|
||||
[#16599](https://github.com/google-gemini/gemini-cli/pull/16599)
|
||||
- feat(admin): apply admin settings to gemini skills/mcp/extensions commands by
|
||||
@skeshive in [#17102](https://github.com/google-gemini/gemini-cli/pull/17102)
|
||||
- fix(core): update telemetry token count after session resume by @psinha40898
|
||||
in [#15491](https://github.com/google-gemini/gemini-cli/pull/15491)
|
||||
- Demote the subagent test to nightly by @gundermanc in
|
||||
[#17105](https://github.com/google-gemini/gemini-cli/pull/17105)
|
||||
- feat(plan): telemetry to track adoption and usage of plan mode by @Adib234 in
|
||||
[#16863](https://github.com/google-gemini/gemini-cli/pull/16863)
|
||||
- feat: Add flash lite utility fallback chain by @adamfweidman in
|
||||
[#17056](https://github.com/google-gemini/gemini-cli/pull/17056)
|
||||
- Fixes Windows crash: "Cannot resize a pty that has already exited" by @dzammit
|
||||
in [#15757](https://github.com/google-gemini/gemini-cli/pull/15757)
|
||||
- feat(core): Add initial eval for generalist agent. by @joshualitt in
|
||||
[#16856](https://github.com/google-gemini/gemini-cli/pull/16856)
|
||||
- feat(core): unify agent enabled and disabled flags by @SandyTao520 in
|
||||
[#17127](https://github.com/google-gemini/gemini-cli/pull/17127)
|
||||
- fix(core): resolve auto model in default strategy by @sehoon38 in
|
||||
[#17116](https://github.com/google-gemini/gemini-cli/pull/17116)
|
||||
- docs: update project context and pr-creator workflow by @NTaylorMullen in
|
||||
[#17119](https://github.com/google-gemini/gemini-cli/pull/17119)
|
||||
- fix(cli): send gemini-cli version as mcp client version by @dsp in
|
||||
[#13407](https://github.com/google-gemini/gemini-cli/pull/13407)
|
||||
- fix(cli): resolve Ctrl+Enter and Ctrl+J newline issues by @imadraude in
|
||||
[#17021](https://github.com/google-gemini/gemini-cli/pull/17021)
|
||||
- Remove missing sidebar item by @chrstnb in
|
||||
[#17145](https://github.com/google-gemini/gemini-cli/pull/17145)
|
||||
- feat(core): Ensure all properties in hooks object are event names. by
|
||||
@joshualitt in
|
||||
[#16870](https://github.com/google-gemini/gemini-cli/pull/16870)
|
||||
- fix(cli): fix newline support broken in previous PR by @scidomino in
|
||||
[#17159](https://github.com/google-gemini/gemini-cli/pull/17159)
|
||||
- Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors.
|
||||
by @gsquared94 in
|
||||
[#16231](https://github.com/google-gemini/gemini-cli/pull/16231)
|
||||
- Add Esc-Esc to clear prompt when it's not empty by @Adib234 in
|
||||
[#17131](https://github.com/google-gemini/gemini-cli/pull/17131)
|
||||
- Avoid spurious warnings about unexpected renders triggered by appEvents and
|
||||
coreEvents. by @jacob314 in
|
||||
[#17160](https://github.com/google-gemini/gemini-cli/pull/17160)
|
||||
- fix(cli): resolve home/end keybinding conflict by @scidomino in
|
||||
[#17124](https://github.com/google-gemini/gemini-cli/pull/17124)
|
||||
- fix(cli): display 'http' type on mcp list by @pamanta in
|
||||
[#16915](https://github.com/google-gemini/gemini-cli/pull/16915)
|
||||
- fix bad fallback logic external editor logic by @scidomino in
|
||||
[#17166](https://github.com/google-gemini/gemini-cli/pull/17166)
|
||||
- Fix bug where System scopes weren't migrated. by @jacob314 in
|
||||
[#17174](https://github.com/google-gemini/gemini-cli/pull/17174)
|
||||
- Fix mcp tool lookup in tool registry by @werdnum in
|
||||
[#17054](https://github.com/google-gemini/gemini-cli/pull/17054)
|
||||
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
|
||||
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
|
||||
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
|
||||
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by
|
||||
@jackwotherspoon in
|
||||
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
|
||||
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
|
||||
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
|
||||
- Enable the ability to queue specific nightly eval tests by @gundermanc in
|
||||
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
|
||||
- docs(hooks): comprehensive update of hook documentation and specs by
|
||||
@abhipatel12 in
|
||||
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
|
||||
- refactor: improve large text paste placeholder by @jacob314 in
|
||||
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
|
||||
- feat: implement /rewind command by @Adib234 in
|
||||
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
|
||||
- Feature/jetbrains ide detection by @SoLoHiC in
|
||||
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
|
||||
- docs: update typo in mcp-server.md file by @schifferl in
|
||||
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
|
||||
- Sanitize command names and descriptions by @ehedlund in
|
||||
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
|
||||
- fix(auth): don't crash when initial auth fails by @skeshive in
|
||||
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
|
||||
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
|
||||
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
|
||||
- feat: add AskUser tool schema by @jackwotherspoon in
|
||||
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
|
||||
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
|
||||
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
|
||||
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in
|
||||
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
|
||||
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
|
||||
now handles the caching internally by @jacob314 in
|
||||
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
|
||||
- ci: allow failure in evals-nightly run step by @gundermanc in
|
||||
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
|
||||
- feat(cli): Add state management and plumbing for agent configuration dialog by
|
||||
@SandyTao520 in
|
||||
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
|
||||
- bug: fix ide-client connection to ide-companion when inside docker via
|
||||
ssh/devcontainer by @kapsner in
|
||||
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
|
||||
- Emit correct newline type return by @scidomino in
|
||||
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
|
||||
- New skill: docs-writer by @g-samroberts in
|
||||
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
|
||||
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
|
||||
@spencer426 in
|
||||
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
|
||||
- Disable tips after 10 runs by @Adib234 in
|
||||
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101)
|
||||
- Fix so rewind starts at the bottom and loadHistory refreshes static content.
|
||||
by @jacob314 in
|
||||
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335)
|
||||
- feat(core): Remove legacy settings. by @joshualitt in
|
||||
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244)
|
||||
- feat(plan): add 'communicate' tool kind by @jerop in
|
||||
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341)
|
||||
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by
|
||||
@mattKorwel in
|
||||
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041)
|
||||
- feat(plan): update UI Theme for Plan Mode by @Adib234 in
|
||||
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243)
|
||||
- fix(ui): stabilize rendering during terminal resize in alternate buffer by
|
||||
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783)
|
||||
- feat(cli): add /agents config command and improve agent discovery by
|
||||
@SandyTao520 in
|
||||
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342)
|
||||
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb
|
||||
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299)
|
||||
- fix(cli)!: Default to interactive mode for positional arguments by
|
||||
@ishaanxgupta in
|
||||
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329)
|
||||
- Fix issue #17080 by @jacob314 in
|
||||
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100)
|
||||
- feat(core): Refresh agents after loading an extension. by @joshualitt in
|
||||
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355)
|
||||
- fix(cli): include source in policy rule display by @allenhutchison in
|
||||
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358)
|
||||
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by
|
||||
@gsquared94 in
|
||||
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236)
|
||||
- Refactor subagent delegation to be one tool per agent by @gundermanc in
|
||||
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346)
|
||||
- fix(core): Include MCP server name in OAuth message by @jerop in
|
||||
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351)
|
||||
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and
|
||||
"maintainer only" by @jacob314 in
|
||||
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324)
|
||||
- feat(plan): implement simple workflow for planning in main agent by @jerop in
|
||||
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326)
|
||||
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in
|
||||
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967)
|
||||
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by
|
||||
@medic-code in
|
||||
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038)
|
||||
- fix(core): add alternative command names for Antigravity editor detec… by
|
||||
@BaeSeokJae in
|
||||
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829)
|
||||
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in
|
||||
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737)
|
||||
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in
|
||||
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390)
|
||||
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in
|
||||
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714)
|
||||
- fix(core): gracefully handle disk full errors in chat recording by
|
||||
@godwiniheuwa in
|
||||
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305)
|
||||
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in
|
||||
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388)
|
||||
- fix(core): use RFC 9728 compliant path-based OAuth protected resource
|
||||
discovery by @vrv in
|
||||
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756)
|
||||
- Update Code Wiki README badge by @PatoBeltran in
|
||||
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229)
|
||||
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in
|
||||
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921)
|
||||
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in
|
||||
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369)
|
||||
- fix(cli): preserve input text when declining tool approval (#15624) by
|
||||
@ManojINaik in
|
||||
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659)
|
||||
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in
|
||||
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403)
|
||||
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon
|
||||
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344)
|
||||
- feat(ui): display user tier in about command by @sehoon38 in
|
||||
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400)
|
||||
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in
|
||||
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574)
|
||||
- fix(cli): change image paste location to global temp directory (#17396) by
|
||||
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396)
|
||||
- Fix line endings issue with Notice file by @scidomino in
|
||||
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417)
|
||||
- feat(plan): implement persistent approvalMode setting by @Adib234 in
|
||||
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350)
|
||||
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in
|
||||
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404)
|
||||
- Allow prompt queueing during MCP initialization by @Adib234 in
|
||||
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395)
|
||||
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520
|
||||
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370)
|
||||
- fix(agents): default to all tools when tool list is omitted in subagents by
|
||||
@gundermanc in
|
||||
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422)
|
||||
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in
|
||||
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276)
|
||||
- fix(core): hide user tier name by @sehoon38 in
|
||||
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418)
|
||||
- feat: Enforce unified folder trust for /directory add by @galz10 in
|
||||
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359)
|
||||
- migrate fireToolNotificationHook to hookSystem by @ved015 in
|
||||
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398)
|
||||
- Clean up dead code by @scidomino in
|
||||
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443)
|
||||
- feat(workflow): add stale pull request closer with linked-issue enforcement by
|
||||
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449)
|
||||
- feat(workflow): expand stale-exempt labels to include help wanted and Public
|
||||
Roadmap by @bdmorgan in
|
||||
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459)
|
||||
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in
|
||||
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460)
|
||||
- Resolves the confusing error message `ripgrep exited with code null that
|
||||
occurs when a search operation is cancelled or aborted by @maximmasiutin in
|
||||
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267)
|
||||
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in
|
||||
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254)
|
||||
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by
|
||||
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412)
|
||||
- docs(hooks): clarify mandatory 'type' field and update hook schema
|
||||
documentation by @abhipatel12 in
|
||||
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499)
|
||||
- Improve error messages on failed onboarding by @gsquared94 in
|
||||
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357)
|
||||
- Follow up to "enableInteractiveShell for external tooling relying on a2a
|
||||
server" by @DavidAPierce in
|
||||
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130)
|
||||
- Fix/issue 17070 by @alih552 in
|
||||
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242)
|
||||
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin
|
||||
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476)
|
||||
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in
|
||||
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470)
|
||||
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in
|
||||
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538)
|
||||
- Fix bug in detecting already added paths. by @jacob314 in
|
||||
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430)
|
||||
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs
|
||||
by @abhipatel12 in
|
||||
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429)
|
||||
- feat(agents): implement first-run experience for project-level sub-agents by
|
||||
@gundermanc in
|
||||
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266)
|
||||
- Update extensions docs by @chrstnb in
|
||||
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093)
|
||||
- Docs: Refactor left nav on the website by @jkcinouye in
|
||||
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
|
||||
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
|
||||
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
|
||||
- feat(plan): add persistent plan file storage by @jerop in
|
||||
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
|
||||
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
|
||||
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
|
||||
- Fix extensions config error by @chrstnb in
|
||||
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
|
||||
- fix(plan): remove subagent invocation from plan mode by @jerop in
|
||||
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
|
||||
- feat(ui): add solid background color option for input prompt by @jacob314 in
|
||||
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
|
||||
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
|
||||
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
|
||||
- feat(cli): add global setting to disable UI spinners by @galz10 in
|
||||
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
|
||||
- fix(security): enforce strict policy directory permissions by @yunaseoul in
|
||||
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
|
||||
- test(core): fix tests in windows by @scidomino in
|
||||
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
|
||||
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
|
||||
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
|
||||
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
|
||||
- Always map mac keys, even on other platforms by @scidomino in
|
||||
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
|
||||
- Ctrl-O by @jacob314 in
|
||||
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
|
||||
- feat(plan): update cycling order of approval modes by @Adib234 in
|
||||
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
|
||||
- fix(cli): restore 'Modify with editor' option in external terminals by
|
||||
@abhipatel12 in
|
||||
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
|
||||
- Slash command for helping in debugging by @gundermanc in
|
||||
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
|
||||
- feat: add double-click to expand/collapse large paste placeholders by
|
||||
@jackwotherspoon in
|
||||
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
|
||||
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
|
||||
@abhipatel12 in
|
||||
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
|
||||
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
|
||||
(regression) by @gsquared94 in
|
||||
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
|
||||
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
|
||||
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
|
||||
- feat(core): enforce server prefixes for MCP tools in agent definitions by
|
||||
@abhipatel12 in
|
||||
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
|
||||
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
|
||||
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
|
||||
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
|
||||
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
|
||||
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
|
||||
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
|
||||
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
|
||||
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
|
||||
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
|
||||
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
|
||||
- paste transform followup by @jacob314 in
|
||||
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
|
||||
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
|
||||
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
|
||||
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
|
||||
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
|
||||
- feat(cli): add oncall command for issue triage by @sehoon38 in
|
||||
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
|
||||
- Fix sidebar issue for extensions link by @chrstnb in
|
||||
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
|
||||
- Change formatting to prevent UI redressing attacks by @scidomino in
|
||||
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
|
||||
- Fix cluster of bugs in the settings dialog. by @jacob314 in
|
||||
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
|
||||
- Update sidebar to resolve site build issues by @chrstnb in
|
||||
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
|
||||
- fix(admin): fix a few bugs related to admin controls by @skeshive in
|
||||
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
|
||||
- revert bad changes to tests by @scidomino in
|
||||
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
|
||||
- feat(cli): show candidate issue state reason and duplicate status in triage by
|
||||
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
|
||||
- Fix missing slash commands when Gemini CLI is in a project with a package.json
|
||||
that doesn't follow semantic versioning by @Adib234 in
|
||||
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
|
||||
- feat(core): Model family-specific system prompts by @joshualitt in
|
||||
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
|
||||
- Sub-agents documentation. by @gundermanc in
|
||||
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
|
||||
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
|
||||
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
|
||||
- Load extension settings for hooks, agents, skills by @chrstnb in
|
||||
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
|
||||
- Fix issue where Gemini CLI can make changes when simply asked a question by
|
||||
@gundermanc in
|
||||
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
|
||||
- Update docs-writer skill for editing and add style guide for reference. by
|
||||
@g-samroberts in
|
||||
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
|
||||
- fix(ux): have user message display a short path for pasted images by @devr0306
|
||||
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
|
||||
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
|
||||
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
|
||||
- GEMINI.md polish by @jacob314 in
|
||||
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
|
||||
- refactor(core): centralize path validation and allow temp dir access for tools
|
||||
by @NTaylorMullen in
|
||||
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
|
||||
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
|
||||
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
|
||||
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
|
||||
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
|
||||
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
|
||||
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
|
||||
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
|
||||
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
|
||||
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
|
||||
mode. by @jacob314 in
|
||||
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
|
||||
- feat(skills): promote skills settings to stable by @abhipatel12 in
|
||||
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
|
||||
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
|
||||
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
|
||||
- feat(ui): add terminal cursor support by @jacob314 in
|
||||
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
|
||||
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
|
||||
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
|
||||
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
|
||||
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
|
||||
- Add support for an additional exclusion file besides .gitignore and
|
||||
.geminiignore by @alisa-alisa in
|
||||
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
|
||||
- feat: add review-frontend-and-fix command by @galz10 in
|
||||
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.25.0-preview.4...v0.26.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.26.0-preview.5...v0.27.0-preview.0
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# CLI cheatsheet
|
||||
|
||||
This page provides a reference for commonly used Gemini CLI commands, options,
|
||||
and parameters.
|
||||
|
||||
## CLI commands
|
||||
|
||||
| Command | Description | Example |
|
||||
| ---------------------------------- | ---------------------------------- | --------------------------------------------------- |
|
||||
| `gemini` | Start interactive REPL | `gemini` |
|
||||
| `gemini "query"` | Query non-interactively, then exit | `gemini "explain this project"` |
|
||||
| `gemini -p "query"` | Query via SDK, then exit | `gemini -p "explain this function"` |
|
||||
| `cat file \| gemini -p "query"` | Process piped content | `cat logs.txt \| gemini -p "explain"` |
|
||||
| `gemini -i "query"` | Execute and continue interactively | `gemini -i "What is the purpose of this project?"` |
|
||||
| `gemini -r "latest"` | Continue most recent session | `gemini -r "latest"` |
|
||||
| `gemini -r "latest" "query"` | Continue session with a new prompt | `gemini -r "latest" "Check for type errors"` |
|
||||
| `gemini -r "<session-id>" "query"` | Resume session by ID | `gemini -r "abc123" "Finish this PR"` |
|
||||
| `gemini update` | Update to latest version | `gemini update` |
|
||||
| `gemini extensions` | Manage extensions | See [Extensions Management](#extensions-management) |
|
||||
| `gemini mcp` | Configure MCP servers | See [MCP Server Management](#mcp-server-management) |
|
||||
|
||||
### Positional arguments
|
||||
|
||||
| Argument | Type | Description |
|
||||
| -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `query` | string (variadic) | Positional prompt. Defaults to one-shot mode. Use `-i/--prompt-interactive` to execute and continue interactively. |
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Alias | Type | Default | Description |
|
||||
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
|
||||
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
|
||||
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
|
||||
| `--allowed-tools` | - | array | - | Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
|
||||
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
|
||||
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
|
||||
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
|
||||
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
|
||||
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
|
||||
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
|
||||
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
|
||||
|
||||
## Model selection
|
||||
|
||||
The `--model` (or `-m`) flag allows you to specify which Gemini model to use.
|
||||
You can use either model aliases (user-friendly names) or concrete model names.
|
||||
|
||||
### Model aliases
|
||||
|
||||
These are convenient shortcuts that map to specific models:
|
||||
|
||||
| Alias | Resolves To | Description |
|
||||
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `auto` | `gemini-2.5-pro` or `gemini-3-pro-preview` | **Default.** Resolves to the preview model if preview features are enabled, otherwise resolves to the standard pro model. |
|
||||
| `pro` | `gemini-2.5-pro` or `gemini-3-pro-preview` | For complex reasoning tasks. Uses preview model if enabled. |
|
||||
| `flash` | `gemini-2.5-flash` | Fast, balanced model for most tasks. |
|
||||
| `flash-lite` | `gemini-2.5-flash-lite` | Fastest model for simple tasks. |
|
||||
|
||||
## Extensions management
|
||||
|
||||
| Command | Description | Example |
|
||||
| -------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `gemini extensions install <source>` | Install extension from Git URL or local path | `gemini extensions install https://github.com/user/my-extension` |
|
||||
| `gemini extensions install <source> --ref <ref>` | Install from specific branch/tag/commit | `gemini extensions install https://github.com/user/my-extension --ref develop` |
|
||||
| `gemini extensions install <source> --auto-update` | Install with auto-update enabled | `gemini extensions install https://github.com/user/my-extension --auto-update` |
|
||||
| `gemini extensions uninstall <name>` | Uninstall one or more extensions | `gemini extensions uninstall my-extension` |
|
||||
| `gemini extensions list` | List all installed extensions | `gemini extensions list` |
|
||||
| `gemini extensions update <name>` | Update a specific extension | `gemini extensions update my-extension` |
|
||||
| `gemini extensions update --all` | Update all extensions | `gemini extensions update --all` |
|
||||
| `gemini extensions enable <name>` | Enable an extension | `gemini extensions enable my-extension` |
|
||||
| `gemini extensions disable <name>` | Disable an extension | `gemini extensions disable my-extension` |
|
||||
| `gemini extensions link <path>` | Link local extension for development | `gemini extensions link /path/to/extension` |
|
||||
| `gemini extensions new <path>` | Create new extension from template | `gemini extensions new ./my-extension` |
|
||||
| `gemini extensions validate <path>` | Validate extension structure | `gemini extensions validate ./my-extension` |
|
||||
|
||||
See [Extensions Documentation](../extensions/index.md) for more details.
|
||||
|
||||
## MCP server management
|
||||
|
||||
| Command | Description | Example |
|
||||
| ------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `gemini mcp add <name> <command>` | Add stdio-based MCP server | `gemini mcp add github npx -y @modelcontextprotocol/server-github` |
|
||||
| `gemini mcp add <name> <url> --transport http` | Add HTTP-based MCP server | `gemini mcp add api-server http://localhost:3000 --transport http` |
|
||||
| `gemini mcp add <name> <command> --env KEY=value` | Add with environment variables | `gemini mcp add slack node server.js --env SLACK_TOKEN=xoxb-xxx` |
|
||||
| `gemini mcp add <name> <command> --scope user` | Add with user scope | `gemini mcp add db node db-server.js --scope user` |
|
||||
| `gemini mcp add <name> <command> --include-tools tool1,tool2` | Add with specific tools | `gemini mcp add github npx -y @modelcontextprotocol/server-github --include-tools list_repos,get_pr` |
|
||||
| `gemini mcp remove <name>` | Remove an MCP server | `gemini mcp remove github` |
|
||||
| `gemini mcp list` | List all configured MCP servers | `gemini mcp list` |
|
||||
|
||||
See [MCP Server Integration](../tools/mcp-server.md) for more details.
|
||||
@@ -288,12 +288,12 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
|
||||
These shortcuts apply directly to the input prompt for text manipulation.
|
||||
|
||||
- **Undo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input
|
||||
prompt.
|
||||
- **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
|
||||
in the input prompt.
|
||||
|
||||
- **Redo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action
|
||||
in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
|
||||
last undone action in the input prompt.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
|
||||
@@ -30,17 +30,17 @@ available combinations.
|
||||
|
||||
#### Editing
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | --------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Cmd + Z (no Shift)`<br />`Alt + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`Shift + Alt + Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
@@ -110,6 +110,7 @@ available combinations.
|
||||
| Focus the Gemini input from the shell input. | `Tab` |
|
||||
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
||||
| Restart the application. | `R` |
|
||||
| Suspend the application (not yet implemented). | `Ctrl + Z` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Rewind
|
||||
|
||||
The `/rewind` command allows you to go back to a previous state in your
|
||||
conversation and, optionally, revert any file changes made by the AI during
|
||||
those interactions. This is a powerful tool for undoing mistakes, exploring
|
||||
different approaches, or simply cleaning up your session history.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the rewind feature, simply type `/rewind` into the input prompt and press
|
||||
**Enter**.
|
||||
|
||||
Alternatively, you can use the keyboard shortcut: **Press `Esc` twice**.
|
||||
|
||||
## Interface
|
||||
|
||||
When you trigger a rewind, an interactive list of your previous interactions
|
||||
appears.
|
||||
|
||||
1. **Select Interaction:** Use the **Up/Down arrow keys** to navigate through
|
||||
the list. The most recent interactions are at the bottom.
|
||||
2. **Preview:** As you select an interaction, you'll see a preview of the user
|
||||
prompt and, if applicable, the number of files changed during that step.
|
||||
3. **Confirm Selection:** Press **Enter** on the interaction you want to rewind
|
||||
back to.
|
||||
4. **Action Selection:** After selecting an interaction, you'll be presented
|
||||
with a confirmation dialog with up to three options:
|
||||
- **Rewind conversation and revert code changes:** Reverts both the chat
|
||||
history and the file modifications to the state before the selected
|
||||
interaction.
|
||||
- **Rewind conversation:** Only reverts the chat history. File changes are
|
||||
kept.
|
||||
- **Revert code changes:** Only reverts the file modifications. The chat
|
||||
history is kept.
|
||||
- **Do nothing (esc):** Cancels the rewind operation.
|
||||
|
||||
If no code changes were made since the selected point, the options related to
|
||||
reverting code changes will be hidden.
|
||||
|
||||
## Key Considerations
|
||||
|
||||
- **Destructive Action:** Rewinding is a destructive action for your current
|
||||
session history and potentially your files. Use it with care.
|
||||
- **Agent Awareness:** When you rewind the conversation, the AI model loses all
|
||||
memory of the interactions that were removed. If you only revert code changes,
|
||||
you may need to inform the model that the files have changed.
|
||||
- **Manual Edits:** Rewinding only affects file changes made by the AI's edit
|
||||
tools. It does **not** undo manual edits you've made or changes triggered by
|
||||
the shell tool (`!`).
|
||||
- **Compression:** Rewind works across chat compression points by reconstructing
|
||||
the history from stored session data.
|
||||
@@ -57,6 +57,7 @@ they appear in the UI.
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
@@ -129,8 +130,9 @@ they appear in the UI.
|
||||
|
||||
### HooksConfig
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
|
||||
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
|
||||
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||
|
||||
<!-- SETTINGS-AUTOGEN:END -->
|
||||
|
||||
+3
-2
@@ -91,8 +91,9 @@ gemini skills disable my-expertise --scope workspace
|
||||
|
||||
## Creating a Skill
|
||||
|
||||
A skill is a directory containing a `SKILL.md` file at its root. This file uses
|
||||
YAML frontmatter for metadata and Markdown for instructions.
|
||||
A skill is a directory containing a `SKILL.md` file at its root. This file
|
||||
serves as the entry point for your skill and uses YAML frontmatter for metadata
|
||||
and Markdown for instructions.
|
||||
|
||||
### Folder Structure
|
||||
|
||||
|
||||
@@ -210,6 +210,10 @@ decision = "ask_user"
|
||||
# The priority of the rule, from 0 to 999.
|
||||
priority = 10
|
||||
|
||||
# (Optional) A custom message to display when a tool call is denied by this rule.
|
||||
# This message is returned to the model and user, useful for explaining *why* it was denied.
|
||||
deny_message = "Deletion is permanent"
|
||||
|
||||
# (Optional) An array of approval modes where this rule is active.
|
||||
modes = ["autoEdit"]
|
||||
```
|
||||
@@ -282,6 +286,7 @@ only the `mcpName`.
|
||||
mcpName = "untrusted-server"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
deny_message = "This server is not trusted by the admin."
|
||||
```
|
||||
|
||||
## Default policies
|
||||
|
||||
@@ -171,11 +171,13 @@ If you need to further tune your sub-agent, you can do so by selecting the model
|
||||
to optimize for with `/model` and then asking the model why it does not think
|
||||
that your sub-agent was called with a specific prompt and the given description.
|
||||
|
||||
## Remote subagents (Agent2Agent)
|
||||
## Remote subagents (Agent2Agent) (experimental)
|
||||
|
||||
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
|
||||
(A2A) protocol.
|
||||
|
||||
> **Note: Remote subagents are currently an experimental feature.**
|
||||
|
||||
See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed
|
||||
configuration and usage instructions.
|
||||
|
||||
|
||||
@@ -244,6 +244,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Show the model name in the chat for each model turn.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.showUserIdentity`** (boolean):
|
||||
- **Description:** Show the logged-in user's identity (e.g. email) in the UI.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.useAlternateBuffer`** (boolean):
|
||||
- **Description:** Use an alternate screen buffer for the UI, preserving shell
|
||||
history.
|
||||
@@ -869,11 +873,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.skills`** (boolean):
|
||||
- **Description:** [Deprecated] Enable Agent Skills (experimental).
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.useOSC52Paste`** (boolean):
|
||||
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
|
||||
(useful for remote sessions).
|
||||
@@ -902,6 +901,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Canonical toggle for the hooks system. When disabled, no
|
||||
hooks will be executed.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`hooksConfig.disabled`** (array):
|
||||
- **Description:** List of hook names (commands) that should be disabled.
|
||||
|
||||
+10
-2
@@ -7,7 +7,8 @@
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{ "label": "Authentication", "slug": "docs/get-started/authentication" },
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" }
|
||||
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" },
|
||||
{ "label": "CLI Reference", "slug": "docs/cli/cli-reference" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -118,7 +119,14 @@
|
||||
"items": [
|
||||
{ "label": "FAQ", "slug": "docs/faq" },
|
||||
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{
|
||||
"label": "Releases",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
]
|
||||
},
|
||||
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
|
||||
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
|
||||
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
|
||||
|
||||
@@ -739,10 +739,21 @@ The MCP integration tracks several states:
|
||||
cautiously and only for servers you completely control
|
||||
- **Access tokens:** Be security-aware when configuring environment variables
|
||||
containing API keys or tokens
|
||||
- **Environment variable redaction:** By default, the Gemini CLI redacts
|
||||
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
|
||||
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
|
||||
spawning MCP servers using the `stdio` transport. This prevents unintended
|
||||
exposure of your credentials to third-party servers.
|
||||
- **Explicit environment variables:** If you need to pass a specific environment
|
||||
variable to an MCP server, you should define it explicitly in the `env`
|
||||
property of the server configuration in `settings.json`.
|
||||
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
||||
available within the sandbox environment
|
||||
available within the sandbox environment.
|
||||
- **Private data:** Using broadly scoped personal access tokens can lead to
|
||||
information leakage between repositories
|
||||
information leakage between repositories.
|
||||
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
|
||||
untrusted or third-party sources. Malicious servers could attempt to
|
||||
exfiltrate data or perform unauthorized actions through the tools they expose.
|
||||
|
||||
### Performance and resource management
|
||||
|
||||
|
||||
+23
-10
@@ -34,6 +34,10 @@ export type EvalPolicy = 'ALWAYS_PASSES' | 'USUALLY_PASSES';
|
||||
export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
const fn = async () => {
|
||||
const rig = new TestRig();
|
||||
const { logDir, sanitizedName } = await prepareLogDir(evalCase.name);
|
||||
const activityLogFile = path.join(logDir, `${sanitizedName}.jsonl`);
|
||||
const logFile = path.join(logDir, `${sanitizedName}.log`);
|
||||
let isSuccess = false;
|
||||
try {
|
||||
rig.setup(evalCase.name, evalCase.params);
|
||||
|
||||
@@ -62,6 +66,9 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
const result = await rig.run({
|
||||
args: evalCase.prompt,
|
||||
approvalMode: evalCase.approvalMode ?? 'yolo',
|
||||
env: {
|
||||
GEMINI_CLI_ACTIVITY_LOG_FILE: activityLogFile,
|
||||
},
|
||||
});
|
||||
|
||||
const unauthorizedErrorPrefix =
|
||||
@@ -73,9 +80,16 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
}
|
||||
|
||||
await evalCase.assert(rig, result);
|
||||
isSuccess = true;
|
||||
} finally {
|
||||
await logToFile(
|
||||
evalCase.name,
|
||||
if (isSuccess) {
|
||||
await fs.promises.unlink(activityLogFile).catch((err) => {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
});
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
logFile,
|
||||
JSON.stringify(rig.readToolLogs(), null, 2),
|
||||
);
|
||||
await rig.cleanup();
|
||||
@@ -89,6 +103,13 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareLogDir(name: string) {
|
||||
const logDir = path.resolve(process.cwd(), 'evals/logs');
|
||||
await fs.promises.mkdir(logDir, { recursive: true });
|
||||
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
return { logDir, sanitizedName };
|
||||
}
|
||||
|
||||
export interface EvalCase {
|
||||
name: string;
|
||||
params?: Record<string, any>;
|
||||
@@ -97,11 +118,3 @@ export interface EvalCase {
|
||||
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
|
||||
assert: (rig: TestRig, result: string) => Promise<void>;
|
||||
}
|
||||
|
||||
async function logToFile(name: string, content: string) {
|
||||
const logDir = 'evals/logs';
|
||||
await fs.promises.mkdir(logDir, { recursive: true });
|
||||
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
const logFile = `${logDir}/${sanitizedName}.log`;
|
||||
await fs.promises.writeFile(logFile, content);
|
||||
}
|
||||
|
||||
@@ -484,9 +484,19 @@ console.log(JSON.stringify({
|
||||
'hooks-system.before-tool-selection.responses',
|
||||
),
|
||||
});
|
||||
// Create inline hook command (works on both Unix and Windows)
|
||||
const hookCommand =
|
||||
"node -e \"console.log(JSON.stringify({hookSpecificOutput: {hookEventName: 'BeforeToolSelection', toolConfig: {mode: 'ANY', allowedFunctionNames: ['read_file', 'run_shell_command']}}}))\"";
|
||||
|
||||
// Write hook script to file (inline node -e has quoting issues on Windows)
|
||||
const hookScript = `console.log(JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'BeforeToolSelection',
|
||||
toolConfig: {
|
||||
mode: 'ANY',
|
||||
allowedFunctionNames: ['read_file', 'run_shell_command']
|
||||
}
|
||||
}
|
||||
}));`;
|
||||
const scriptPath = join(rig.testDir!, 'before_tool_selection_hook.cjs');
|
||||
writeFileSync(scriptPath, hookScript);
|
||||
|
||||
rig.setup('should modify tool selection with BeforeToolSelection hooks', {
|
||||
settings: {
|
||||
@@ -500,7 +510,7 @@ console.log(JSON.stringify({
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: hookCommand,
|
||||
command: `node "${scriptPath.replace(/\\/g, '/')}"`,
|
||||
timeout: 5000,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I could not find the file `nonexistent-file-that-does-not-exist.txt` in the current directory or its subdirectories. Please verify the file path or name."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":25,"totalTokenCount":35,"promptTokensDetails":[{"modality":"TEXT","tokenCount":10}]}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hello! How can I help you today?"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":9,"totalTokenCount":14,"promptTokensDetails":[{"modality":"TEXT","tokenCount":5}]}}]}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { join } from 'node:path';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe('stdout-stderr-output', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should send model response to stdout and app messages to stderr', async ({
|
||||
signal,
|
||||
}) => {
|
||||
await rig.setup('prompt-output-test', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'stdout-stderr-output.responses',
|
||||
),
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await rig.runWithStreams(['-p', 'Say hello'], {
|
||||
signal,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toLowerCase()).toContain('hello');
|
||||
expect(stdout).not.toMatch(/^\[ERROR\]/m);
|
||||
expect(stdout).not.toMatch(/^\[INFO\]/m);
|
||||
});
|
||||
|
||||
it('should handle missing file with message to stdout and error to stderr', async ({
|
||||
signal,
|
||||
}) => {
|
||||
await rig.setup('error-output-test', {
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'stdout-stderr-output-error.responses',
|
||||
),
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await rig.runWithStreams(
|
||||
['-p', '@nonexistent-file-that-does-not-exist.txt explain this'],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toLowerCase()).toMatch(
|
||||
/could not find|not exist|does not exist/,
|
||||
);
|
||||
});
|
||||
});
|
||||
Generated
+8
-31
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -2251,7 +2251,6 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2432,7 +2431,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2466,7 +2464,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2835,7 +2832,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2869,7 +2865,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1"
|
||||
@@ -2922,7 +2917,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1",
|
||||
@@ -4128,7 +4122,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4406,7 +4399,6 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5399,7 +5391,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8409,7 +8400,6 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8950,7 +8940,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -10552,7 +10541,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -14311,7 +14299,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14322,7 +14309,6 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -16559,7 +16545,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16783,8 +16768,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16792,7 +16776,6 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -16965,7 +16948,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17173,7 +17155,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17287,7 +17268,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17300,7 +17280,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -18005,7 +17984,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -18021,7 +17999,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -18077,7 +18055,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
@@ -18164,7 +18142,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
@@ -18300,7 +18278,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -18323,7 +18300,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18340,7 +18317,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"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.27.0-nightly.20260121.97aac696f"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.27.0-nightly.20260121.97aac696f",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type ExtensionSetting,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import prompts from 'prompts';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
const {
|
||||
mockExtensionManager,
|
||||
@@ -79,11 +80,15 @@ vi.mock('../../config/settings.js', () => ({
|
||||
}));
|
||||
|
||||
describe('extensions configure command', () => {
|
||||
let tempWorkspaceDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(debugLogger, 'log');
|
||||
vi.spyOn(debugLogger, 'error');
|
||||
vi.clearAllMocks();
|
||||
|
||||
tempWorkspaceDir = fs.mkdtempSync('gemini-cli-test-workspace');
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
// Default behaviors
|
||||
mockLoadSettings.mockReturnValue({ merged: {} });
|
||||
mockGetExtensionAndManager.mockResolvedValue({
|
||||
@@ -141,6 +146,7 @@ describe('extensions configure command', () => {
|
||||
'TEST_VAR',
|
||||
promptForSetting,
|
||||
'user',
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -186,6 +192,7 @@ describe('extensions configure command', () => {
|
||||
'VAR_1',
|
||||
promptForSetting,
|
||||
'user',
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ async function configureSpecificSetting(
|
||||
settingKey,
|
||||
promptForSetting,
|
||||
scope,
|
||||
process.cwd(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,6 +219,7 @@ async function configureExtensionSettings(
|
||||
setting.envVar,
|
||||
promptForSetting,
|
||||
scope,
|
||||
process.cwd(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Themes Example
|
||||
|
||||
This is an example of a Gemini CLI extension that adds a custom theme.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Link this extension:
|
||||
|
||||
```bash
|
||||
gemini extensions link packages/cli/src/commands/extensions/examples/themes-example
|
||||
```
|
||||
|
||||
2. Set the theme in your settings file (`~/.gemini/config.yaml`):
|
||||
|
||||
```yaml
|
||||
ui:
|
||||
theme: 'shades-of-green-theme (themes-example)'
|
||||
```
|
||||
|
||||
Alternatively, you can set it through the UI by running `gemini` and then
|
||||
typing `/theme` and pressing Enter.
|
||||
|
||||
3. **Observe the Changes:**
|
||||
|
||||
After setting the theme, you should see the changes reflected in the Gemini
|
||||
CLI's UI. The background will be a dark green, the primary text a lighter
|
||||
green, and various other UI elements will display different shades of green,
|
||||
as defined in this extension's `gemini-extension.json` file.
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "themes-example",
|
||||
"version": "1.0.0",
|
||||
"themes": [
|
||||
{
|
||||
"name": "shades-of-green-theme",
|
||||
"type": "custom",
|
||||
"background": {
|
||||
"primary": "#1a362a"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#a6e3a1",
|
||||
"secondary": "#6e8e7a",
|
||||
"link": "#89e689"
|
||||
},
|
||||
"status": {
|
||||
"success": "#76c076",
|
||||
"warning": "#d9e689",
|
||||
"error": "#b34e4e"
|
||||
},
|
||||
"border": {
|
||||
"default": "#4a6c5a"
|
||||
},
|
||||
"ui": {
|
||||
"comment": "#6e8e7a"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
|
||||
// Mock dependencies
|
||||
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||
const emitFeedback = vi.hoisted(() => vi.fn());
|
||||
const debugLogger = vi.hoisted(() => ({
|
||||
log: vi.fn((message, ...args) => {
|
||||
emitConsoleLog('log', format(message, ...args));
|
||||
@@ -40,6 +41,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
...actual,
|
||||
coreEvents: {
|
||||
emitConsoleLog,
|
||||
emitFeedback,
|
||||
},
|
||||
debugLogger,
|
||||
};
|
||||
@@ -84,6 +86,42 @@ describe('extensions update command', () => {
|
||||
});
|
||||
|
||||
describe('handleUpdate', () => {
|
||||
it('should list installed extensions when requested extension is not found', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const extensions = [
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
{ name: 'ext2', version: '2.0.0' },
|
||||
];
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue(extensions);
|
||||
|
||||
await handleUpdate({ name: 'missing-extension' });
|
||||
|
||||
expect(emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Extension "missing-extension" not found.\n\nInstalled extensions:\next1 (1.0.0)\next2 (2.0.0)\n\nRun "gemini extensions list" for details.',
|
||||
);
|
||||
expect(mockUpdateExtension).not.toHaveBeenCalled();
|
||||
mockCwd.mockRestore();
|
||||
});
|
||||
|
||||
it('should log a helpful message when no extensions are installed and requested extension is not found', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await handleUpdate({ name: 'missing-extension' });
|
||||
|
||||
expect(emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Extension "missing-extension" not found.\n\nNo extensions installed.',
|
||||
);
|
||||
expect(mockUpdateExtension).not.toHaveBeenCalled();
|
||||
mockCwd.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
@@ -46,7 +46,21 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
(extension) => extension.name === args.name,
|
||||
);
|
||||
if (!extension) {
|
||||
debugLogger.log(`Extension "${args.name}" not found.`);
|
||||
if (extensions.length === 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Extension "${args.name}" not found.\n\nNo extensions installed.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const installedExtensions = extensions
|
||||
.map((extension) => `${extension.name} (${extension.version})`)
|
||||
.join('\n');
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Extension "${args.name}" not found.\n\nInstalled extensions:\n${installedExtensions}\n\nRun "gemini extensions list" for details.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!extension.installMetadata) {
|
||||
@@ -63,7 +77,6 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
debugLogger.log(`Extension "${args.name}" is already up to date.`);
|
||||
return;
|
||||
}
|
||||
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
||||
const updatedExtensionInfo = (await updateExtension(
|
||||
extension,
|
||||
extensionManager,
|
||||
|
||||
@@ -128,6 +128,13 @@ async function addMcpServer(
|
||||
|
||||
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
||||
|
||||
if (transport === 'stdio') {
|
||||
debugLogger.warn(
|
||||
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
|
||||
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isExistingServer) {
|
||||
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
||||
} else {
|
||||
|
||||
@@ -588,7 +588,7 @@ describe('parseArguments', () => {
|
||||
process.argv = ['node', 'script.js', 'skills', 'list'];
|
||||
// Skills command enabled by default or via experimental
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { skills: true },
|
||||
skills: { enabled: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.isCommand).toBe(true);
|
||||
|
||||
@@ -304,7 +304,7 @@ export async function parseArguments(
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings.experimental?.skills || (settings.skills?.enabled ?? true)) {
|
||||
if (settings.skills?.enabled ?? true) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
// Register hooks command if hooks are enabled
|
||||
@@ -755,8 +755,7 @@ export async function loadCliConfig(
|
||||
plan: settings.experimental?.plan,
|
||||
enableEventDrivenScheduler:
|
||||
settings.experimental?.enableEventDrivenScheduler,
|
||||
skillsSupport:
|
||||
settings.experimental?.skills || (settings.skills?.enabled ?? true),
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
|
||||
@@ -306,6 +306,7 @@ System using model: \${MODEL_NAME}
|
||||
'MY_VALUE',
|
||||
mockRequestSetting,
|
||||
ExtensionSettingScope.USER,
|
||||
process.cwd(),
|
||||
);
|
||||
|
||||
await extensionManager.restartExtension(extension);
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
beforeAll,
|
||||
afterAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
afterEach,
|
||||
} from 'vitest';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
|
||||
import { GEMINI_DIR, type Config } from '@google/gemini-cli-core';
|
||||
import { createTestMergedSettings, SettingScope } from './settings.js';
|
||||
|
||||
describe('ExtensionManager theme loading', () => {
|
||||
let extensionManager: ExtensionManager;
|
||||
let userExtensionsDir: string;
|
||||
let tempHomeDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempHomeDir = await fs.promises.mkdtemp(
|
||||
path.join(fs.realpathSync('/tmp'), 'gemini-cli-test-'),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (tempHomeDir) {
|
||||
await fs.promises.rm(tempHomeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
process.env['GEMINI_CLI_HOME'] = tempHomeDir;
|
||||
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
|
||||
// Ensure userExtensionsDir is clean for each test
|
||||
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
settings: createTestMergedSettings({
|
||||
experimental: { extensionConfig: true },
|
||||
security: { blockGitExtensions: false },
|
||||
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
|
||||
tools: { enableHooks: true },
|
||||
}),
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
workspaceDir: tempHomeDir,
|
||||
enabledExtensionOverrides: [],
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
themeManager.clearExtensionThemes();
|
||||
themeManager.loadCustomThemes({});
|
||||
themeManager.setActiveTheme(DEFAULT_THEME.name);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env['GEMINI_CLI_HOME'];
|
||||
});
|
||||
|
||||
it('should register themes from an extension when started', async () => {
|
||||
const registerSpy = vi.spyOn(themeManager, 'registerExtensionThemes');
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'my-theme-extension',
|
||||
themes: [
|
||||
{
|
||||
name: 'My-Awesome-Theme',
|
||||
type: 'custom',
|
||||
text: {
|
||||
primary: '#FF00FF',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const mockConfig = {
|
||||
getEnableExtensionReloading: () => false,
|
||||
getMcpClientManager: () => ({
|
||||
startExtension: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
getGeminiClient: () => ({
|
||||
isInitialized: () => false,
|
||||
updateSystemInstruction: vi.fn(),
|
||||
setTools: vi.fn(),
|
||||
}),
|
||||
getHookSystem: () => undefined,
|
||||
getWorkingDir: () => tempHomeDir,
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
getDebugMode: () => false,
|
||||
getFileExclusions: () => ({
|
||||
isIgnored: () => false,
|
||||
}),
|
||||
getGeminiMdFilePaths: () => [],
|
||||
getMcpServers: () => ({}),
|
||||
getAllowedMcpServers: () => [],
|
||||
getSanitizationConfig: () => ({
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
}),
|
||||
getShellExecutionConfig: () => ({
|
||||
terminalWidth: 80,
|
||||
terminalHeight: 24,
|
||||
showColor: false,
|
||||
pager: 'cat',
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
getToolRegistry: () => ({
|
||||
getTools: () => [],
|
||||
}),
|
||||
getProxy: () => undefined,
|
||||
getFileService: () => ({
|
||||
findFiles: async () => [],
|
||||
}),
|
||||
getExtensionLoader: () => ({
|
||||
getExtensions: () => [],
|
||||
}),
|
||||
isTrustedFolder: () => true,
|
||||
getImportFormat: () => 'tree',
|
||||
} as unknown as Config;
|
||||
|
||||
await extensionManager.start(mockConfig);
|
||||
|
||||
expect(registerSpy).toHaveBeenCalledWith('my-theme-extension', [
|
||||
{
|
||||
name: 'My-Awesome-Theme',
|
||||
type: 'custom',
|
||||
text: {
|
||||
primary: '#FF00FF',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should revert to default theme when extension is stopped', async () => {
|
||||
const extensionName = 'my-theme-extension';
|
||||
const themeName = 'My-Awesome-Theme';
|
||||
const namespacedThemeName = `${themeName} (${extensionName})`;
|
||||
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: extensionName,
|
||||
themes: [
|
||||
{
|
||||
name: themeName,
|
||||
type: 'custom',
|
||||
text: {
|
||||
primary: '#FF00FF',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
const mockConfig = {
|
||||
getWorkingDir: () => tempHomeDir,
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
getWorkspaceContext: () => ({
|
||||
getDirectories: () => [],
|
||||
}),
|
||||
getDebugMode: () => false,
|
||||
getFileService: () => ({
|
||||
findFiles: async () => [],
|
||||
}),
|
||||
getExtensionLoader: () => ({
|
||||
getExtensions: () => [],
|
||||
}),
|
||||
isTrustedFolder: () => true,
|
||||
getImportFormat: () => 'tree',
|
||||
getFileFilteringOptions: () => ({
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
}),
|
||||
getDiscoveryMaxDirs: () => 200,
|
||||
getMcpClientManager: () => ({
|
||||
getMcpInstructions: () => '',
|
||||
startExtension: vi.fn().mockResolvedValue(undefined),
|
||||
stopExtension: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
setUserMemory: vi.fn(),
|
||||
setGeminiMdFileCount: vi.fn(),
|
||||
setGeminiMdFilePaths: vi.fn(),
|
||||
getEnableExtensionReloading: () => true,
|
||||
getGeminiClient: () => ({
|
||||
isInitialized: () => false,
|
||||
updateSystemInstruction: vi.fn(),
|
||||
setTools: vi.fn(),
|
||||
}),
|
||||
getHookSystem: () => undefined,
|
||||
getProxy: () => undefined,
|
||||
getAgentRegistry: () => ({
|
||||
reload: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
await extensionManager.start(mockConfig);
|
||||
|
||||
// Set the active theme to the one from the extension
|
||||
themeManager.setActiveTheme(namespacedThemeName);
|
||||
expect(themeManager.getActiveTheme().name).toBe(namespacedThemeName);
|
||||
|
||||
// Stop the extension
|
||||
await extensionManager.disableExtension(extensionName, SettingScope.User);
|
||||
|
||||
// Check that the active theme has reverted to the default
|
||||
expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name);
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
ExtensionSettingScope,
|
||||
} from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { themeManager } from '../ui/themes/theme-manager.js';
|
||||
|
||||
interface ExtensionManagerParams {
|
||||
enabledExtensionOverrides?: string[];
|
||||
@@ -437,6 +438,19 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
extensionIdentifier.toLowerCase(),
|
||||
);
|
||||
if (!extension) {
|
||||
const extensionDir = path.join(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
extensionIdentifier,
|
||||
);
|
||||
if (fs.existsSync(extensionDir)) {
|
||||
await fs.promises.rm(extensionDir, { recursive: true, force: true });
|
||||
this.extensionEnablementManager.remove(extensionIdentifier);
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
`Uninstalled broken extension '${extensionIdentifier}'.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Extension not found.`);
|
||||
}
|
||||
await this.unloadExtension(extension);
|
||||
@@ -468,6 +482,20 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
);
|
||||
}
|
||||
|
||||
protected override async startExtension(extension: GeminiCLIExtension) {
|
||||
await super.startExtension(extension);
|
||||
if (extension.themes) {
|
||||
themeManager.registerExtensionThemes(extension.name, extension.themes);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async stopExtension(extension: GeminiCLIExtension) {
|
||||
await super.stopExtension(extension);
|
||||
if (extension.themes) {
|
||||
themeManager.unregisterExtensionThemes(extension.name, extension.themes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all installed extensions, should only be called once.
|
||||
*/
|
||||
@@ -500,25 +528,24 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
extensionDir: string,
|
||||
): Promise<GeminiCLIExtension | null> {
|
||||
this.loadedExtensions ??= [];
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const installMetadata = loadInstallMetadata(extensionDir);
|
||||
let effectiveExtensionPath = extensionDir;
|
||||
if (
|
||||
(installMetadata?.type === 'git' ||
|
||||
installMetadata?.type === 'github-release') &&
|
||||
this.settings.security.blockGitExtensions
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (installMetadata?.type === 'link') {
|
||||
effectiveExtensionPath = installMetadata.source;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const installMetadata = loadInstallMetadata(extensionDir);
|
||||
if (
|
||||
(installMetadata?.type === 'git' ||
|
||||
installMetadata?.type === 'github-release') &&
|
||||
this.settings.security.blockGitExtensions
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (installMetadata?.type === 'link') {
|
||||
effectiveExtensionPath = installMetadata.source;
|
||||
}
|
||||
let config = await this.loadExtensionConfig(effectiveExtensionPath);
|
||||
if (
|
||||
this.getExtensions().find((extension) => extension.name === config.name)
|
||||
@@ -698,6 +725,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
resolvedSettings,
|
||||
skills,
|
||||
agents: agentLoadResult.agents,
|
||||
themes: config.themes,
|
||||
};
|
||||
this.loadedExtensions = [...this.loadedExtensions, extension];
|
||||
|
||||
|
||||
@@ -1769,6 +1769,39 @@ ${INSTALL_WARNING_MESSAGE}`,
|
||||
});
|
||||
|
||||
describe('uninstallExtension', () => {
|
||||
it('should uninstall a broken extension without crashing', async () => {
|
||||
const badExtDir = path.join(userExtensionsDir, 'broken-ext');
|
||||
fs.mkdirSync(badExtDir);
|
||||
// Malformed JSON to simulate a broken extension
|
||||
fs.writeFileSync(
|
||||
path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME),
|
||||
'{ "name": "broken-ext"',
|
||||
);
|
||||
|
||||
// Attempt to load extensions. The broken one should be skipped.
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
await extensionManager.loadExtensions();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badExtDir}/gemini-extension.json`,
|
||||
),
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
|
||||
// Ensure no extensions were loaded
|
||||
expect(extensionManager.getExtensions()).toHaveLength(0);
|
||||
|
||||
// Attempt to uninstall the broken extension by its name.
|
||||
await expect(
|
||||
extensionManager.uninstallExtension('broken-ext', false),
|
||||
).resolves.toBeUndefined(); // Should resolve, not throw an error
|
||||
|
||||
// Verify the directory is removed
|
||||
expect(fs.existsSync(badExtDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('should uninstall an extension by name', async () => {
|
||||
const sourceExtDir = createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
ExtensionInstallMetadata,
|
||||
CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -27,6 +28,11 @@ export interface ExtensionConfig {
|
||||
contextFileName?: string | string[];
|
||||
excludeTools?: string[];
|
||||
settings?: ExtensionSetting[];
|
||||
/**
|
||||
* Custom themes contributed by this extension.
|
||||
* These themes will be registered when the extension is activated.
|
||||
*/
|
||||
themes?: CustomTheme[];
|
||||
}
|
||||
|
||||
export interface ExtensionUpdateInfo {
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function updateSetting(
|
||||
settingKey: string,
|
||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
workspaceDir: string,
|
||||
): Promise<void> {
|
||||
const { name: extensionName, settings } = extensionConfig;
|
||||
if (!settings || settings.length === 0) {
|
||||
|
||||
@@ -85,6 +85,7 @@ export enum Command {
|
||||
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
|
||||
CLEAR_SCREEN = 'app.clearScreen',
|
||||
RESTART_APP = 'app.restart',
|
||||
SUSPEND_APP = 'app.suspend',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,8 +171,15 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
],
|
||||
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
|
||||
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
|
||||
[Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
|
||||
[Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
|
||||
[Command.UNDO]: [
|
||||
{ key: 'z', cmd: true, shift: false },
|
||||
{ key: 'z', alt: true, shift: false },
|
||||
],
|
||||
[Command.REDO]: [
|
||||
{ key: 'z', ctrl: true, shift: true },
|
||||
{ key: 'z', cmd: true, shift: true },
|
||||
{ key: 'z', alt: true, shift: true },
|
||||
],
|
||||
|
||||
// Scrolling
|
||||
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
|
||||
@@ -265,6 +273,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
|
||||
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
|
||||
[Command.RESTART_APP]: [{ key: 'r' }],
|
||||
[Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }],
|
||||
};
|
||||
|
||||
interface CommandCategory {
|
||||
@@ -374,6 +383,7 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
Command.UNFOCUS_SHELL_INPUT,
|
||||
Command.CLEAR_SCREEN,
|
||||
Command.RESTART_APP,
|
||||
Command.SUSPEND_APP,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -464,4 +474,5 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
|
||||
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
||||
[Command.RESTART_APP]: 'Restart the application.',
|
||||
[Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).',
|
||||
};
|
||||
|
||||
@@ -2302,18 +2302,18 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should set skills based on advancedFeaturesEnabled', () => {
|
||||
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||
const loadedSettings = loadSettings();
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
cliFeatureSetting: {
|
||||
advancedFeaturesEnabled: true,
|
||||
unmanagedCapabilitiesEnabled: true,
|
||||
},
|
||||
});
|
||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
cliFeatureSetting: {
|
||||
advancedFeaturesEnabled: false,
|
||||
unmanagedCapabilitiesEnabled: false,
|
||||
},
|
||||
});
|
||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(false);
|
||||
|
||||
@@ -361,10 +361,9 @@ export class LoadedSettings {
|
||||
admin.extensions = {
|
||||
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled ?? false,
|
||||
};
|
||||
|
||||
if (cliFeatureSetting?.advancedFeaturesEnabled !== undefined) {
|
||||
admin.skills = { enabled: cliFeatureSetting.advancedFeaturesEnabled };
|
||||
}
|
||||
admin.skills = {
|
||||
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
|
||||
};
|
||||
|
||||
this._remoteAdminSettings = { admin };
|
||||
this._merged = this.computeMergedSettings();
|
||||
|
||||
@@ -9,19 +9,17 @@
|
||||
// to regenerate the settings reference in `docs/get-started/configuration.md`.
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
BugCommandSettings,
|
||||
TelemetrySettings,
|
||||
AuthType,
|
||||
AgentOverride,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
DEFAULT_MODEL_CONFIGS,
|
||||
type MCPServerConfig,
|
||||
type BugCommandSettings,
|
||||
type TelemetrySettings,
|
||||
type AuthType,
|
||||
type AgentOverride,
|
||||
type CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||
import type { SessionRetentionSettings } from './settings.js';
|
||||
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
|
||||
|
||||
@@ -526,6 +524,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Show the model name in the chat for each model turn.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showUserIdentity: {
|
||||
type: 'boolean',
|
||||
label: 'Show User Identity',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
"Show the logged-in user's identity (e.g. email) in the UI.",
|
||||
showInDialog: true,
|
||||
},
|
||||
useAlternateBuffer: {
|
||||
type: 'boolean',
|
||||
label: 'Use Alternate Screen Buffer',
|
||||
@@ -1490,15 +1498,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||
showInDialog: false,
|
||||
},
|
||||
skills: {
|
||||
type: 'boolean',
|
||||
label: 'Agent Skills (Deprecated)',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: '[Deprecated] Enable Agent Skills (experimental).',
|
||||
showInDialog: false,
|
||||
},
|
||||
useOSC52Paste: {
|
||||
type: 'boolean',
|
||||
label: 'Use OSC 52 Paste',
|
||||
@@ -1602,11 +1601,11 @@ const SETTINGS_SCHEMA = {
|
||||
type: 'boolean',
|
||||
label: 'Enable Hooks',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description:
|
||||
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
|
||||
showInDialog: false,
|
||||
showInDialog: true,
|
||||
},
|
||||
disabled: {
|
||||
type: 'array',
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { loadCliConfig, parseArguments } from './config.js';
|
||||
import * as trustedFolders from './trustedFolders.js';
|
||||
import { loadServerHierarchicalMemory } from '@google/gemini-cli-core';
|
||||
import { type Settings, createTestMergedSettings } from './settings.js';
|
||||
|
||||
vi.mock('./trustedFolders.js');
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
loadServerHierarchicalMemory: vi.fn(),
|
||||
getPty: vi.fn().mockResolvedValue({ name: 'test-pty' }),
|
||||
getVersion: vi.fn().mockResolvedValue('0.0.0-test'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Agent Skills Backward Compatibility', () => {
|
||||
const originalArgv = process.argv;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(trustedFolders.isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
} as unknown as trustedFolders.TrustResult);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
});
|
||||
|
||||
describe('loadCliConfig', () => {
|
||||
it('should default skillsSupport to true when no settings are present', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const settings = createTestMergedSettings({});
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize skills.enabled=false from settings', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
skills: { enabled: false },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should support legacy experimental.skills=true from settings', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { skills: true },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize legacy experimental.skills=true over new skills.enabled=false', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
skills: { enabled: false },
|
||||
experimental: { skills: true },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should still be enabled by default if legacy experimental.skills is false (since new default is true)', async () => {
|
||||
vi.mocked(loadServerHierarchicalMemory).mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { skills: false },
|
||||
} as unknown as Settings);
|
||||
|
||||
process.argv = ['node', 'gemini'];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
'test-session',
|
||||
await parseArguments(settings),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
config as unknown as { isSkillsSupportEnabled: () => boolean }
|
||||
).isSkillsSupportEnabled(),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,11 @@ import type { LoadedSettings } from './config/settings.js';
|
||||
// Mock core modules
|
||||
vi.mock('./ui/hooks/atCommandProcessor.js');
|
||||
|
||||
const mockRegisterActivityLogger = vi.hoisted(() => vi.fn());
|
||||
vi.mock('./utils/activityLogger.js', () => ({
|
||||
registerActivityLogger: mockRegisterActivityLogger,
|
||||
}));
|
||||
|
||||
const mockCoreEvents = vi.hoisted(() => ({
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
@@ -259,6 +264,52 @@ describe('runNonInteractive', () => {
|
||||
// so we no longer expect shutdownTelemetry to be called directly here
|
||||
});
|
||||
|
||||
it('should register activity logger when GEMINI_CLI_ACTIVITY_LOG_FILE is set', async () => {
|
||||
vi.stubEnv('GEMINI_CLI_ACTIVITY_LOG_FILE', '/tmp/test.jsonl');
|
||||
const events: ServerGeminiStreamEvent[] = [
|
||||
{
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
||||
},
|
||||
];
|
||||
mockGeminiClient.sendMessageStream.mockReturnValue(
|
||||
createStreamFromEvents(events),
|
||||
);
|
||||
|
||||
await runNonInteractive({
|
||||
config: mockConfig,
|
||||
settings: mockSettings,
|
||||
input: 'test',
|
||||
prompt_id: 'prompt-id-activity-logger',
|
||||
});
|
||||
|
||||
expect(mockRegisterActivityLogger).toHaveBeenCalledWith(mockConfig);
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should not register activity logger when GEMINI_CLI_ACTIVITY_LOG_FILE is not set', async () => {
|
||||
vi.stubEnv('GEMINI_CLI_ACTIVITY_LOG_FILE', '');
|
||||
const events: ServerGeminiStreamEvent[] = [
|
||||
{
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
||||
},
|
||||
];
|
||||
mockGeminiClient.sendMessageStream.mockReturnValue(
|
||||
createStreamFromEvents(events),
|
||||
);
|
||||
|
||||
await runNonInteractive({
|
||||
config: mockConfig,
|
||||
settings: mockSettings,
|
||||
input: 'test',
|
||||
prompt_id: 'prompt-id-activity-logger-off',
|
||||
});
|
||||
|
||||
expect(mockRegisterActivityLogger).not.toHaveBeenCalled();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should handle a single tool call and respond', async () => {
|
||||
const toolCallEvent: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
|
||||
@@ -70,6 +70,14 @@ export async function runNonInteractive({
|
||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||
},
|
||||
});
|
||||
|
||||
if (config.storage && process.env['GEMINI_CLI_ACTIVITY_LOG_FILE']) {
|
||||
const { registerActivityLogger } = await import(
|
||||
'./utils/activityLogger.js'
|
||||
);
|
||||
registerActivityLogger(config);
|
||||
}
|
||||
|
||||
const { stdout: workingStdout } = createWorkingStdio();
|
||||
const textOutput = new TextOutput(workingStdout);
|
||||
|
||||
|
||||
@@ -119,11 +119,12 @@ describe('BuiltinCommandLoader', () => {
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
getExtensionsEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
|
||||
isAgentsEnabled: vi.fn().mockReturnValue(false),
|
||||
getMcpEnabled: vi.fn().mockReturnValue(true),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
getAllSkills: vi.fn().mockReturnValue([]),
|
||||
isAdminEnabled: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -260,11 +261,12 @@ describe('BuiltinCommandLoader profile', () => {
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
getExtensionsEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
|
||||
isAgentsEnabled: vi.fn().mockReturnValue(false),
|
||||
getMcpEnabled: vi.fn().mockReturnValue(true),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
getAllSkills: vi.fn().mockReturnValue([]),
|
||||
isAdminEnabled: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
@@ -9,12 +9,13 @@ import * as path from 'node:path';
|
||||
import {
|
||||
type MCPServerConfig,
|
||||
type ExtensionInstallMetadata,
|
||||
type ExtensionSetting,
|
||||
type CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
INSTALL_METADATA_FILENAME,
|
||||
} from '../config/extensions/variables.js';
|
||||
import type { ExtensionSetting } from '../config/extensions/extensionSettings.js';
|
||||
|
||||
export function createExtension({
|
||||
extensionsDir = 'extensions-dir',
|
||||
@@ -25,12 +26,20 @@ export function createExtension({
|
||||
mcpServers = {} as Record<string, MCPServerConfig>,
|
||||
installMetadata = undefined as ExtensionInstallMetadata | undefined,
|
||||
settings = undefined as ExtensionSetting[] | undefined,
|
||||
themes = undefined as CustomTheme[] | undefined,
|
||||
} = {}): string {
|
||||
const extDir = path.join(extensionsDir, name);
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
||||
JSON.stringify({ name, version, contextFileName, mcpServers, settings }),
|
||||
JSON.stringify({
|
||||
name,
|
||||
version,
|
||||
contextFileName,
|
||||
mcpServers,
|
||||
settings,
|
||||
themes,
|
||||
}),
|
||||
);
|
||||
|
||||
if (addContextFile) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { act, useContext, type ReactElement } from 'react';
|
||||
import { AppContainer } from './AppContainer.js';
|
||||
import { SettingsContext } from './contexts/SettingsContext.js';
|
||||
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
|
||||
import { MessageType } from './types.js';
|
||||
import {
|
||||
type Config,
|
||||
makeFakeConfig,
|
||||
@@ -28,8 +29,16 @@ import {
|
||||
type UserFeedbackPayload,
|
||||
type ResumedSessionData,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
type ContentGeneratorConfig,
|
||||
type AgentDefinition,
|
||||
MessageBusType,
|
||||
QuestionType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
AskUserActionsContext,
|
||||
type AskUserState,
|
||||
} from './contexts/AskUserActionsContext.js';
|
||||
|
||||
// Mock coreEvents
|
||||
const mockCoreEvents = vi.hoisted(() => ({
|
||||
@@ -44,6 +53,11 @@ const mockIdeClient = vi.hoisted(() => ({
|
||||
getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||
}));
|
||||
|
||||
// Mock UserAccountManager
|
||||
const mockUserAccountManager = vi.hoisted(() => ({
|
||||
getCachedGoogleAccount: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
// Mock stdout
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockStdout: { write: vi.fn() },
|
||||
@@ -73,6 +87,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
})),
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
UserAccountManager: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockUserAccountManager),
|
||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn(),
|
||||
})),
|
||||
@@ -107,9 +124,11 @@ vi.mock('ink', async (importOriginal) => {
|
||||
// so we can assert against them in our tests.
|
||||
let capturedUIState: UIState;
|
||||
let capturedUIActions: UIActions;
|
||||
let capturedAskUserRequest: AskUserState | null;
|
||||
function TestContextConsumer() {
|
||||
capturedUIState = useContext(UIStateContext)!;
|
||||
capturedUIActions = useContext(UIActionsContext)!;
|
||||
capturedAskUserRequest = useContext(AskUserActionsContext)?.request ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -185,6 +204,7 @@ import {
|
||||
disableMouseEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
|
||||
|
||||
describe('AppContainer State Management', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -259,6 +279,7 @@ describe('AppContainer State Management', () => {
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
capturedUIState = null!;
|
||||
capturedAskUserRequest = null;
|
||||
|
||||
// **Provide a default return value for EVERY mocked hook.**
|
||||
mockedUseQuotaAndFallback.mockReturnValue({
|
||||
@@ -395,6 +416,7 @@ describe('AppContainer State Management', () => {
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: false,
|
||||
hideWindowTitle: false,
|
||||
showUserIdentity: true,
|
||||
},
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
@@ -466,6 +488,162 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication Check', () => {
|
||||
it('displays correct message for LOGIN_WITH_GOOGLE auth type', async () => {
|
||||
// Explicitly mock implementation to ensure we control the instance
|
||||
(UserAccountManager as unknown as Mock).mockImplementation(
|
||||
() => mockUserAccountManager,
|
||||
);
|
||||
|
||||
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
|
||||
'test@example.com',
|
||||
);
|
||||
const mockAddItem = vi.fn();
|
||||
mockedUseHistory.mockReturnValue({
|
||||
history: [],
|
||||
addItem: mockAddItem,
|
||||
updateItem: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
// Explicitly enable showUserIdentity
|
||||
mockSettings.merged.ui = {
|
||||
...mockSettings.merged.ui,
|
||||
showUserIdentity: true,
|
||||
};
|
||||
|
||||
// Need to ensure config.getContentGeneratorConfig() returns appropriate authType
|
||||
const authConfig = makeFakeConfig();
|
||||
// Mock getTargetDir as well since makeFakeConfig might not set it up fully for the component
|
||||
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||
mockExtensionManager,
|
||||
);
|
||||
|
||||
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ config: authConfig });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(UserAccountManager).toHaveBeenCalled();
|
||||
expect(
|
||||
mockUserAccountManager.getCachedGoogleAccount,
|
||||
).toHaveBeenCalled();
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: 'Logged in with Google: test@example.com (Plan: Standard Tier)',
|
||||
}),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
it('displays correct message for USE_GEMINI auth type', async () => {
|
||||
// Explicitly mock implementation to ensure we control the instance
|
||||
(UserAccountManager as unknown as Mock).mockImplementation(
|
||||
() => mockUserAccountManager,
|
||||
);
|
||||
|
||||
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(null);
|
||||
const mockAddItem = vi.fn();
|
||||
mockedUseHistory.mockReturnValue({
|
||||
history: [],
|
||||
addItem: mockAddItem,
|
||||
updateItem: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
const authConfig = makeFakeConfig();
|
||||
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||
mockExtensionManager,
|
||||
);
|
||||
|
||||
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ config: authConfig });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: expect.stringContaining('Authenticated with gemini-api-key'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display authentication message if showUserIdentity is false', async () => {
|
||||
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
|
||||
'test@example.com',
|
||||
);
|
||||
const mockAddItem = vi.fn();
|
||||
mockedUseHistory.mockReturnValue({
|
||||
history: [],
|
||||
addItem: mockAddItem,
|
||||
updateItem: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
mockSettings.merged.ui = {
|
||||
...mockSettings.merged.ui,
|
||||
showUserIdentity: false,
|
||||
};
|
||||
|
||||
const authConfig = makeFakeConfig();
|
||||
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||
mockExtensionManager,
|
||||
);
|
||||
|
||||
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ config: authConfig });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
// Give it some time to potentially call addItem
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Context Providers', () => {
|
||||
it('provides AppContext with correct values', async () => {
|
||||
let unmount: () => void;
|
||||
@@ -1891,7 +2069,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
});
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
@@ -1936,7 +2114,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
});
|
||||
|
||||
pressKey({ name: 'd', ctrl: true });
|
||||
@@ -2498,6 +2676,41 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should show ask user dialog when request is received', async () => {
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
const questions = [
|
||||
{
|
||||
question: 'What is your favorite color?',
|
||||
header: 'Color Preference',
|
||||
type: QuestionType.TEXT,
|
||||
},
|
||||
];
|
||||
|
||||
await act(async () => {
|
||||
await mockConfig.getMessageBus().publish({
|
||||
type: MessageBusType.ASK_USER_REQUEST,
|
||||
questions,
|
||||
correlationId: 'test-id',
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(capturedAskUserRequest).not.toBeNull();
|
||||
expect(capturedAskUserRequest?.questions).toEqual(questions);
|
||||
expect(capturedAskUserRequest?.correlationId).toBe('test-id');
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regression Tests', () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
getErrorMessage,
|
||||
getAllGeminiMdFilenames,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
clearCachedCredentialFile,
|
||||
type ResumedSessionData,
|
||||
recordExitFail,
|
||||
@@ -186,11 +187,58 @@ const SHELL_HEIGHT_PADDING = 10;
|
||||
|
||||
export const AppContainer = (props: AppContainerProps) => {
|
||||
const { config, initializationResult, resumedSessionData } = props;
|
||||
const settings = useSettings();
|
||||
|
||||
const historyManager = useHistory({
|
||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||
});
|
||||
const { addItem } = historyManager;
|
||||
|
||||
const authCheckPerformed = useRef(false);
|
||||
useEffect(() => {
|
||||
if (authCheckPerformed.current) return;
|
||||
authCheckPerformed.current = true;
|
||||
|
||||
if (resumedSessionData || settings.merged.ui.showUserIdentity === false) {
|
||||
return;
|
||||
}
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
|
||||
// Run this asynchronously to avoid blocking the event loop.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
(async () => {
|
||||
try {
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const email = userAccountManager.getCachedGoogleAccount();
|
||||
const tierName = config.getUserTierName();
|
||||
|
||||
if (authType) {
|
||||
let message =
|
||||
authType === AuthType.LOGIN_WITH_GOOGLE
|
||||
? email
|
||||
? `Logged in with Google: ${email}`
|
||||
: 'Logged in with Google'
|
||||
: `Authenticated with ${authType}`;
|
||||
if (tierName) {
|
||||
message += ` (Plan: ${tierName})`;
|
||||
}
|
||||
addItem({
|
||||
type: MessageType.INFO,
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore errors during initial auth check
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
config,
|
||||
resumedSessionData,
|
||||
settings.merged.ui.showUserIdentity,
|
||||
addItem,
|
||||
]);
|
||||
|
||||
useMemoryMonitor(historyManager);
|
||||
const settings = useSettings();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||
@@ -1443,6 +1491,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
||||
setShowErrorDetails((prev) => !prev);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
||||
handleWarning('Undo has been moved to Cmd + Z or Alt/Opt + Z');
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
||||
setShowFullTodos((prev) => !prev);
|
||||
return true;
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('bugCommand', () => {
|
||||
getHistory: () => [],
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -106,6 +107,7 @@ describe('bugCommand', () => {
|
||||
* **Operating System:** test-platform v20.0.0
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** gemini-pro
|
||||
* **Auth Type:** oauth-personal
|
||||
* **Memory Usage:** 100 MB
|
||||
* **Terminal Name:** Test Terminal
|
||||
* **Terminal Background:** #000000
|
||||
@@ -133,6 +135,7 @@ describe('bugCommand', () => {
|
||||
getHistory: () => history,
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp/gemini',
|
||||
},
|
||||
@@ -178,6 +181,7 @@ describe('bugCommand', () => {
|
||||
getHistory: () => [],
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -192,6 +196,7 @@ describe('bugCommand', () => {
|
||||
* **Operating System:** test-platform v20.0.0
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** gemini-pro
|
||||
* **Auth Type:** vertex-ai
|
||||
* **Memory Usage:** 100 MB
|
||||
* **Terminal Name:** Test Terminal
|
||||
* **Terminal Background:** #000000
|
||||
|
||||
@@ -54,6 +54,7 @@ export const bugCommand: SlashCommand = {
|
||||
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
|
||||
? 'Supported'
|
||||
: 'Unsupported';
|
||||
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
|
||||
|
||||
let info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
@@ -62,6 +63,7 @@ export const bugCommand: SlashCommand = {
|
||||
* **Operating System:** ${osVersion}
|
||||
* **Sandbox Environment:** ${sandboxEnv}
|
||||
* **Model Version:** ${modelVersion}
|
||||
* **Auth Type:** ${authType}
|
||||
* **Memory Usage:** ${memoryUsage}
|
||||
* **Terminal Name:** ${terminalName}
|
||||
* **Terminal Background:** ${terminalBgColor}
|
||||
|
||||
@@ -67,6 +67,9 @@ describe('directoryCommand', () => {
|
||||
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
|
||||
getGeminiClient: vi.fn().mockReturnValue({
|
||||
addDirectoryContext: vi.fn(),
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
recordDirectories: vi.fn(),
|
||||
}),
|
||||
}),
|
||||
getWorkingDir: () => path.resolve('/test/dir'),
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
|
||||
@@ -57,6 +57,13 @@ async function finishAddingDirectories(
|
||||
const gemini = config.getGeminiClient();
|
||||
if (gemini) {
|
||||
await gemini.addDirectoryContext();
|
||||
|
||||
// Persist directories to session file for resume support
|
||||
const chatRecordingService = gemini.getChatRecordingService();
|
||||
const workspaceContext = config.getWorkspaceContext();
|
||||
chatRecordingService?.recordDirectories(
|
||||
workspaceContext.getDirectories(),
|
||||
);
|
||||
}
|
||||
addItem({
|
||||
type: MessageType.INFO,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MessageType } from '../types.js';
|
||||
import type { HookRegistryEntry } from '@google/gemini-cli-core';
|
||||
import { HookType, HookEventName, ConfigSource } from '@google/gemini-cli-core';
|
||||
import type { CommandContext } from './types.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
describe('hooksCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
@@ -34,6 +35,11 @@ describe('hooksCommand', () => {
|
||||
};
|
||||
};
|
||||
setValue: ReturnType<typeof vi.fn>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
workspace: { path: string; settings: any };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
user: { path: string; settings: any };
|
||||
forScope: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -56,6 +62,17 @@ describe('hooksCommand', () => {
|
||||
};
|
||||
|
||||
// Create mock settings
|
||||
const mockUser = {
|
||||
path: '/mock/user.json',
|
||||
settings: { hooksConfig: { disabled: [] } },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
const mockWorkspace = {
|
||||
path: '/mock/workspace.json',
|
||||
settings: { hooksConfig: { disabled: [] } },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
mockSettings = {
|
||||
merged: {
|
||||
hooksConfig: {
|
||||
@@ -63,7 +80,15 @@ describe('hooksCommand', () => {
|
||||
},
|
||||
},
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
workspace: mockWorkspace,
|
||||
user: mockUser,
|
||||
forScope: vi.fn((scope) => {
|
||||
if (scope === SettingScope.User) return mockUser;
|
||||
if (scope === SettingScope.Workspace) return mockWorkspace;
|
||||
return mockUser;
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
// Create mock context with config and settings
|
||||
mockContext = createMockCommandContext({
|
||||
@@ -272,11 +297,12 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should enable a hook and update settings', async () => {
|
||||
// Update the context's settings with disabled hooks
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [
|
||||
// Update the user settings with disabled hooks
|
||||
mockSettings.user.settings.hooksConfig.disabled = [
|
||||
'test-hook',
|
||||
'other-hook',
|
||||
];
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = [];
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
@@ -288,7 +314,7 @@ describe('hooksCommand', () => {
|
||||
const result = await enableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
SettingScope.User,
|
||||
'hooksConfig.disabled',
|
||||
['other-hook'],
|
||||
);
|
||||
@@ -299,28 +325,8 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Hook "test-hook" enabled successfully.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle error when enabling hook fails', async () => {
|
||||
mockSettings.setValue.mockImplementationOnce(() => {
|
||||
throw new Error('Failed to save settings');
|
||||
});
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
);
|
||||
if (!enableCmd?.action) {
|
||||
throw new Error('enable command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to enable hook: Failed to save settings',
|
||||
content:
|
||||
'Hook "test-hook" enabled by removing it from the disabled list in user (/mock/user.json) and workspace (/mock/workspace.json) settings.',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -332,7 +338,7 @@ describe('hooksCommand', () => {
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
true,
|
||||
false, // Must be disabled for enable completion
|
||||
);
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
|
||||
@@ -404,7 +410,9 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should disable a hook and update settings', async () => {
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [];
|
||||
// Ensure not disabled anywhere
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = [];
|
||||
mockSettings.user.settings.hooksConfig.disabled = [];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -415,8 +423,9 @@ describe('hooksCommand', () => {
|
||||
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
// Should default to workspace if present
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
SettingScope.Workspace,
|
||||
'hooksConfig.disabled',
|
||||
['test-hook'],
|
||||
);
|
||||
@@ -427,13 +436,14 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Hook "test-hook" disabled successfully.',
|
||||
content:
|
||||
'Hook "test-hook" disabled by adding it to the disabled list in workspace (/mock/workspace.json) settings.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should synchronize with hook system even if hook is already in disabled list', async () => {
|
||||
// Update the context's settings with the hook already disabled
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = ['test-hook'];
|
||||
it('should return info when hook is already disabled', async () => {
|
||||
// Update the context's settings with the hook already disabled in Workspace
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = ['test-hook'];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -445,38 +455,29 @@ describe('hooksCommand', () => {
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
|
||||
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||
'test-hook',
|
||||
false,
|
||||
);
|
||||
expect(mockConfig.updateDisabledHooks).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Hook "test-hook" disabled successfully.',
|
||||
content: 'Hook "test-hook" is already disabled.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle error when disabling hook fails', async () => {
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [];
|
||||
mockSettings.setValue.mockImplementationOnce(() => {
|
||||
throw new Error('Failed to save settings');
|
||||
});
|
||||
|
||||
it('should complete hook names using friendly names', () => {
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
true, // Must be enabled for disable completion
|
||||
);
|
||||
if (!disableCmd?.action) {
|
||||
throw new Error('disable command must have an action');
|
||||
}
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
mockHookSystem.getAllHooks.mockReturnValue([hookEntry]);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to disable hook: Failed to save settings',
|
||||
});
|
||||
const completions = disableCmd.completion!(mockContext, 'frie');
|
||||
expect(completions).toContain('friendly-name');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -513,50 +514,52 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return matching hook names', () => {
|
||||
it('should return matching hook names based on status', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('test-hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook-2', HookEventName.AfterTool, true),
|
||||
createMockHook('other-hook', HookEventName.AfterAgent, false),
|
||||
createMockHook('test-hook-enabled', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook-disabled', HookEventName.AfterTool, false),
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
);
|
||||
if (!enableCmd?.completion) {
|
||||
throw new Error('enable command must have completion');
|
||||
}
|
||||
)!;
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const result = enableCmd.completion(mockContext, 'test');
|
||||
expect(result).toEqual(['test-hook-1', 'test-hook-2']);
|
||||
const enableResult = enableCmd.completion!(mockContext, 'test');
|
||||
expect(enableResult).toEqual(['test-hook-disabled']);
|
||||
|
||||
const disableResult = disableCmd.completion!(mockContext, 'test');
|
||||
expect(disableResult).toEqual(['test-hook-enabled']);
|
||||
});
|
||||
|
||||
it('should return all hook names when partial is empty', () => {
|
||||
it('should return all relevant hook names when partial is empty', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||
createMockHook('hook-enabled', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-disabled', HookEventName.AfterTool, false),
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
);
|
||||
if (!enableCmd?.completion) {
|
||||
throw new Error('enable command must have completion');
|
||||
}
|
||||
)!;
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const result = enableCmd.completion(mockContext, '');
|
||||
expect(result).toEqual(['hook-1', 'hook-2']);
|
||||
expect(enableCmd.completion!(mockContext, '')).toEqual(['hook-disabled']);
|
||||
expect(disableCmd.completion!(mockContext, '')).toEqual(['hook-enabled']);
|
||||
});
|
||||
|
||||
it('should handle hooks without command name gracefully', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('test-hook', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook', HookEventName.BeforeTool, false),
|
||||
{
|
||||
...createMockHook('', HookEventName.AfterTool, true),
|
||||
...createMockHook('', HookEventName.AfterTool, false),
|
||||
config: { command: '', type: HookType.Command, timeout: 30 },
|
||||
},
|
||||
];
|
||||
@@ -636,7 +639,7 @@ describe('hooksCommand', () => {
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String), // enableAll uses legacy logic so it might return 'Workspace' or 'User' depending on ternary
|
||||
'hooksConfig.disabled',
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -12,7 +12,9 @@ import type {
|
||||
MessageActionReturn,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { SettingScope, isLoadableSettingScope } from '../../config/settings.js';
|
||||
import { enableHook, disableHook } from '../../utils/hookSettings.js';
|
||||
import { renderHookActionFeedback } from '../../utils/hookUtils.js';
|
||||
|
||||
/**
|
||||
* Display a formatted list of hooks with their status
|
||||
@@ -74,39 +76,23 @@ async function enableAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const disabledHooks = settings.merged.hooksConfig.disabled;
|
||||
// Remove from disabled list if present
|
||||
const newDisabledHooks = disabledHooks.filter(
|
||||
(name: string) => name !== hookName,
|
||||
const result = enableHook(settings, hookName);
|
||||
|
||||
if (result.status === 'success') {
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
}
|
||||
|
||||
const feedback = renderHookActionFeedback(
|
||||
result,
|
||||
(label, path) => `${label} (${path})`,
|
||||
);
|
||||
|
||||
// Update settings (setValue automatically saves)
|
||||
try {
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
|
||||
// Enable in hook system
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Hook "${hookName}" enabled successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to enable hook: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: result.status === 'error' ? 'error' : 'info',
|
||||
content: feedback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,44 +129,31 @@ async function disableAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const disabledHooks = settings.merged.hooksConfig.disabled;
|
||||
// Add to disabled list if not already present
|
||||
try {
|
||||
if (!disabledHooks.includes(hookName)) {
|
||||
const newDisabledHooks = [...disabledHooks, hookName];
|
||||
const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User;
|
||||
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
||||
}
|
||||
const result = disableHook(settings, hookName, scope);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
|
||||
// Always disable in hook system to ensure in-memory state matches settings
|
||||
if (result.status === 'success') {
|
||||
hookSystem.setHookEnabled(hookName, false);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Hook "${hookName}" disabled successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to disable hook: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const feedback = renderHookActionFeedback(
|
||||
result,
|
||||
(label, path) => `${label} (${path})`,
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: result.status === 'error' ? 'error' : 'info',
|
||||
content: feedback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion function for hook names
|
||||
* Completion function for enabled hook names (to be disabled)
|
||||
*/
|
||||
function completeHookNames(
|
||||
function completeEnabledHookNames(
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): string[] {
|
||||
@@ -191,8 +164,30 @@ function completeHookNames(
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
const hookNames = allHooks.map((hook) => getHookDisplayName(hook));
|
||||
return hookNames.filter((name) => name.startsWith(partialArg));
|
||||
return allHooks
|
||||
.filter((hook) => hook.enabled)
|
||||
.map((hook) => getHookDisplayName(hook))
|
||||
.filter((name) => name.startsWith(partialArg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion function for disabled hook names (to be enabled)
|
||||
*/
|
||||
function completeDisabledHookNames(
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): string[] {
|
||||
const { config } = context.services;
|
||||
if (!config) return [];
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
return allHooks
|
||||
.filter((hook) => !hook.enabled)
|
||||
.map((hook) => getHookDisplayName(hook))
|
||||
.filter((name) => name.startsWith(partialArg));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,13 +242,12 @@ async function enableAllAction(
|
||||
}
|
||||
|
||||
try {
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', []);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
const scopes = [SettingScope.Workspace, SettingScope.User];
|
||||
for (const scope of scopes) {
|
||||
if (isLoadableSettingScope(scope)) {
|
||||
settings.setValue(scope, 'hooksConfig.disabled', []);
|
||||
}
|
||||
}
|
||||
|
||||
for (const hook of disabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
@@ -325,9 +319,6 @@ async function disableAllAction(
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', allHookNames);
|
||||
|
||||
// Update core config so re-initialization (e.g. extension reload) respects the change
|
||||
config.updateDisabledHooks(settings.merged.hooksConfig.disabled);
|
||||
|
||||
for (const hook of enabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
hookSystem.setHookEnabled(hookName, false);
|
||||
@@ -361,7 +352,7 @@ const enableCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: enableAction,
|
||||
completion: completeHookNames,
|
||||
completion: completeDisabledHookNames,
|
||||
};
|
||||
|
||||
const disableCommand: SlashCommand = {
|
||||
@@ -370,7 +361,7 @@ const disableCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: disableAction,
|
||||
completion: completeHookNames,
|
||||
completion: completeEnabledHookNames,
|
||||
};
|
||||
|
||||
const enableAllCommand: SlashCommand = {
|
||||
|
||||
@@ -12,6 +12,17 @@ import { MessageType } from '../types.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
UserAccountManager: vi.fn().mockImplementation(() => ({
|
||||
getCachedGoogleAccount: vi.fn().mockReturnValue('mock@example.com'),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('statsCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
const startTime = new Date('2025-07-14T10:00:00.000Z');
|
||||
@@ -40,6 +51,9 @@ describe('statsCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.STATS,
|
||||
duration: expectedDuration,
|
||||
selectedAuthType: '',
|
||||
tier: undefined,
|
||||
userEmail: 'mock@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,8 +62,10 @@ describe('statsCommand', () => {
|
||||
|
||||
const mockQuota = { buckets: [] };
|
||||
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
|
||||
const mockGetUserTierName = vi.fn().mockReturnValue('Basic');
|
||||
mockContext.services.config = {
|
||||
refreshUserQuota: mockRefreshUserQuota,
|
||||
getUserTierName: mockGetUserTierName,
|
||||
} as unknown as Config;
|
||||
|
||||
await statsCommand.action(mockContext, '');
|
||||
@@ -58,6 +74,7 @@ describe('statsCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
quotas: mockQuota,
|
||||
tier: 'Basic',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -73,6 +90,9 @@ describe('statsCommand', () => {
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.MODEL_STATS,
|
||||
selectedAuthType: '',
|
||||
tier: undefined,
|
||||
userEmail: 'mock@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,15 +4,33 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { HistoryItemStats } from '../types.js';
|
||||
import type {
|
||||
HistoryItemStats,
|
||||
HistoryItemModelStats,
|
||||
HistoryItemToolStats,
|
||||
} from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import { UserAccountManager } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
|
||||
function getUserIdentity(context: CommandContext) {
|
||||
const selectedAuthType =
|
||||
context.services.settings.merged.security.auth.selectedType || '';
|
||||
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const cachedAccount = userAccountManager.getCachedGoogleAccount();
|
||||
const userEmail = cachedAccount ?? undefined;
|
||||
|
||||
const tier = context.services.config?.getUserTierName();
|
||||
|
||||
return { selectedAuthType, userEmail, tier };
|
||||
}
|
||||
|
||||
async function defaultSessionView(context: CommandContext) {
|
||||
const now = new Date();
|
||||
const { sessionStartTime } = context.session.stats;
|
||||
@@ -25,9 +43,14 @@ async function defaultSessionView(context: CommandContext) {
|
||||
}
|
||||
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
||||
|
||||
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
|
||||
|
||||
const statsItem: HistoryItemStats = {
|
||||
type: MessageType.STATS,
|
||||
duration: formatDuration(wallDuration),
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
};
|
||||
|
||||
if (context.services.config) {
|
||||
@@ -65,9 +88,13 @@ export const statsCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context: CommandContext) => {
|
||||
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
|
||||
context.ui.addItem({
|
||||
type: MessageType.MODEL_STATS,
|
||||
});
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
} as HistoryItemModelStats);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,7 +105,7 @@ export const statsCommand: SlashCommand = {
|
||||
action: (context: CommandContext) => {
|
||||
context.ui.addItem({
|
||||
type: MessageType.TOOL_STATS,
|
||||
});
|
||||
} as HistoryItemToolStats);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { AboutBox } from './AboutBox.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('AboutBox', () => {
|
||||
};
|
||||
|
||||
it('renders with required props', () => {
|
||||
const { lastFrame } = render(<AboutBox {...defaultProps} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...defaultProps} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('About Gemini CLI');
|
||||
expect(output).toContain('1.0.0');
|
||||
@@ -42,7 +42,7 @@ describe('AboutBox', () => {
|
||||
['tier', 'Enterprise', 'Tier'],
|
||||
])('renders optional prop %s', (prop, value, label) => {
|
||||
const props = { ...defaultProps, [prop]: value };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(label);
|
||||
expect(output).toContain(value);
|
||||
@@ -50,14 +50,14 @@ describe('AboutBox', () => {
|
||||
|
||||
it('renders Auth Method with email when userEmail is provided', () => {
|
||||
const props = { ...defaultProps, userEmail: 'test@example.com' };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Logged in with Google (test@example.com)');
|
||||
});
|
||||
|
||||
it('renders Auth Method correctly when not oauth', () => {
|
||||
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('api-key');
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
interface AboutBoxProps {
|
||||
cliVersion: string;
|
||||
@@ -31,123 +32,130 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
ideClient,
|
||||
userEmail,
|
||||
tier,
|
||||
}) => (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
marginY={1}
|
||||
width="100%"
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
About Gemini CLI
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
CLI Version
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
marginY={1}
|
||||
width="100%"
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
About Gemini CLI
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Git Commit
|
||||
CLI Version
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Model
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Sandbox
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{sandboxEnv}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
OS
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{osVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Auth Method
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{selectedAuthType.startsWith('oauth')
|
||||
? userEmail
|
||||
? `Logged in with Google (${userEmail})`
|
||||
: 'Logged in with Google'
|
||||
: selectedAuthType}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{tier && (
|
||||
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Git Commit
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Tier
|
||||
Model
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{tier}</Text>
|
||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{gcpProject && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
GCP Project
|
||||
Sandbox
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{gcpProject}</Text>
|
||||
<Text color={theme.text.primary}>{sandboxEnv}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{ideClient && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
IDE Client
|
||||
OS
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{ideClient}</Text>
|
||||
<Text color={theme.text.primary}>{osVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
{showUserIdentity && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Auth Method
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{selectedAuthType.startsWith('oauth')
|
||||
? userEmail
|
||||
? `Logged in with Google (${userEmail})`
|
||||
: 'Logged in with Google'
|
||||
: selectedAuthType}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{showUserIdentity && tier && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Tier
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{tier}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{gcpProject && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
GCP Project
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{gcpProject}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{ideClient && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
IDE Client
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{ideClient}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -122,9 +122,18 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
<StatsDisplay
|
||||
duration={itemForDisplay.duration}
|
||||
quotas={itemForDisplay.quotas}
|
||||
selectedAuthType={itemForDisplay.selectedAuthType}
|
||||
userEmail={itemForDisplay.userEmail}
|
||||
tier={itemForDisplay.tier}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'model_stats' && (
|
||||
<ModelStatsDisplay
|
||||
selectedAuthType={itemForDisplay.selectedAuthType}
|
||||
userEmail={itemForDisplay.userEmail}
|
||||
tier={itemForDisplay.tier}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'model_stats' && <ModelStatsDisplay />}
|
||||
{itemForDisplay.type === 'tool_stats' && <ToolStatsDisplay />}
|
||||
{itemForDisplay.type === 'model' && (
|
||||
<ModelMessage model={itemForDisplay.model} />
|
||||
|
||||
@@ -8,6 +8,8 @@ import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import * as SettingsContext from '../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -20,7 +22,16 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../contexts/SettingsContext.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SettingsContext>();
|
||||
return {
|
||||
...actual,
|
||||
useSettings: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
const useSettingsMock = vi.mocked(SettingsContext.useSettings);
|
||||
|
||||
const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
@@ -36,6 +47,14 @@ const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
useSettingsMock.mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
showUserIdentity: true,
|
||||
},
|
||||
},
|
||||
} as unknown as LoadedSettings);
|
||||
|
||||
return render(<ModelStatsDisplay />, width);
|
||||
};
|
||||
|
||||
@@ -368,4 +387,74 @@ describe('<ModelStatsDisplay />', () => {
|
||||
expect(output).toContain('gemini-3-flash-');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render user identity information when provided', () => {
|
||||
useSettingsMock.mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
showUserIdentity: true,
|
||||
},
|
||||
},
|
||||
} as unknown as LoadedSettings);
|
||||
|
||||
const { lastFrame } = render(
|
||||
<ModelStatsDisplay
|
||||
selectedAuthType="oauth"
|
||||
userEmail="test@example.com"
|
||||
tier="Pro"
|
||||
/>,
|
||||
);
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
metrics: {
|
||||
models: {
|
||||
'gemini-2.5-pro': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
tokens: {
|
||||
input: 10,
|
||||
prompt: 10,
|
||||
candidates: 20,
|
||||
total: 30,
|
||||
cached: 0,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Auth Method:');
|
||||
expect(output).toContain('Logged in with Google');
|
||||
expect(output).toContain('(test@example.com)');
|
||||
expect(output).toContain('Tier:');
|
||||
expect(output).toContain('Pro');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../utils/computeStats.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { Table, type Column } from './Table.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
interface StatRowData {
|
||||
metric: string;
|
||||
@@ -24,9 +25,21 @@ interface StatRowData {
|
||||
[key: string]: string | React.ReactNode | boolean | undefined;
|
||||
}
|
||||
|
||||
export const ModelStatsDisplay: React.FC = () => {
|
||||
interface ModelStatsDisplayProps {
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
tier?: string;
|
||||
}
|
||||
|
||||
export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { models } = stats.metrics;
|
||||
const settings = useSettings();
|
||||
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||
const activeModels = Object.entries(models).filter(
|
||||
([, metrics]) => metrics.api.totalRequests > 0,
|
||||
);
|
||||
@@ -75,10 +88,12 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
return row;
|
||||
};
|
||||
|
||||
const rows: StatRowData[] = [
|
||||
// API Section
|
||||
{ metric: 'API', isSection: true },
|
||||
createRow('Requests', (m) => m.api.totalRequests.toLocaleString()),
|
||||
const rows: StatRowData[] = [];
|
||||
|
||||
// API Section
|
||||
rows.push({ metric: 'API', isSection: true });
|
||||
rows.push(createRow('Requests', (m) => m.api.totalRequests.toLocaleString()));
|
||||
rows.push(
|
||||
createRow('Errors', (m) => {
|
||||
const errorRate = calculateErrorRate(m);
|
||||
return (
|
||||
@@ -91,18 +106,24 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
</Text>
|
||||
);
|
||||
}),
|
||||
);
|
||||
rows.push(
|
||||
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
|
||||
);
|
||||
|
||||
// Spacer
|
||||
{ metric: '' },
|
||||
// Spacer
|
||||
rows.push({ metric: '' });
|
||||
|
||||
// Tokens Section
|
||||
{ metric: 'Tokens', isSection: true },
|
||||
// Tokens Section
|
||||
rows.push({ metric: 'Tokens', isSection: true });
|
||||
rows.push(
|
||||
createRow('Total', (m) => (
|
||||
<Text color={theme.text.secondary}>
|
||||
{m.tokens.total.toLocaleString()}
|
||||
</Text>
|
||||
)),
|
||||
);
|
||||
rows.push(
|
||||
createRow(
|
||||
'Input',
|
||||
(m) => (
|
||||
@@ -112,7 +133,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
),
|
||||
{ isSubtle: true },
|
||||
),
|
||||
];
|
||||
);
|
||||
|
||||
if (hasCached) {
|
||||
rows.push(
|
||||
@@ -214,6 +235,31 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
Model Stats For Nerds
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
|
||||
{showUserIdentity && selectedAuthType && (
|
||||
<Box>
|
||||
<Box width={28}>
|
||||
<Text color={theme.text.link}>Auth Method:</Text>
|
||||
</Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{selectedAuthType.startsWith('oauth')
|
||||
? userEmail
|
||||
? `Logged in with Google (${userEmail})`
|
||||
: 'Logged in with Google'
|
||||
: selectedAuthType}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{showUserIdentity && tier && (
|
||||
<Box>
|
||||
<Box width={28}>
|
||||
<Text color={theme.text.link}>Tier:</Text>
|
||||
</Box>
|
||||
<Text color={theme.text.primary}>{tier}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{showUserIdentity && (selectedAuthType || tier) && <Box height={1} />}
|
||||
|
||||
<Table data={rows} columns={columns} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
@@ -35,7 +35,9 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
return render(<SessionSummaryDisplay duration="1h 23m 45s" />);
|
||||
return renderWithProviders(<SessionSummaryDisplay duration="1h 23m 45s" />, {
|
||||
width: 100,
|
||||
});
|
||||
};
|
||||
|
||||
describe('<SessionSummaryDisplay />', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
@@ -39,7 +39,7 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
return render(<StatsDisplay duration="1s" />);
|
||||
return renderWithProviders(<StatsDisplay duration="1s" />, { width: 100 });
|
||||
};
|
||||
|
||||
// Helper to create metrics with default zero values
|
||||
@@ -381,8 +381,9 @@ describe('<StatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Agent powering down. Goodbye!');
|
||||
@@ -439,8 +440,9 @@ describe('<StatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -484,8 +486,9 @@ describe('<StatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -498,4 +501,64 @@ describe('<StatsDisplay />', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Identity Display', () => {
|
||||
it('renders User row with Auth Method and Tier', () => {
|
||||
const metrics = createTestMetrics();
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StatsDisplay
|
||||
duration="1s"
|
||||
selectedAuthType="oauth"
|
||||
userEmail="test@example.com"
|
||||
tier="Pro"
|
||||
/>,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Auth Method:');
|
||||
expect(output).toContain('Logged in with Google (test@example.com)');
|
||||
expect(output).toContain('Tier:');
|
||||
expect(output).toContain('Pro');
|
||||
});
|
||||
|
||||
it('renders User row with API Key and no Tier', () => {
|
||||
const metrics = createTestMetrics();
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StatsDisplay duration="1s" selectedAuthType="Google API Key" />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Auth Method:');
|
||||
expect(output).toContain('Google API Key');
|
||||
expect(output).not.toContain('Tier:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type RetrieveUserQuotaResponse,
|
||||
VALID_GEMINI_MODELS,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
// A more flexible and powerful StatRow component
|
||||
interface StatRowProps {
|
||||
@@ -364,17 +365,25 @@ interface StatsDisplayProps {
|
||||
duration: string;
|
||||
title?: string;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
tier?: string;
|
||||
}
|
||||
|
||||
export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
duration,
|
||||
title,
|
||||
quotas,
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { metrics } = stats;
|
||||
const { models, tools, files } = metrics;
|
||||
const computed = computeSessionStats(metrics);
|
||||
const settings = useSettings();
|
||||
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||
|
||||
const successThresholds = {
|
||||
green: TOOL_SUCCESS_RATE_HIGH,
|
||||
@@ -417,6 +426,22 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
<StatRow title="Session ID:">
|
||||
<Text color={theme.text.primary}>{stats.sessionId}</Text>
|
||||
</StatRow>
|
||||
{showUserIdentity && selectedAuthType && (
|
||||
<StatRow title="Auth Method:">
|
||||
<Text color={theme.text.primary}>
|
||||
{selectedAuthType.startsWith('oauth')
|
||||
? userEmail
|
||||
? `Logged in with Google (${userEmail})`
|
||||
: 'Logged in with Google'
|
||||
: selectedAuthType}
|
||||
</Text>
|
||||
</StatRow>
|
||||
)}
|
||||
{showUserIdentity && tier && (
|
||||
<StatRow title="Tier:">
|
||||
<Text color={theme.text.primary}>{tier}</Text>
|
||||
</StatRow>
|
||||
)}
|
||||
<StatRow title="Tool Calls:">
|
||||
<Text color={theme.text.primary}>
|
||||
{tools.totalCalls} ({' '}
|
||||
|
||||
@@ -109,15 +109,6 @@ export function ThemeDialog({
|
||||
},
|
||||
);
|
||||
|
||||
// Generate theme items filtered by selected scope
|
||||
const customThemes =
|
||||
selectedScope === SettingScope.User
|
||||
? settings.user.settings.ui?.customThemes || {}
|
||||
: settings.merged.ui.customThemes;
|
||||
const builtInThemes = themeManager
|
||||
.getAvailableThemes()
|
||||
.filter((theme) => theme.type !== 'custom');
|
||||
const customThemeNames = Object.keys(customThemes);
|
||||
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
const terminalThemeType = getThemeTypeFromBackgroundColor(
|
||||
@@ -125,8 +116,9 @@ export function ThemeDialog({
|
||||
);
|
||||
|
||||
// Generate theme items
|
||||
const themeItems = [
|
||||
...builtInThemes.map((theme) => {
|
||||
const themeItems = themeManager
|
||||
.getAvailableThemes()
|
||||
.map((theme) => {
|
||||
const fullTheme = themeManager.getTheme(theme.name);
|
||||
const themeBackground = fullTheme
|
||||
? resolveColor(fullTheme.colors.Background)
|
||||
@@ -140,28 +132,14 @@ export function ThemeDialog({
|
||||
terminalBackgroundColor,
|
||||
terminalThemeType,
|
||||
);
|
||||
}),
|
||||
...customThemeNames.map((name) => {
|
||||
const themeConfig = customThemes[name];
|
||||
const bg = themeConfig.background?.primary ?? themeConfig.Background;
|
||||
const themeBackground = bg ? resolveColor(bg) : undefined;
|
||||
|
||||
return generateThemeItem(
|
||||
name,
|
||||
'Custom',
|
||||
'custom',
|
||||
themeBackground,
|
||||
terminalBackgroundColor,
|
||||
terminalThemeType,
|
||||
);
|
||||
}),
|
||||
].sort((a, b) => {
|
||||
// Show compatible themes first
|
||||
if (a.isCompatible && !b.isCompatible) return -1;
|
||||
if (!a.isCompatible && b.isCompatible) return 1;
|
||||
// Then sort by name
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Show compatible themes first
|
||||
if (a.isCompatible && !b.isCompatible) return -1;
|
||||
if (!a.isCompatible && b.isCompatible) return 1;
|
||||
// Then sort by name
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
// Find the index of the selected theme, but only if it exists in the list
|
||||
const initialThemeIndex = themeItems.findIndex(
|
||||
@@ -314,9 +292,20 @@ export function ThemeDialog({
|
||||
};
|
||||
|
||||
if (item.themeNameDisplay && item.themeTypeDisplay) {
|
||||
const match = item.themeNameDisplay.match(/^(.*) \((.*)\)$/);
|
||||
let themeNamePart: React.ReactNode = item.themeNameDisplay;
|
||||
if (match) {
|
||||
themeNamePart = (
|
||||
<>
|
||||
{match[1]}{' '}
|
||||
<Text color={theme.text.secondary}>({match[2]})</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text color={titleColor} wrap="truncate" key={item.key}>
|
||||
{item.themeNameDisplay}{' '}
|
||||
{themeNamePart}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{item.themeTypeDisplay}
|
||||
</Text>
|
||||
|
||||
@@ -137,6 +137,56 @@ describe('ToolConfirmationQueue', () => {
|
||||
expect(lastFrame()).toContain('Press ctrl-o to show more lines');
|
||||
});
|
||||
|
||||
it('calculates availableContentHeight based on availableTerminalHeight from UI state', async () => {
|
||||
const longDiff = '@@ -1,1 +1,50 @@\n' + '+line\n'.repeat(50);
|
||||
const confirmingTool = {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'replace',
|
||||
description: 'edit file',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'edit' as const,
|
||||
title: 'Confirm edit',
|
||||
fileName: 'test.ts',
|
||||
filePath: '/test.ts',
|
||||
fileDiff: longDiff,
|
||||
originalContent: 'old',
|
||||
newContent: 'new',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
},
|
||||
index: 1,
|
||||
total: 1,
|
||||
};
|
||||
|
||||
// Use a small availableTerminalHeight to force truncation
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationQueue
|
||||
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
|
||||
/>,
|
||||
{
|
||||
config: mockConfig,
|
||||
useAlternateBuffer: false,
|
||||
uiState: {
|
||||
terminalWidth: 80,
|
||||
terminalHeight: 40,
|
||||
availableTerminalHeight: 10,
|
||||
constrainHeight: true,
|
||||
streamingState: StreamingState.WaitingForConfirmation,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// With availableTerminalHeight = 10:
|
||||
// maxHeight = Math.max(10 - 1, 4) = 9
|
||||
// availableContentHeight = Math.max(9 - 6, 4) = 4
|
||||
// MaxSizedBox in ToolConfirmationMessage will use 4
|
||||
// It should show truncation message
|
||||
await waitFor(() => expect(lastFrame()).toContain('first 49 lines hidden'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not render expansion hint when constrainHeight is false', () => {
|
||||
const longDiff = 'line\n'.repeat(50);
|
||||
const confirmingTool = {
|
||||
|
||||
@@ -26,17 +26,23 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const { mainAreaWidth, terminalHeight, constrainHeight } = useUIState();
|
||||
const {
|
||||
mainAreaWidth,
|
||||
terminalHeight,
|
||||
constrainHeight,
|
||||
availableTerminalHeight: uiAvailableHeight,
|
||||
} = useUIState();
|
||||
const { tool, index, total } = confirmingTool;
|
||||
|
||||
// Safety check: ToolConfirmationMessage requires confirmationDetails
|
||||
if (!tool.confirmationDetails) return null;
|
||||
|
||||
// V1: Constrain the queue to at most 50% of the terminal height to ensure
|
||||
// some history is always visible and to prevent flickering.
|
||||
// We pass this to ToolConfirmationMessage so it can calculate internal
|
||||
// truncation while keeping buttons visible.
|
||||
const maxHeight = Math.floor(terminalHeight * 0.5);
|
||||
// Render up to 100% of the available terminal height (minus 1 line for safety)
|
||||
// to maximize space for diffs and other content.
|
||||
const maxHeight =
|
||||
uiAvailableHeight !== undefined
|
||||
? Math.max(uiAvailableHeight - 1, 4)
|
||||
: Math.floor(terminalHeight * 0.5);
|
||||
|
||||
// ToolConfirmationMessage needs to know the height available for its OWN content.
|
||||
// We subtract the lines used by the Queue wrapper:
|
||||
|
||||
@@ -413,371 +413,3 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render accessibility settings enabled correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render all boolean settings disabled correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false* │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false* │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update false* │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render default state correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render file filtering settings configured correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render focused on scope selector correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ > Apply To │
|
||||
│ ● 1. User Settings │
|
||||
│ 2. Workspace Settings │
|
||||
│ 3. System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render mixed boolean and number settings correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render tools and security settings correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render various boolean settings enabled correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) true* │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolConfirmationQueue > calculates availableContentHeight based on availableTerminalHeight from UI state 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required 1 of 1 │
|
||||
│ │
|
||||
│ ? replace edit file │
|
||||
│ │
|
||||
│ ... first 49 lines hidden ... │
|
||||
│ 50 line │
|
||||
│ Apply this change? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. Modify with external editor │
|
||||
│ 4. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Press ctrl-o to show more lines
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationQueue > does not render expansion hint when constrainHeight is false 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required 1 of 1 │
|
||||
|
||||
@@ -8,14 +8,19 @@ import {
|
||||
renderWithProviders,
|
||||
createMockSettings,
|
||||
} from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { ASK_USER_DISPLAY_NAME, makeFakeConfig } from '@google/gemini-cli-core';
|
||||
import os from 'node:os';
|
||||
|
||||
describe('<ToolGroupMessage />', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const createToolCall = (
|
||||
overrides: Partial<IndividualToolCallDisplay> = {},
|
||||
): IndividualToolCallDisplay => ({
|
||||
@@ -35,16 +40,16 @@ describe('<ToolGroupMessage />', () => {
|
||||
isFocused: true,
|
||||
};
|
||||
|
||||
const baseMockConfig = {
|
||||
getModel: () => 'gemini-pro',
|
||||
getTargetDir: () => '/test',
|
||||
getDebugMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getEnableInteractiveShell: () => true,
|
||||
getPreviewFeatures: () => false,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
const baseMockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
debugMode: false,
|
||||
folderTrust: false,
|
||||
ideMode: false,
|
||||
enableInteractiveShell: true,
|
||||
previewFeatures: false,
|
||||
enableEventDrivenScheduler: true,
|
||||
});
|
||||
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', () => {
|
||||
@@ -83,10 +88,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Error,
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -116,10 +122,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -177,10 +184,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Pending,
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -381,10 +389,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
describe('Border Color Logic', () => {
|
||||
it('uses yellow border when tools are pending', () => {
|
||||
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -503,10 +512,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -539,10 +549,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
const settings = createMockSettings({
|
||||
security: { enablePermanentToolApproval: true },
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -574,10 +585,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -604,10 +616,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
const mockConfig = baseMockConfig;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -640,10 +649,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
const mockConfig = baseMockConfig;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -657,5 +663,98 @@ describe('<ToolGroupMessage />', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders nothing when only tool is in-progress AskUser with borderBottom=false', () => {
|
||||
// AskUser tools in progress are rendered by AskUserDialog, not ToolGroupMessage.
|
||||
// When AskUser is the only tool and borderBottom=false (no border to close),
|
||||
// the component should render nothing.
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'ask-user-tool',
|
||||
name: 'Ask User',
|
||||
status: ToolCallStatus.Executing,
|
||||
}),
|
||||
];
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={toolCalls}
|
||||
borderBottom={false}
|
||||
/>,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ask User Filtering', () => {
|
||||
it.each([
|
||||
ToolCallStatus.Pending,
|
||||
ToolCallStatus.Executing,
|
||||
ToolCallStatus.Confirming,
|
||||
])('filters out ask_user when status is %s', (status) => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: `ask-user-${status}`,
|
||||
name: ASK_USER_DISPLAY_NAME,
|
||||
status,
|
||||
}),
|
||||
];
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([ToolCallStatus.Success, ToolCallStatus.Error])(
|
||||
'does NOT filter out ask_user when status is %s',
|
||||
(status) => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: `ask-user-${status}`,
|
||||
name: ASK_USER_DISPLAY_NAME,
|
||||
status,
|
||||
}),
|
||||
];
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it('shows other tools when ask_user is filtered out', () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'other-tool',
|
||||
name: 'other-tool',
|
||||
status: ToolCallStatus.Success,
|
||||
}),
|
||||
createToolCall({
|
||||
callId: 'ask-user-pending',
|
||||
name: ASK_USER_DISPLAY_NAME,
|
||||
status: ToolCallStatus.Pending,
|
||||
}),
|
||||
];
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,13 +116,11 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
[toolCalls, isEventDriven],
|
||||
);
|
||||
|
||||
// If all tools are hidden (e.g. group only contains confirming or pending tools),
|
||||
// render nothing in the history log unless we have a border override.
|
||||
if (
|
||||
visibleToolCalls.length === 0 &&
|
||||
borderTopOverride === undefined &&
|
||||
borderBottomOverride === undefined
|
||||
) {
|
||||
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools
|
||||
// in event-driven mode), only render if we need to close a border from previous
|
||||
// tool groups. borderBottomOverride=true means we must render the closing border;
|
||||
// undefined or false means there's nothing to display.
|
||||
if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ x Ask User │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ Ask User │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Executing 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ other-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
@@ -80,6 +110,8 @@ exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialo
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ success-tool A tool for testing │
|
||||
|
||||
@@ -822,7 +822,7 @@ Return a JSON object with:
|
||||
minHeight={Math.min(candLines.length, VISIBLE_LINES_DETAIL)}
|
||||
>
|
||||
{candViewLines.map((line: string, i: number) => (
|
||||
<Text key={i} wrap="truncate-end">
|
||||
<Text key={i} wrap="wrap">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
@@ -881,7 +881,7 @@ Return a JSON object with:
|
||||
minHeight={Math.min(targetLines.length, visibleLines)}
|
||||
>
|
||||
{targetViewLines.map((line, i) => (
|
||||
<Text key={i} italic wrap="truncate-end">
|
||||
<Text key={i} italic wrap="wrap">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
@@ -949,7 +949,7 @@ Return a JSON object with:
|
||||
? 'blue'
|
||||
: undefined
|
||||
}
|
||||
wrap="truncate-end"
|
||||
wrap="wrap"
|
||||
>
|
||||
{absoluteIndex + 1}. <Text bold>#{c.number}</Text>{' '}
|
||||
<Text color={getStateColor(c.state, c.stateReason)}>
|
||||
@@ -963,7 +963,7 @@ Return a JSON object with:
|
||||
- {c.title} (Score: {c.score}/100)
|
||||
</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text color="gray" wrap="truncate-end">
|
||||
<Text color="gray" wrap="wrap">
|
||||
Reactions: {getReactionCount(c)} | {c.reason}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -997,14 +997,14 @@ Return a JSON object with:
|
||||
<Text bold color="blue">
|
||||
Analysis:{' '}
|
||||
</Text>
|
||||
<Text wrap="truncate-end"> {state.message}</Text>
|
||||
<Text wrap="wrap"> {state.message}</Text>
|
||||
</Box>
|
||||
{state.suggestedComment && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="gray">
|
||||
Suggested Comment:
|
||||
</Text>
|
||||
<Text italic color="gray" wrap="truncate-end">
|
||||
<Text italic color="gray" wrap="wrap">
|
||||
"{state.suggestedComment}"
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -29,7 +29,7 @@ export const TOOL_STATUS = {
|
||||
// Maximum number of MCP resources to display per server before truncating
|
||||
export const MAX_MCP_RESOURCES_TO_SHOW = 10;
|
||||
|
||||
export const WARNING_PROMPT_DURATION_MS = 1000;
|
||||
export const WARNING_PROMPT_DURATION_MS = 3000;
|
||||
export const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
||||
export const SHELL_ACTION_REQUIRED_TITLE_DELAY_MS = 30000;
|
||||
export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000;
|
||||
|
||||
@@ -110,8 +110,8 @@ export const INFORMATIVE_TIPS = [
|
||||
'Delete from the cursor to the end of the line with Ctrl+K…',
|
||||
'Clear the entire input prompt with a double-press of Esc…',
|
||||
'Paste from your clipboard with Ctrl+V…',
|
||||
'Undo text edits in the input with Ctrl+Z…',
|
||||
'Redo undone text edits with Ctrl+Shift+Z…',
|
||||
'Undo text edits in the input with Cmd+Z or Alt+Z…',
|
||||
'Redo undone text edits with Shift+Cmd+Z or Shift+Alt+Z…',
|
||||
'Open the current prompt in an external editor with Ctrl+X…',
|
||||
'In menus, move up/down with k/j or the arrow keys…',
|
||||
'In menus, select an item by typing its number…',
|
||||
|
||||
@@ -24,9 +24,8 @@ interface AskUserActionsContextValue {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
const AskUserActionsContext = createContext<AskUserActionsContextValue | null>(
|
||||
null,
|
||||
);
|
||||
export const AskUserActionsContext =
|
||||
createContext<AskUserActionsContextValue | null>(null);
|
||||
|
||||
export const useAskUserActions = () => {
|
||||
const context = useContext(AskUserActionsContext);
|
||||
|
||||
@@ -124,6 +124,8 @@ function charLengthAt(str: string, i: number): number {
|
||||
return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
|
||||
}
|
||||
|
||||
// Note: we do not convert alt+z, alt+shift+z, or alt+v here
|
||||
// because mac users have alternative hotkeys.
|
||||
const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
||||
'\u222B': 'b', // "∫" back one word
|
||||
'\u0192': 'f', // "ƒ" forward one word
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
debugLogger,
|
||||
runInDevTraceSpan,
|
||||
EDIT_TOOL_NAMES,
|
||||
ASK_USER_TOOL_NAME,
|
||||
processRestorableToolCalls,
|
||||
recordToolCallInteractions,
|
||||
ToolErrorType,
|
||||
@@ -367,6 +368,14 @@ export const useGeminiStream = (
|
||||
const isEventDriven = config.isEventDrivenSchedulerEnabled();
|
||||
const anyVisibleInHistory = pushedToolCallIds.size > 0;
|
||||
const anyVisibleInPending = remainingTools.some((tc) => {
|
||||
// AskUser tools are rendered by AskUserDialog, not ToolGroupMessage
|
||||
const isInProgress =
|
||||
tc.status !== 'success' &&
|
||||
tc.status !== 'error' &&
|
||||
tc.status !== 'cancelled';
|
||||
if (tc.request.name === ASK_USER_TOOL_NAME && isInProgress) {
|
||||
return false;
|
||||
}
|
||||
if (!isEventDriven) return true;
|
||||
return (
|
||||
tc.status !== 'scheduled' &&
|
||||
|
||||
@@ -219,4 +219,40 @@ describe('useHistoryManager', () => {
|
||||
expect(result.current.history[0].id).toBeGreaterThanOrEqual(before + 1);
|
||||
expect(result.current.history[0].id).toBeLessThanOrEqual(after + 1);
|
||||
});
|
||||
|
||||
describe('initialItems with auth information', () => {
|
||||
it('should initialize with auth information', () => {
|
||||
const email = 'user@example.com';
|
||||
const tier = 'Pro';
|
||||
const authMessage = `Authenticated as: ${email} (Plan: ${tier})`;
|
||||
const initialItems: HistoryItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'info',
|
||||
text: authMessage,
|
||||
},
|
||||
];
|
||||
const { result } = renderHook(() => useHistory({ initialItems }));
|
||||
expect(result.current.history).toHaveLength(1);
|
||||
expect(result.current.history[0].text).toBe(authMessage);
|
||||
});
|
||||
|
||||
it('should add items with auth information via addItem', () => {
|
||||
const { result } = renderHook(() => useHistory());
|
||||
const email = 'user@example.com';
|
||||
const tier = 'Pro';
|
||||
const authMessage = `Authenticated as: ${email} (Plan: ${tier})`;
|
||||
|
||||
act(() => {
|
||||
result.current.addItem({
|
||||
type: 'info',
|
||||
text: authMessage,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.history).toHaveLength(1);
|
||||
expect(result.current.history[0].text).toBe(authMessage);
|
||||
expect(result.current.history[0].type).toBe('info');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,10 +36,12 @@ export interface UseHistoryManagerReturn {
|
||||
*/
|
||||
export function useHistory({
|
||||
chatRecordingService,
|
||||
initialItems = [],
|
||||
}: {
|
||||
chatRecordingService?: ChatRecordingService | null;
|
||||
initialItems?: HistoryItem[];
|
||||
} = {}): UseHistoryManagerReturn {
|
||||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
||||
const [history, setHistory] = useState<HistoryItem[]>(initialItems);
|
||||
const messageIdCounterRef = useRef(0);
|
||||
|
||||
// Generates a unique message ID based on a timestamp and a counter.
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type HookEndPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { act } from 'react';
|
||||
import { WARNING_PROMPT_DURATION_MS } from '../constants.js';
|
||||
|
||||
describe('useHookDisplayState', () => {
|
||||
beforeEach(() => {
|
||||
@@ -53,7 +54,7 @@ describe('useHookDisplayState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove a hook immediately if duration > 1s', () => {
|
||||
it('should remove a hook immediately if duration > minimum duration', () => {
|
||||
const { result } = renderHook(() => useHookDisplayState());
|
||||
|
||||
const startPayload: HookStartPayload = {
|
||||
@@ -65,9 +66,9 @@ describe('useHookDisplayState', () => {
|
||||
coreEvents.emitHookStart(startPayload);
|
||||
});
|
||||
|
||||
// Advance time by 1.1 seconds
|
||||
// Advance time by slightly more than the minimum duration
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1100);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 100);
|
||||
});
|
||||
|
||||
const endPayload: HookEndPayload = {
|
||||
@@ -83,7 +84,7 @@ describe('useHookDisplayState', () => {
|
||||
expect(result.current).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should delay removal if duration < 1s', () => {
|
||||
it('should delay removal if duration < minimum duration', () => {
|
||||
const { result } = renderHook(() => useHookDisplayState());
|
||||
|
||||
const startPayload: HookStartPayload = {
|
||||
@@ -113,9 +114,9 @@ describe('useHookDisplayState', () => {
|
||||
// Should still be present
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// Advance remaining time (900ms needed, let's go 950ms)
|
||||
// Advance remaining time + buffer
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(950);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS - 100 + 50);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(0);
|
||||
@@ -138,7 +139,7 @@ describe('useHookDisplayState', () => {
|
||||
|
||||
expect(result.current).toHaveLength(2);
|
||||
|
||||
// End h1 (total time 500ms -> needs 500ms delay)
|
||||
// End h1 (total time 500ms -> needs remaining delay)
|
||||
act(() => {
|
||||
coreEvents.emitHookEnd({
|
||||
hookName: 'h1',
|
||||
@@ -150,15 +151,24 @@ describe('useHookDisplayState', () => {
|
||||
// h1 still there
|
||||
expect(result.current).toHaveLength(2);
|
||||
|
||||
// Advance 600ms. h1 should disappear. h2 has been running for 600ms.
|
||||
// Advance enough for h1 to expire.
|
||||
// h1 ran for 500ms. Needs WARNING_PROMPT_DURATION_MS total.
|
||||
// So advance WARNING_PROMPT_DURATION_MS - 500 + 100.
|
||||
const advanceForH1 = WARNING_PROMPT_DURATION_MS - 500 + 100;
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(600);
|
||||
vi.advanceTimersByTime(advanceForH1);
|
||||
});
|
||||
|
||||
// h1 should disappear. h2 has been running for 500 (initial) + advanceForH1.
|
||||
expect(result.current).toHaveLength(1);
|
||||
expect(result.current[0].name).toBe('h2');
|
||||
|
||||
// End h2 (total time 600ms -> needs 400ms delay)
|
||||
// End h2.
|
||||
// h2 duration so far: 0 (start) -> 500 (start h2) -> (end h1) -> advanceForH1.
|
||||
// Actually h2 started at t=500. Current time is t=500 + advanceForH1.
|
||||
// Duration = advanceForH1.
|
||||
// advanceForH1 = 3000 - 500 + 100 = 2600.
|
||||
// So h2 has run for 2600ms. Needs 400ms more.
|
||||
act(() => {
|
||||
coreEvents.emitHookEnd({
|
||||
hookName: 'h2',
|
||||
@@ -169,6 +179,8 @@ describe('useHookDisplayState', () => {
|
||||
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// Advance remaining needed for h2 + buffer
|
||||
// 3000 - 2600 = 400.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
@@ -199,34 +211,42 @@ describe('useHookDisplayState', () => {
|
||||
expect(result.current[0].name).toBe('same-hook');
|
||||
expect(result.current[1].name).toBe('same-hook');
|
||||
|
||||
// End Hook 1 at t=600 (Duration 600ms -> delay 400ms)
|
||||
// End Hook 1 at t=600 (Duration 600ms -> delay needed)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
coreEvents.emitHookEnd({ ...hook, success: true });
|
||||
});
|
||||
|
||||
// Both still visible (Hook 1 pending removal in 400ms)
|
||||
// Both still visible
|
||||
expect(result.current).toHaveLength(2);
|
||||
|
||||
// Advance 400ms (t=1000). Hook 1 should be removed.
|
||||
// Advance to make Hook 1 expire.
|
||||
// Hook 1 duration 600ms. Needs WARNING_PROMPT_DURATION_MS total.
|
||||
// Needs WARNING_PROMPT_DURATION_MS - 600 more.
|
||||
const advanceForHook1 = WARNING_PROMPT_DURATION_MS - 600;
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(400);
|
||||
vi.advanceTimersByTime(advanceForHook1);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// End Hook 2 at t=1100 (Duration: 1100 - 500 = 600ms -> delay 400ms)
|
||||
// End Hook 2.
|
||||
// Hook 2 started at t=500.
|
||||
// Current time: t = 600 (hook 1 end) + advanceForHook1 = 600 + 3000 - 600 = 3000.
|
||||
// Hook 2 duration = 3000 - 500 = 2500ms.
|
||||
// Needs 3000 - 2500 = 500ms more.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
vi.advanceTimersByTime(100); // just a small step before ending
|
||||
coreEvents.emitHookEnd({ ...hook, success: true });
|
||||
});
|
||||
|
||||
// Hook 2 still visible (pending removal in 400ms)
|
||||
// Hook 2 still visible (pending removal)
|
||||
// Total run time: 2500 + 100 = 2600ms. Needs 400ms.
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// Advance 400ms (t=1500). Hook 2 should be removed.
|
||||
// Advance remaining
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(400);
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(0);
|
||||
|
||||
@@ -177,6 +177,84 @@ describe('useSessionResume', () => {
|
||||
expect(mockRefreshStatic).toHaveBeenCalledTimes(1);
|
||||
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith([], resumedData);
|
||||
});
|
||||
|
||||
it('should restore directories from resumed session data', async () => {
|
||||
const mockAddDirectories = vi
|
||||
.fn()
|
||||
.mockReturnValue({ added: [], failed: [] });
|
||||
const mockWorkspaceContext = {
|
||||
addDirectories: mockAddDirectories,
|
||||
};
|
||||
const configWithWorkspace = {
|
||||
...mockConfig,
|
||||
getWorkspaceContext: vi.fn().mockReturnValue(mockWorkspaceContext),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
config: configWithWorkspace as unknown as Config,
|
||||
}),
|
||||
);
|
||||
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
directories: ['/restored/dir1', '/restored/dir2'],
|
||||
},
|
||||
filePath: '/path/to/session.json',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadHistoryForResume([], [], resumedData);
|
||||
});
|
||||
|
||||
expect(configWithWorkspace.getWorkspaceContext).toHaveBeenCalled();
|
||||
expect(mockAddDirectories).toHaveBeenCalledWith([
|
||||
'/restored/dir1',
|
||||
'/restored/dir2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not call addDirectories when no directories in resumed session', async () => {
|
||||
const mockAddDirectories = vi.fn();
|
||||
const mockWorkspaceContext = {
|
||||
addDirectories: mockAddDirectories,
|
||||
};
|
||||
const configWithWorkspace = {
|
||||
...mockConfig,
|
||||
getWorkspaceContext: vi.fn().mockReturnValue(mockWorkspaceContext),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
config: configWithWorkspace as unknown as Config,
|
||||
}),
|
||||
);
|
||||
|
||||
const resumedData: ResumedSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-123',
|
||||
projectHash: 'project-123',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
lastUpdated: '2025-01-01T01:00:00Z',
|
||||
messages: [] as MessageRecord[],
|
||||
// No directories field
|
||||
},
|
||||
filePath: '/path/to/session.json',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadHistoryForResume([], [], resumedData);
|
||||
});
|
||||
|
||||
expect(mockAddDirectories).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('callback stability', () => {
|
||||
|
||||
@@ -71,6 +71,17 @@ export function useSessionResume({
|
||||
});
|
||||
refreshStaticRef.current(); // Force Static component to re-render with the updated history.
|
||||
|
||||
// Restore directories from the resumed session
|
||||
if (
|
||||
resumedData.conversation.directories &&
|
||||
resumedData.conversation.directories.length > 0
|
||||
) {
|
||||
const workspaceContext = config.getWorkspaceContext();
|
||||
// Add back any directories that were saved in the session
|
||||
// but filter out ones that no longer exist
|
||||
workspaceContext.addDirectories(resumedData.conversation.directories);
|
||||
}
|
||||
|
||||
// Give the history to the Gemini client.
|
||||
await config.getGeminiClient()?.resumeChat(clientHistory, resumedData);
|
||||
} catch (error) {
|
||||
|
||||
@@ -131,13 +131,24 @@ describe('keyMatchers', () => {
|
||||
},
|
||||
{
|
||||
command: Command.UNDO,
|
||||
positive: [createKey('z', { shift: false, ctrl: true })],
|
||||
negative: [createKey('z'), createKey('z', { shift: true, ctrl: true })],
|
||||
positive: [
|
||||
createKey('z', { shift: false, cmd: true }),
|
||||
createKey('z', { shift: false, alt: true }),
|
||||
],
|
||||
negative: [
|
||||
createKey('z'),
|
||||
createKey('z', { shift: true, cmd: true }),
|
||||
createKey('z', { shift: false, ctrl: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
command: Command.REDO,
|
||||
positive: [createKey('z', { shift: true, ctrl: true })],
|
||||
negative: [createKey('z'), createKey('z', { shift: false, ctrl: true })],
|
||||
positive: [
|
||||
createKey('z', { shift: true, cmd: true }),
|
||||
createKey('z', { shift: true, alt: true }),
|
||||
createKey('z', { shift: true, ctrl: true }),
|
||||
],
|
||||
negative: [createKey('z'), createKey('z', { shift: false, cmd: true })],
|
||||
},
|
||||
|
||||
// Screen control
|
||||
|
||||
@@ -11,7 +11,7 @@ if (process.env['NO_COLOR'] !== undefined) {
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { themeManager, DEFAULT_THEME } from './theme-manager.js';
|
||||
import type { CustomTheme } from './theme.js';
|
||||
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import type * as osActual from 'node:os';
|
||||
@@ -188,4 +188,54 @@ describe('ThemeManager', () => {
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extension themes', () => {
|
||||
it('should register and unregister themes from extensions with namespacing', () => {
|
||||
const extTheme: CustomTheme = {
|
||||
...validCustomTheme,
|
||||
name: 'ExtensionTheme',
|
||||
};
|
||||
const extensionName = 'test-extension';
|
||||
const namespacedName = `ExtensionTheme (${extensionName})`;
|
||||
|
||||
themeManager.registerExtensionThemes(extensionName, [extTheme]);
|
||||
expect(themeManager.getCustomThemeNames()).toContain(namespacedName);
|
||||
expect(themeManager.isCustomTheme(namespacedName)).toBe(true);
|
||||
|
||||
themeManager.unregisterExtensionThemes(extensionName, [extTheme]);
|
||||
expect(themeManager.getCustomThemeNames()).not.toContain(namespacedName);
|
||||
expect(themeManager.isCustomTheme(namespacedName)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not allow extension themes to overwrite built-in themes even with prefixing', () => {
|
||||
// availableThemes has 'Ayu'.
|
||||
// We verify that it DOES prefix, so it won't collide even if extension name is similar.
|
||||
themeManager.registerExtensionThemes('Ext', [
|
||||
{ ...validCustomTheme, name: 'Theme' },
|
||||
]);
|
||||
expect(themeManager.getCustomThemeNames()).toContain('Theme (Ext)');
|
||||
});
|
||||
|
||||
it('should allow extension themes and settings themes to coexist', () => {
|
||||
const extTheme: CustomTheme = {
|
||||
...validCustomTheme,
|
||||
name: 'ExtensionTheme',
|
||||
};
|
||||
const settingsTheme: CustomTheme = {
|
||||
...validCustomTheme,
|
||||
name: 'SettingsTheme',
|
||||
};
|
||||
|
||||
themeManager.registerExtensionThemes('Ext', [extTheme]);
|
||||
themeManager.loadCustomThemes({ SettingsTheme: settingsTheme });
|
||||
|
||||
expect(themeManager.getCustomThemeNames()).toContain(
|
||||
'ExtensionTheme (Ext)',
|
||||
);
|
||||
expect(themeManager.getCustomThemeNames()).toContain('SettingsTheme');
|
||||
|
||||
expect(themeManager.isCustomTheme('ExtensionTheme (Ext)')).toBe(true);
|
||||
expect(themeManager.isCustomTheme('SettingsTheme')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,8 @@ import { ShadesOfPurple } from './shades-of-purple.js';
|
||||
import { XCode } from './xcode.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import type { Theme, ThemeType, CustomTheme } from './theme.js';
|
||||
import type { Theme, ThemeType } from './theme.js';
|
||||
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||
import { createCustomTheme, validateCustomTheme } from './theme.js';
|
||||
import type { SemanticColors } from './semantic-tokens.js';
|
||||
import { ANSI } from './ansi.js';
|
||||
@@ -38,7 +39,9 @@ export const DEFAULT_THEME: Theme = DefaultDark;
|
||||
class ThemeManager {
|
||||
private readonly availableThemes: Theme[];
|
||||
private activeTheme: Theme;
|
||||
private customThemes: Map<string, Theme> = new Map();
|
||||
private settingsThemes: Map<string, Theme> = new Map();
|
||||
private extensionThemes: Map<string, Theme> = new Map();
|
||||
private fileThemes: Map<string, Theme> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.availableThemes = [
|
||||
@@ -65,7 +68,7 @@ class ThemeManager {
|
||||
* @param customThemesSettings Custom themes from settings.
|
||||
*/
|
||||
loadCustomThemes(customThemesSettings?: Record<string, CustomTheme>): void {
|
||||
this.customThemes.clear();
|
||||
this.settingsThemes.clear();
|
||||
|
||||
if (!customThemesSettings) {
|
||||
return;
|
||||
@@ -88,7 +91,7 @@ class ThemeManager {
|
||||
|
||||
try {
|
||||
const theme = createCustomTheme(themeWithDefaults);
|
||||
this.customThemes.set(name, theme);
|
||||
this.settingsThemes.set(name, theme);
|
||||
} catch (error) {
|
||||
debugLogger.warn(`Failed to load custom theme "${name}":`, error);
|
||||
}
|
||||
@@ -96,16 +99,103 @@ class ThemeManager {
|
||||
debugLogger.warn(`Invalid custom theme "${name}": ${validation.error}`);
|
||||
}
|
||||
}
|
||||
// If the current active theme is a custom theme, keep it if still valid
|
||||
// If the current active theme is a settings theme, keep it if still valid
|
||||
if (
|
||||
this.activeTheme &&
|
||||
this.activeTheme.type === 'custom' &&
|
||||
this.customThemes.has(this.activeTheme.name)
|
||||
this.settingsThemes.has(this.activeTheme.name)
|
||||
) {
|
||||
this.activeTheme = this.customThemes.get(this.activeTheme.name)!;
|
||||
this.activeTheme = this.settingsThemes.get(this.activeTheme.name)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads custom themes from extensions.
|
||||
* @param extensionName The name of the extension providing the themes.
|
||||
* @param customThemes Custom themes from extensions.
|
||||
*/
|
||||
registerExtensionThemes(
|
||||
extensionName: string,
|
||||
customThemes?: CustomTheme[],
|
||||
): void {
|
||||
if (!customThemes) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugLogger.log(
|
||||
`Registering extension themes for "${extensionName}":`,
|
||||
customThemes,
|
||||
);
|
||||
|
||||
for (const customThemeConfig of customThemes) {
|
||||
const namespacedName = `${customThemeConfig.name} (${extensionName})`;
|
||||
|
||||
// Check for collisions with built-in themes (unlikely with prefix, but safe)
|
||||
if (this.availableThemes.some((t) => t.name === namespacedName)) {
|
||||
debugLogger.warn(
|
||||
`Theme name collision: "${namespacedName}" is a built-in theme. Skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const validation = validateCustomTheme(customThemeConfig);
|
||||
if (validation.isValid) {
|
||||
if (validation.warning) {
|
||||
debugLogger.warn(`Theme "${namespacedName}": ${validation.warning}`);
|
||||
}
|
||||
const themeWithDefaults: CustomTheme = {
|
||||
...DEFAULT_THEME.colors,
|
||||
...customThemeConfig,
|
||||
name: namespacedName,
|
||||
type: 'custom',
|
||||
};
|
||||
|
||||
try {
|
||||
const theme = createCustomTheme(themeWithDefaults);
|
||||
this.extensionThemes.set(namespacedName, theme);
|
||||
debugLogger.log(`Registered theme: ${namespacedName}`);
|
||||
} catch (error) {
|
||||
debugLogger.warn(
|
||||
`Failed to load custom theme "${namespacedName}":`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugLogger.warn(
|
||||
`Invalid custom theme "${namespacedName}": ${validation.error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters custom themes from extensions.
|
||||
* @param extensionName The name of the extension.
|
||||
* @param customThemes Custom themes to unregister.
|
||||
*/
|
||||
unregisterExtensionThemes(
|
||||
extensionName: string,
|
||||
customThemes?: CustomTheme[],
|
||||
): void {
|
||||
if (!customThemes) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const theme of customThemes) {
|
||||
const namespacedName = `${theme.name} (${extensionName})`;
|
||||
this.extensionThemes.delete(namespacedName);
|
||||
debugLogger.log(`Unregistered theme: ${namespacedName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all registered extension themes.
|
||||
* This is primarily for testing purposes to reset state between tests.
|
||||
*/
|
||||
clearExtensionThemes(): void {
|
||||
this.extensionThemes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active theme.
|
||||
* @param themeName The name of the theme to set as active.
|
||||
@@ -133,13 +223,23 @@ class ThemeManager {
|
||||
const isBuiltIn = this.availableThemes.some(
|
||||
(t) => t.name === this.activeTheme.name,
|
||||
);
|
||||
const isCustom = [...this.customThemes.values()].includes(
|
||||
this.activeTheme,
|
||||
);
|
||||
const isCustom =
|
||||
[...this.settingsThemes.values()].includes(this.activeTheme) ||
|
||||
[...this.extensionThemes.values()].includes(this.activeTheme) ||
|
||||
[...this.fileThemes.values()].includes(this.activeTheme);
|
||||
|
||||
if (isBuiltIn || isCustom) {
|
||||
return this.activeTheme;
|
||||
}
|
||||
|
||||
// If the theme object is no longer valid, try to find it again by name.
|
||||
// This handles the case where extensions are reloaded and theme objects
|
||||
// are re-created.
|
||||
const reloadedTheme = this.findThemeByName(this.activeTheme.name);
|
||||
if (reloadedTheme) {
|
||||
this.activeTheme = reloadedTheme;
|
||||
return this.activeTheme;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default if no active theme or if it's no longer valid.
|
||||
@@ -155,12 +255,20 @@ class ThemeManager {
|
||||
return this.getActiveTheme().semanticColors;
|
||||
}
|
||||
|
||||
private _getAllCustomThemes(): Theme[] {
|
||||
return [
|
||||
...Array.from(this.settingsThemes.values()),
|
||||
...Array.from(this.extensionThemes.values()),
|
||||
...Array.from(this.fileThemes.values()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of custom theme names.
|
||||
* @returns Array of custom theme names.
|
||||
*/
|
||||
getCustomThemeNames(): string[] {
|
||||
return Array.from(this.customThemes.keys());
|
||||
return this._getAllCustomThemes().map((theme) => theme.name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +277,11 @@ class ThemeManager {
|
||||
* @returns True if the theme is custom.
|
||||
*/
|
||||
isCustomTheme(themeName: string): boolean {
|
||||
return this.customThemes.has(themeName);
|
||||
return (
|
||||
this.settingsThemes.has(themeName) ||
|
||||
this.extensionThemes.has(themeName) ||
|
||||
this.fileThemes.has(themeName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,13 +294,11 @@ class ThemeManager {
|
||||
isCustom: false,
|
||||
}));
|
||||
|
||||
const customThemes = Array.from(this.customThemes.values()).map(
|
||||
(theme) => ({
|
||||
name: theme.name,
|
||||
type: theme.type,
|
||||
isCustom: true,
|
||||
}),
|
||||
);
|
||||
const customThemes = this._getAllCustomThemes().map((theme) => ({
|
||||
name: theme.name,
|
||||
type: theme.type,
|
||||
isCustom: true,
|
||||
}));
|
||||
|
||||
const allThemes = [...builtInThemes, ...customThemes];
|
||||
|
||||
@@ -232,7 +342,7 @@ class ThemeManager {
|
||||
* @returns A list of all available themes.
|
||||
*/
|
||||
getAllThemes(): Theme[] {
|
||||
return [...this.availableThemes, ...Array.from(this.customThemes.values())];
|
||||
return [...this.availableThemes, ...this._getAllCustomThemes()];
|
||||
}
|
||||
|
||||
private isPath(themeName: string): boolean {
|
||||
@@ -249,8 +359,8 @@ class ThemeManager {
|
||||
const canonicalPath = fs.realpathSync(path.resolve(themePath));
|
||||
|
||||
// 1. Check cache using the canonical path.
|
||||
if (this.customThemes.has(canonicalPath)) {
|
||||
return this.customThemes.get(canonicalPath);
|
||||
if (this.fileThemes.has(canonicalPath)) {
|
||||
return this.fileThemes.get(canonicalPath);
|
||||
}
|
||||
|
||||
// 2. Perform security check.
|
||||
@@ -288,7 +398,7 @@ class ThemeManager {
|
||||
};
|
||||
|
||||
const theme = createCustomTheme(themeWithDefaults);
|
||||
this.customThemes.set(canonicalPath, theme); // Cache by canonical path
|
||||
this.fileThemes.set(canonicalPath, theme); // Cache by canonical path
|
||||
return theme;
|
||||
} catch (error) {
|
||||
// Any error in the process (file not found, bad JSON, etc.) is caught here.
|
||||
@@ -318,13 +428,21 @@ class ThemeManager {
|
||||
return builtInTheme;
|
||||
}
|
||||
|
||||
// Then check custom themes that have been loaded from settings, or file paths
|
||||
// Then check custom themes that have been loaded from settings, extensions, or file paths
|
||||
if (this.isPath(themeName)) {
|
||||
return this.loadThemeFromFile(themeName);
|
||||
}
|
||||
|
||||
if (this.customThemes.has(themeName)) {
|
||||
return this.customThemes.get(themeName);
|
||||
if (this.settingsThemes.has(themeName)) {
|
||||
return this.settingsThemes.get(themeName);
|
||||
}
|
||||
|
||||
if (this.extensionThemes.has(themeName)) {
|
||||
return this.extensionThemes.get(themeName);
|
||||
}
|
||||
|
||||
if (this.fileThemes.has(themeName)) {
|
||||
return this.fileThemes.get(themeName);
|
||||
}
|
||||
|
||||
// If it's not a built-in, not in cache, and not a valid file path,
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as themeModule from './theme.js';
|
||||
import {
|
||||
createCustomTheme,
|
||||
validateCustomTheme,
|
||||
pickDefaultThemeName,
|
||||
darkTheme,
|
||||
type Theme,
|
||||
} from './theme.js';
|
||||
import { themeManager } from './theme-manager.js';
|
||||
|
||||
const { validateCustomTheme, createCustomTheme } = themeModule;
|
||||
type CustomTheme = themeModule.CustomTheme;
|
||||
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||
|
||||
describe('createCustomTheme', () => {
|
||||
const baseTheme: CustomTheme = {
|
||||
@@ -152,7 +156,6 @@ describe('themeManager.loadCustomThemes', () => {
|
||||
};
|
||||
|
||||
it('should use values from DEFAULT_THEME when DiffAdded and DiffRemoved are not provided', () => {
|
||||
const { darkTheme } = themeModule;
|
||||
const legacyTheme: Partial<CustomTheme> = { ...baseTheme };
|
||||
delete legacyTheme.DiffAdded;
|
||||
delete legacyTheme.DiffRemoved;
|
||||
@@ -170,12 +173,11 @@ describe('themeManager.loadCustomThemes', () => {
|
||||
});
|
||||
|
||||
describe('pickDefaultThemeName', () => {
|
||||
const { pickDefaultThemeName } = themeModule;
|
||||
const mockThemes = [
|
||||
{ name: 'Dark Theme', type: 'dark', colors: { Background: '#000000' } },
|
||||
{ name: 'Light Theme', type: 'light', colors: { Background: '#ffffff' } },
|
||||
{ name: 'Blue Theme', type: 'dark', colors: { Background: '#0000ff' } },
|
||||
] as unknown as themeModule.Theme[];
|
||||
] as unknown as Theme[];
|
||||
|
||||
it('should return exact match if found', () => {
|
||||
expect(
|
||||
|
||||
@@ -5,13 +5,19 @@
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import type { SemanticColors } from './semantic-tokens.js';
|
||||
|
||||
import {
|
||||
resolveColor,
|
||||
interpolateColor,
|
||||
getThemeTypeFromBackgroundColor,
|
||||
} from './color-utils.js';
|
||||
|
||||
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||
|
||||
export type { CustomTheme };
|
||||
|
||||
export type ThemeType = 'light' | 'dark' | 'ansi' | 'custom';
|
||||
|
||||
export interface ColorsTheme {
|
||||
@@ -33,57 +39,6 @@ export interface ColorsTheme {
|
||||
GradientColors?: string[];
|
||||
}
|
||||
|
||||
export interface CustomTheme {
|
||||
type: 'custom';
|
||||
name: string;
|
||||
|
||||
text?: {
|
||||
primary?: string;
|
||||
secondary?: string;
|
||||
link?: string;
|
||||
accent?: string;
|
||||
response?: string;
|
||||
};
|
||||
background?: {
|
||||
primary?: string;
|
||||
diff?: {
|
||||
added?: string;
|
||||
removed?: string;
|
||||
};
|
||||
};
|
||||
border?: {
|
||||
default?: string;
|
||||
focused?: string;
|
||||
};
|
||||
ui?: {
|
||||
comment?: string;
|
||||
symbol?: string;
|
||||
gradient?: string[];
|
||||
};
|
||||
status?: {
|
||||
error?: string;
|
||||
success?: string;
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
// Legacy properties (all optional)
|
||||
Background?: string;
|
||||
Foreground?: string;
|
||||
LightBlue?: string;
|
||||
AccentBlue?: string;
|
||||
AccentPurple?: string;
|
||||
AccentCyan?: string;
|
||||
AccentGreen?: string;
|
||||
AccentYellow?: string;
|
||||
AccentRed?: string;
|
||||
DiffAdded?: string;
|
||||
DiffRemoved?: string;
|
||||
Comment?: string;
|
||||
Gray?: string;
|
||||
DarkGray?: string;
|
||||
GradientColors?: string[];
|
||||
}
|
||||
|
||||
export const lightTheme: ColorsTheme = {
|
||||
type: 'light',
|
||||
Background: '#FAFAFA',
|
||||
|
||||
@@ -157,10 +157,16 @@ export type HistoryItemStats = HistoryItemBase & {
|
||||
type: 'stats';
|
||||
duration: string;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
tier?: string;
|
||||
};
|
||||
|
||||
export type HistoryItemModelStats = HistoryItemBase & {
|
||||
type: 'model_stats';
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
tier?: string;
|
||||
};
|
||||
|
||||
export type HistoryItemToolStats = HistoryItemBase & {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TableRenderer } from './TableRenderer.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
describe('TableRenderer', () => {
|
||||
it('renders a 3x3 table correctly', () => {
|
||||
const headers = ['Header 1', 'Header 2', 'Header 3'];
|
||||
const rows = [
|
||||
['Row 1, Col 1', 'Row 1, Col 2', 'Row 1, Col 3'],
|
||||
['Row 2, Col 1', 'Row 2, Col 2', 'Row 2, Col 3'],
|
||||
['Row 3, Col 1', 'Row 3, Col 2', 'Row 3, Col 3'],
|
||||
];
|
||||
const terminalWidth = 80;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<TableRenderer
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Header 1');
|
||||
expect(output).toContain('Row 1, Col 1');
|
||||
expect(output).toContain('Row 3, Col 3');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a table with long headers and 4 columns correctly', () => {
|
||||
const headers = [
|
||||
'Very Long Column Header One',
|
||||
'Very Long Column Header Two',
|
||||
'Very Long Column Header Three',
|
||||
'Very Long Column Header Four',
|
||||
];
|
||||
const rows = [
|
||||
['Data 1.1', 'Data 1.2', 'Data 1.3', 'Data 1.4'],
|
||||
['Data 2.1', 'Data 2.2', 'Data 2.3', 'Data 2.4'],
|
||||
['Data 3.1', 'Data 3.2', 'Data 3.3', 'Data 3.4'],
|
||||
];
|
||||
const terminalWidth = 80;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<TableRenderer
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
// Since terminalWidth is 80 and headers are long, they might be truncated.
|
||||
// We just check for some of the content.
|
||||
expect(output).toContain('Data 1.1');
|
||||
expect(output).toContain('Data 3.4');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -34,11 +34,24 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
});
|
||||
|
||||
// Ensure table fits within terminal width
|
||||
const totalWidth = columnWidths.reduce((sum, width) => sum + width + 1, 1);
|
||||
// We calculate scale based on content width vs available width (terminal - borders)
|
||||
// First, extract content widths by removing the 2-char padding.
|
||||
const contentWidths = columnWidths.map((width) => Math.max(0, width - 2));
|
||||
const totalContentWidth = contentWidths.reduce(
|
||||
(sum, width) => sum + width,
|
||||
0,
|
||||
);
|
||||
|
||||
// Fixed overhead includes padding (2 per column) and separators (1 per column + 1 final).
|
||||
const fixedOverhead = headers.length * 2 + (headers.length + 1);
|
||||
|
||||
// Subtract 1 from available width to avoid edge-case wrapping on some terminals
|
||||
const availableWidth = Math.max(0, terminalWidth - fixedOverhead - 1);
|
||||
|
||||
const scaleFactor =
|
||||
totalWidth > terminalWidth ? terminalWidth / totalWidth : 1;
|
||||
const adjustedWidths = columnWidths.map((width) =>
|
||||
Math.floor(width * scaleFactor),
|
||||
totalContentWidth > availableWidth ? availableWidth / totalContentWidth : 1;
|
||||
const adjustedWidths = contentWidths.map(
|
||||
(width) => Math.floor(width * scaleFactor) + 2,
|
||||
);
|
||||
|
||||
// Helper function to render a cell with proper width
|
||||
@@ -124,14 +137,16 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
|
||||
return (
|
||||
<Text color={theme.text.primary}>
|
||||
│{' '}
|
||||
<Text color={theme.border.default}>│</Text>{' '}
|
||||
{renderedCells.map((cell, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{cell}
|
||||
{index < renderedCells.length - 1 ? ' │ ' : ''}
|
||||
{index < renderedCells.length - 1 && (
|
||||
<Text color={theme.border.default}>{' │ '}</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}{' '}
|
||||
│
|
||||
<Text color={theme.border.default}>│</Text>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,18 @@ exports[`TableRenderer > handles rows with missing cells 1`] = `
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > renders a 3x3 table correctly 1`] = `
|
||||
"
|
||||
┌──────────────┬──────────────┬──────────────┐
|
||||
│ Header 1 │ Header 2 │ Header 3 │
|
||||
├──────────────┼──────────────┼──────────────┤
|
||||
│ Row 1, Col 1 │ Row 1, Col 2 │ Row 1, Col 3 │
|
||||
│ Row 2, Col 1 │ Row 2, Col 2 │ Row 2, Col 3 │
|
||||
│ Row 3, Col 1 │ Row 3, Col 2 │ Row 3, Col 3 │
|
||||
└──────────────┴──────────────┴──────────────┘
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > renders a simple table correctly 1`] = `
|
||||
"
|
||||
┌─────────┬──────────┬──────────┐
|
||||
@@ -42,6 +54,18 @@ exports[`TableRenderer > renders a simple table correctly 1`] = `
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > renders a table with long headers and 4 columns correctly 1`] = `
|
||||
"
|
||||
┌──────────────────┬──────────────────┬───────────────────┬──────────────────┐
|
||||
│ Very Long Col... │ Very Long Col... │ Very Long Colu... │ Very Long Col... │
|
||||
├──────────────────┼──────────────────┼───────────────────┼──────────────────┤
|
||||
│ Data 1.1 │ Data 1.2 │ Data 1.3 │ Data 1.4 │
|
||||
│ Data 2.1 │ Data 2.2 │ Data 2.3 │ Data 2.4 │
|
||||
│ Data 3.1 │ Data 3.2 │ Data 3.3 │ Data 3.4 │
|
||||
└──────────────────┴──────────────────┴───────────────────┴──────────────────┘
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > truncates content when terminal width is small 1`] = `
|
||||
"
|
||||
┌────────┬─────────┬─────────┐
|
||||
|
||||
@@ -323,13 +323,17 @@ export class ActivityLogger extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the activity logger if debug mode and interactive session are enabled.
|
||||
* Registers the activity logger.
|
||||
* Captures network and console logs to a session-specific JSONL file.
|
||||
*
|
||||
* The log file location can be overridden via the GEMINI_CLI_ACTIVITY_LOG_FILE
|
||||
* environment variable. If not set, defaults to logs/session-{sessionId}.jsonl
|
||||
* in the project's temp directory.
|
||||
*
|
||||
* @param config The CLI configuration
|
||||
*/
|
||||
export function registerActivityLogger(config: Config) {
|
||||
if (config.isInteractive() && config.storage && config.getDebugMode()) {
|
||||
if (config.storage) {
|
||||
const capture = ActivityLogger.getInstance();
|
||||
capture.enable();
|
||||
|
||||
@@ -338,10 +342,10 @@ export function registerActivityLogger(config: Config) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const logFile = path.join(
|
||||
logsDir,
|
||||
`session-${config.getSessionId()}.jsonl`,
|
||||
);
|
||||
const logFile =
|
||||
process.env['GEMINI_CLI_ACTIVITY_LOG_FILE'] ||
|
||||
path.join(logsDir, `session-${config.getSessionId()}.jsonl`);
|
||||
|
||||
const writeToLog = (type: 'console' | 'network', payload: unknown) => {
|
||||
try {
|
||||
const entry =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user