mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 08:40:58 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 290550358a | |||
| 8349cc0d10 | |||
| f55b52ab58 | |||
| fd85363d13 | |||
| 7ce9aa518e | |||
| 68bde280d0 | |||
| 09800ea74e | |||
| 9cf06a3e00 | |||
| 3c33fcb69c | |||
| 6f4a2a2c2a | |||
| 9954615542 | |||
| c7a044b091 |
@@ -1,22 +0,0 @@
|
||||
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.
|
||||
"""
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"experimental": {
|
||||
"skills": true
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
---
|
||||
name: docs-writer
|
||||
description:
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
a natural tone.
|
||||
- **Simple vocabulary:** Use common words. Define technical terms when
|
||||
necessary.
|
||||
- **Conciseness:** Keep sentences short and focused, but don't omit helpful
|
||||
information.
|
||||
- **Conciseness:** Keep sentences short and focused.
|
||||
- **"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). Note that this output contains your email address. Consider removing it before submitting.'
|
||||
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux).'
|
||||
value: |-
|
||||
<details>
|
||||
<summary>Client Information</summary>
|
||||
|
||||
@@ -72,8 +72,7 @@ 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,23 +18,6 @@ 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,
|
||||
|
||||
+347
-315
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.26.0
|
||||
# Latest stable release: v0.25.0
|
||||
|
||||
Released: January 27, 2026
|
||||
Released: January 20, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,328 +11,360 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- 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
|
||||
- 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
|
||||
@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
|
||||
[#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
|
||||
@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
|
||||
@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
|
||||
[#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
|
||||
@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
|
||||
[#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
|
||||
@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
|
||||
[#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
|
||||
@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
|
||||
[#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
|
||||
@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) 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
|
||||
[#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
|
||||
@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)
|
||||
[#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)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.25.2...v0.26.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.24.5...v0.25.0
|
||||
|
||||
+309
-414
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.27.0-preview.0
|
||||
# Preview release: Release v0.26.0-preview.0
|
||||
|
||||
Released: January 27, 2026
|
||||
Released: January 21, 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,425 +13,320 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
|
||||
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
|
||||
- Remove unused modelHooks and toolHooks by @ved015 in
|
||||
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
|
||||
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
|
||||
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
|
||||
- Update Attempt text to Retry when showing the retry happening to the … by
|
||||
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
|
||||
- chore(skills): update pr-creator skill workflow by @sehoon38 in
|
||||
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
|
||||
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
|
||||
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
|
||||
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
|
||||
- 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
|
||||
@gemini-cli-robot in
|
||||
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
|
||||
- Remove other rewind reference in docs by @chrstnb in
|
||||
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
|
||||
- feat(skills): add code-reviewer skill by @sehoon38 in
|
||||
[#17187](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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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)
|
||||
[#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
|
||||
@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)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.26.0-preview.5...v0.27.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.25.0-preview.4...v0.26.0-preview.0
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# 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.
|
||||
+83
-135
@@ -10,14 +10,6 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
- **`/about`**
|
||||
- **Description:** Show version info. Please share this information when
|
||||
filing issues.
|
||||
|
||||
- **`/auth`**
|
||||
- **Description:** Open a dialog that lets you change the authentication
|
||||
method.
|
||||
|
||||
- **`/bug`**
|
||||
- **Description:** File an issue about Gemini CLI. By default, the issue is
|
||||
filed within the GitHub repository for Gemini CLI. The string you enter
|
||||
@@ -30,21 +22,10 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
conversation state interactively, or resuming a previous state from a later
|
||||
session.
|
||||
- **Sub-commands:**
|
||||
- **`delete <tag>`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for chat state resumption.
|
||||
- **Note:** This command only lists chats saved within the current
|
||||
project. Because chat history is project-scoped, chats saved in other
|
||||
project directories will not be displayed.
|
||||
- **`resume <tag>`**
|
||||
- **Description:** Resumes a conversation from a previous save.
|
||||
- **Note:** You can only resume chats that were saved within the current
|
||||
project. To resume a chat from a different project, you must run the
|
||||
Gemini CLI from that project's directory.
|
||||
- **`save <tag>`**
|
||||
- **`save`**
|
||||
- **Description:** Saves the current conversation history. You must add a
|
||||
`<tag>` for identifying the conversation state.
|
||||
- **Usage:** `/chat save <tag>`
|
||||
- **Details on checkpoint location:** The default locations for saved chat
|
||||
checkpoints are:
|
||||
- Linux/macOS: `~/.gemini/tmp/<project_hash>/`
|
||||
@@ -56,11 +37,25 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
conversation states. For automatic checkpoints created before file
|
||||
modifications, see the
|
||||
[Checkpointing documentation](../cli/checkpointing.md).
|
||||
- **`share [filename]`**
|
||||
- **`resume`**
|
||||
- **Description:** Resumes a conversation from a previous save.
|
||||
- **Usage:** `/chat resume <tag>`
|
||||
- **Note:** You can only resume chats that were saved within the current
|
||||
project. To resume a chat from a different project, you must run the
|
||||
Gemini CLI from that project's directory.
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for chat state resumption.
|
||||
- **Note:** This command only lists chats saved within the current
|
||||
project. Because chat history is project-scoped, chats saved in other
|
||||
project directories will not be displayed.
|
||||
- **`delete`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **Usage:** `/chat delete <tag>`
|
||||
- **`share`**
|
||||
- **Description** Writes the current conversation to a provided Markdown
|
||||
or JSON file. If no filename is provided, then the CLI will generate
|
||||
one.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`.
|
||||
or JSON file.
|
||||
- **Usage** `/chat share file.md` or `/chat share file.json`. If no
|
||||
filename is provided, then the CLI will generate one.
|
||||
|
||||
- **`/clear`**
|
||||
- **Description:** Clear the terminal screen, including the visible session
|
||||
@@ -103,9 +98,6 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
`--include-directories`.
|
||||
- **Usage:** `/directory show`
|
||||
|
||||
- **`/docs`**
|
||||
- **Description:** Open the Gemini CLI documentation in your browser.
|
||||
|
||||
- **`/editor`**
|
||||
- **Description:** Open a dialog for selecting supported editors.
|
||||
|
||||
@@ -117,65 +109,30 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Description:** Display help information about Gemini CLI, including
|
||||
available commands and their usage.
|
||||
|
||||
- **`/hooks`**
|
||||
- **Description:** Manage hooks, which allow you to intercept and customize
|
||||
Gemini CLI behavior at specific lifecycle events.
|
||||
- **Sub-commands:**
|
||||
- **`disable-all`**:
|
||||
- **Description:** Disable all enabled hooks.
|
||||
- **`disable <hook-name>`**:
|
||||
- **Description:** Disable a hook by name.
|
||||
- **`enable-all`**:
|
||||
- **Description:** Enable all disabled hooks.
|
||||
- **`enable <hook-name>`**:
|
||||
- **Description:** Enable a hook by name.
|
||||
- **`list`** (or `show`, `panel`):
|
||||
- **Description:** Display all registered hooks with their status.
|
||||
|
||||
- **`/ide`**
|
||||
- **Description:** Manage IDE integration.
|
||||
- **Sub-commands:**
|
||||
- **`disable`**:
|
||||
- **Description:** Disable IDE integration.
|
||||
- **`enable`**:
|
||||
- **Description:** Enable IDE integration.
|
||||
- **`install`**:
|
||||
- **Description:** Install required IDE companion.
|
||||
- **`status`**:
|
||||
- **Description:** Check status of IDE integration.
|
||||
|
||||
- **`/init`**
|
||||
- **Description:** To help users easily create a `GEMINI.md` file, this
|
||||
command analyzes the current directory and generates a tailored context
|
||||
file, making it simpler for them to provide project-specific instructions to
|
||||
the Gemini agent.
|
||||
|
||||
- **`/introspect`**
|
||||
- **Description:** Provide debugging information about the current Gemini CLI
|
||||
session, including the state of loaded sub-agents and active hooks. This
|
||||
command is primarily for advanced users and developers.
|
||||
|
||||
- **`/mcp`**
|
||||
- **Description:** Manage configured Model Context Protocol (MCP) servers.
|
||||
- **Sub-commands:**
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
- **`desc`**
|
||||
- **Description:** List configured MCP servers and tools with
|
||||
descriptions.
|
||||
- **`schema`**:
|
||||
- **Description:** List configured MCP servers and tools with descriptions
|
||||
and schemas.
|
||||
- **`auth`**:
|
||||
- **Description:** Authenticate with an OAuth-enabled MCP server.
|
||||
- **Usage:** `/mcp auth <server-name>`
|
||||
- **Details:** If `<server-name>` is provided, it initiates the OAuth flow
|
||||
for that server. If no server name is provided, it lists all configured
|
||||
servers that support OAuth authentication.
|
||||
- **`desc`**
|
||||
- **Description:** List configured MCP servers and tools with
|
||||
descriptions.
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
- **`refresh`**:
|
||||
- **Description:** Restarts all MCP servers and re-discovers their
|
||||
available tools.
|
||||
- **`schema`**:
|
||||
- **Description:** List configured MCP servers and tools with descriptions
|
||||
and schemas.
|
||||
|
||||
- [**`/model`**](./model.md)
|
||||
- **Description:** Opens a dialog to choose your Gemini model.
|
||||
|
||||
- **`/memory`**
|
||||
- **Description:** Manage the AI's instructional context (hierarchical memory
|
||||
@@ -184,40 +141,23 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`add`**:
|
||||
- **Description:** Adds the following text to the AI's memory. Usage:
|
||||
`/memory add <text to remember>`
|
||||
- **`list`**:
|
||||
- **Description:** Lists the paths of the GEMINI.md files in use for
|
||||
hierarchical memory.
|
||||
- **`refresh`**:
|
||||
- **Description:** Reload the hierarchical instructional memory from all
|
||||
`GEMINI.md` files found in the configured locations (global,
|
||||
project/ancestors, and sub-directories). This command updates the model
|
||||
with the latest `GEMINI.md` content.
|
||||
- **`show`**:
|
||||
- **Description:** Display the full, concatenated content of the current
|
||||
hierarchical memory that has been loaded from all `GEMINI.md` files.
|
||||
This lets you inspect the instructional context being provided to the
|
||||
Gemini model.
|
||||
- **`refresh`**:
|
||||
- **Description:** Reload the hierarchical instructional memory from all
|
||||
`GEMINI.md` files found in the configured locations (global,
|
||||
project/ancestors, and sub-directories). This command updates the model
|
||||
with the latest `GEMINI.md` content.
|
||||
- **`list`**:
|
||||
- **Description:** Lists the paths of the GEMINI.md files in use for
|
||||
hierarchical memory.
|
||||
- **Note:** For more details on how `GEMINI.md` files contribute to
|
||||
hierarchical memory, see the
|
||||
[CLI Configuration documentation](../get-started/configuration.md).
|
||||
|
||||
- [**`/model`**](./model.md)
|
||||
- **Description:** Opens a dialog to choose your Gemini model.
|
||||
|
||||
- **`/policies`**
|
||||
- **Description:** Manage policies.
|
||||
- **Sub-commands:**
|
||||
- **`list`**:
|
||||
- **Description:** List all active policies grouped by mode.
|
||||
|
||||
- **`/privacy`**
|
||||
- **Description:** Display the Privacy Notice and allow users to select
|
||||
whether they consent to the collection of their data for service improvement
|
||||
purposes.
|
||||
|
||||
- **`/quit`** (or **`/exit`**)
|
||||
- **Description:** Exit Gemini CLI.
|
||||
|
||||
- **`/restore`**
|
||||
- **Description:** Restores the project files to the state they were in just
|
||||
before a tool was executed. This is particularly useful for undoing file
|
||||
@@ -228,24 +168,18 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
[settings](../get-started/configuration.md). See
|
||||
[Checkpointing documentation](../cli/checkpointing.md) for more details.
|
||||
|
||||
- **`/rewind`**
|
||||
- **Description:** Navigates backward through the conversation history,
|
||||
allowing you to review past interactions and potentially revert to a
|
||||
previous state. This feature helps in managing complex or branched
|
||||
conversations.
|
||||
|
||||
- **`/resume`**
|
||||
- **Description:** Browse and resume previous conversation sessions. Opens an
|
||||
interactive session browser where you can search, filter, and select from
|
||||
automatically saved conversations.
|
||||
- **Features:**
|
||||
- **Management:** Delete unwanted sessions directly from the browser
|
||||
- **Resume:** Select any session to resume and continue the conversation
|
||||
- **Search:** Use `/` to search through conversation content across all
|
||||
sessions
|
||||
- **Session Browser:** Interactive interface showing all saved sessions with
|
||||
timestamps, message counts, and first user message for context
|
||||
- **Search:** Use `/` to search through conversation content across all
|
||||
sessions
|
||||
- **Sorting:** Sort sessions by date or message count
|
||||
- **Management:** Delete unwanted sessions directly from the browser
|
||||
- **Resume:** Select any session to resume and continue the conversation
|
||||
- **Note:** All conversations are automatically saved as you chat - no manual
|
||||
saving required. See [Session Management](../cli/session-management.md) for
|
||||
complete details.
|
||||
@@ -264,23 +198,19 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
modify them as desired. Changes to some settings are applied immediately,
|
||||
while others require a restart.
|
||||
|
||||
- **`/setup-github`**
|
||||
- **Description:** Set up GitHub Actions to triage issues and review PRs with
|
||||
Gemini.
|
||||
|
||||
- [**`/skills`**](./skills.md)
|
||||
- **Description:** Manage Agent Skills, which provide on-demand expertise and
|
||||
specialized workflows.
|
||||
- **Sub-commands:**
|
||||
- **`disable <name>`**:
|
||||
- **Description:** Disable a specific skill by name.
|
||||
- **Usage:** `/skills disable <name>`
|
||||
- **`enable <name>`**:
|
||||
- **Description:** Enable a specific skill by name.
|
||||
- **Usage:** `/skills enable <name>`
|
||||
- **`list`**:
|
||||
- **Description:** List all discovered skills and their current status
|
||||
(enabled/disabled).
|
||||
- **`enable`**:
|
||||
- **Description:** Enable a specific skill by name.
|
||||
- **Usage:** `/skills enable <name>`
|
||||
- **`disable`**:
|
||||
- **Description:** Disable a specific skill by name.
|
||||
- **Usage:** `/skills disable <name>`
|
||||
- **`reload`**:
|
||||
- **Description:** Refresh the list of discovered skills from all tiers
|
||||
(workspace, user, and extensions).
|
||||
@@ -292,14 +222,18 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
cached tokens are being used, which occurs with API key authentication but
|
||||
not with OAuth authentication at this time.
|
||||
|
||||
- **`/terminal-setup`**
|
||||
- **Description:** Configure terminal keybindings for multiline input (VS
|
||||
Code, Cursor, Windsurf).
|
||||
|
||||
- [**`/theme`**](./themes.md)
|
||||
- **Description:** Open a dialog that lets you change the visual theme of
|
||||
Gemini CLI.
|
||||
|
||||
- **`/auth`**
|
||||
- **Description:** Open a dialog that lets you change the authentication
|
||||
method.
|
||||
|
||||
- **`/about`**
|
||||
- **Description:** Show version info. Please share this information when
|
||||
filing issues.
|
||||
|
||||
- [**`/tools`**](../tools/index.md)
|
||||
- **Description:** Display a list of tools that are currently available within
|
||||
Gemini CLI.
|
||||
@@ -311,23 +245,37 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`nodesc`** or **`nodescriptions`**:
|
||||
- **Description:** Hide tool descriptions, showing only the tool names.
|
||||
|
||||
- **`/privacy`**
|
||||
- **Description:** Display the Privacy Notice and allow users to select
|
||||
whether they consent to the collection of their data for service improvement
|
||||
purposes.
|
||||
|
||||
- **`/quit`** (or **`/exit`**)
|
||||
- **Description:** Exit Gemini CLI.
|
||||
|
||||
- **`/vim`**
|
||||
- **Description:** Toggle vim mode on or off. When vim mode is enabled, the
|
||||
input area supports vim-style navigation and editing commands in both NORMAL
|
||||
and INSERT modes.
|
||||
- **Features:**
|
||||
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
|
||||
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
|
||||
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
|
||||
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
|
||||
- **NORMAL mode:** Navigate with `h`, `j`, `k`, `l`; jump by words with `w`,
|
||||
`b`, `e`; go to line start/end with `0`, `$`, `^`; go to specific lines
|
||||
with `G` (or `gg` for first line)
|
||||
- **INSERT mode:** Standard text input with escape to return to NORMAL mode
|
||||
- **Editing commands:** Delete with `x`, change with `c`, insert with `i`,
|
||||
`a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw`
|
||||
- **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`)
|
||||
- **Repeat last command:** Use `.` to repeat the last editing operation
|
||||
- **Persistent setting:** Vim mode preference is saved to
|
||||
`~/.gemini/settings.json` and restored between sessions
|
||||
- **Repeat last command:** Use `.` to repeat the last editing operation
|
||||
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
|
||||
footer
|
||||
- **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the
|
||||
footer
|
||||
|
||||
- **`/init`**
|
||||
- **Description:** To help users easily create a `GEMINI.md` file, this
|
||||
command analyzes the current directory and generates a tailored context
|
||||
file, making it simpler for them to provide project-specific instructions to
|
||||
the Gemini agent.
|
||||
|
||||
### Custom commands
|
||||
|
||||
@@ -340,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 **Cmd+z** or **Alt+z** to undo the last action
|
||||
in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input
|
||||
prompt.
|
||||
|
||||
- **Redo:**
|
||||
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
|
||||
last undone action in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action
|
||||
in the input prompt.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Creating Agent Skills
|
||||
|
||||
This guide provides an overview of how to create your own Agent Skills to extend
|
||||
the capabilities of Gemini CLI.
|
||||
|
||||
## Getting started: The `skill-creator` skill
|
||||
|
||||
The recommended way to create a new skill is to use the built-in `skill-creator`
|
||||
skill. To use it, ask Gemini CLI to create a new skill for you.
|
||||
|
||||
**Example prompt:**
|
||||
|
||||
> "create a new skill called 'code-reviewer'"
|
||||
|
||||
Gemini CLI will then use the `skill-creator` to generate the skill:
|
||||
|
||||
1. Generate a new directory for your skill (e.g., `my-new-skill/`).
|
||||
2. Create a `SKILL.md` file with the necessary YAML frontmatter (`name` and
|
||||
`description`).
|
||||
3. Create the standard resource directories: `scripts/`, `references/`, and
|
||||
`assets/`.
|
||||
|
||||
## Manual skill creation
|
||||
|
||||
If you prefer to create skills manually:
|
||||
|
||||
1. **Create a directory** for your skill (e.g., `my-new-skill/`).
|
||||
2. **Create a `SKILL.md` file** inside the new directory.
|
||||
|
||||
To add additional resources that support the skill, refer to the skill
|
||||
structure.
|
||||
|
||||
## Skill structure
|
||||
|
||||
A skill is a directory containing a `SKILL.md` file at its root.
|
||||
|
||||
### Folder structure
|
||||
|
||||
While a `SKILL.md` file is the only required component, we recommend the
|
||||
following structure for organizing your skill's resources:
|
||||
|
||||
```text
|
||||
my-skill/
|
||||
├── SKILL.md (Required) Instructions and metadata
|
||||
├── scripts/ (Optional) Executable scripts
|
||||
├── references/ (Optional) Static documentation
|
||||
└── assets/ (Optional) Templates and other resources
|
||||
```
|
||||
|
||||
### `SKILL.md` file
|
||||
|
||||
The `SKILL.md` file is the core of your skill. This file uses YAML frontmatter
|
||||
for metadata and Markdown for instructions. For example:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-reviewer
|
||||
description:
|
||||
Use this skill to review code. It supports both local changes and remote Pull
|
||||
Requests.
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
This skill guides the agent in conducting thorough code reviews.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Determine Review Target
|
||||
|
||||
- **Remote PR**: If the user gives a PR number or URL, target that remote PR.
|
||||
- **Local Changes**: If changes are local... ...
|
||||
```
|
||||
|
||||
- **`name`**: A unique identifier for the skill. This should match the directory
|
||||
name.
|
||||
- **`description`**: A description of what the skill does and when Gemini should
|
||||
use it.
|
||||
- **Body**: The Markdown body of the file contains the instructions that guide
|
||||
the agent's behavior when the skill is active.
|
||||
@@ -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. | `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` |
|
||||
| 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` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
@@ -110,7 +110,6 @@ 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 -->
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# 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,7 +57,6 @@ 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` |
|
||||
|
||||
+77
-5
@@ -89,6 +89,83 @@ gemini skills enable my-expertise
|
||||
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.
|
||||
|
||||
### Folder Structure
|
||||
|
||||
Skills are self-contained directories. At a minimum, a skill requires a
|
||||
`SKILL.md` file, but can include other resources:
|
||||
|
||||
```text
|
||||
my-skill/
|
||||
├── SKILL.md (Required) Instructions and metadata
|
||||
├── scripts/ (Optional) Executable scripts/tools
|
||||
├── references/ (Optional) Static documentation and examples
|
||||
└── assets/ (Optional) Templates and binary resources
|
||||
```
|
||||
|
||||
### Basic Structure (SKILL.md)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: <unique-name>
|
||||
description: <what the skill does and when Gemini should use it>
|
||||
---
|
||||
|
||||
<your instructions for how the agent should behave / use the skill>
|
||||
```
|
||||
|
||||
- **`name`**: A unique identifier (lowercase, alphanumeric, and dashes).
|
||||
- **`description`**: The most critical field. Gemini uses this to decide when
|
||||
the skill is relevant. Be specific about the expertise provided.
|
||||
- **Body**: Everything below the second `---` is injected as expert procedural
|
||||
guidance for the model.
|
||||
|
||||
### Example: Team Code Reviewer
|
||||
|
||||
Create `~/.gemini/skills/code-reviewer/SKILL.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-reviewer
|
||||
description:
|
||||
Expertise in reviewing code for style, security, and performance. Use when the
|
||||
user asks for "feedback," a "review," or to "check" their changes.
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
You are an expert code reviewer. When reviewing code, follow this workflow:
|
||||
|
||||
1. **Analyze**: Review the staged changes or specific files provided. Ensure
|
||||
that the changes are scoped properly and represent minimal changes required
|
||||
to address the issue.
|
||||
2. **Style**: Ensure code follows the workspace's conventions and idiomatic
|
||||
patterns as described in the `GEMINI.md` file.
|
||||
3. **Security**: Flag any potential security vulnerabilities.
|
||||
4. **Tests**: Verify that new logic has corresponding test coverage and that
|
||||
the test coverage adequately validates the changes.
|
||||
|
||||
Provide your feedback as a concise bulleted list of "Strengths" and
|
||||
"Opportunities."
|
||||
```
|
||||
|
||||
### Resource Conventions
|
||||
|
||||
While you can structure your skill directory however you like, the Agent Skills
|
||||
standard encourages these conventions:
|
||||
|
||||
- **`scripts/`**: Executable scripts (bash, python, node) the agent can run.
|
||||
- **`references/`**: Static documentation, schemas, or example data for the
|
||||
agent to consult.
|
||||
- **`assets/`**: Code templates, boilerplate, or binary resources.
|
||||
|
||||
When a skill is activated, Gemini CLI provides the model with a tree view of the
|
||||
entire skill directory, allowing it to discover and utilize these assets.
|
||||
|
||||
## How it Works (Security & Privacy)
|
||||
|
||||
1. **Discovery**: At the start of a session, Gemini CLI scans the discovery
|
||||
@@ -105,8 +182,3 @@ gemini skills disable my-expertise --scope workspace
|
||||
it permission to read any bundled assets.
|
||||
5. **Execution**: The model proceeds with the specialized expertise active. It
|
||||
is instructed to prioritize the skill's procedural guidance within reason.
|
||||
|
||||
## Creating your own skills
|
||||
|
||||
To create your own skills, see the
|
||||
[Create Agent Skills](./guides/creating-skills.md) guide.
|
||||
|
||||
@@ -210,10 +210,6 @@ 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"]
|
||||
```
|
||||
@@ -286,7 +282,6 @@ only the `mcpName`.
|
||||
mcpName = "untrusted-server"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
deny_message = "This server is not trusted by the admin."
|
||||
```
|
||||
|
||||
## Default policies
|
||||
@@ -295,9 +290,9 @@ The Gemini CLI ships with a set of default policies to provide a safe
|
||||
out-of-the-box experience.
|
||||
|
||||
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
|
||||
- **Agent delegation** defaults to **`ask_user`** to ensure remote agents can
|
||||
prompt for confirmation, but local sub-agent actions are executed silently and
|
||||
checked individually.
|
||||
- **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to
|
||||
ensure remote agents can prompt for confirmation, but local sub-agent actions
|
||||
are executed silently and checked individually.
|
||||
- **Write tools** (like `write_file`, `run_shell_command`) default to
|
||||
**`ask_user`**.
|
||||
- In **`yolo`** mode, a high-priority rule allows all tools.
|
||||
|
||||
+29
-34
@@ -24,29 +24,30 @@ the main agent's context or toolset.
|
||||
|
||||
## What are sub-agents?
|
||||
|
||||
Sub-agents are "specialists" that the main Gemini agent can hire for a specific
|
||||
job.
|
||||
Think of sub-agents as "specialists" that the main Gemini agent can hire for a
|
||||
specific job.
|
||||
|
||||
- **Focused context:** Each sub-agent has its own system prompt and persona.
|
||||
- **Specialized tools:** Sub-agents can have a restricted or specialized set of
|
||||
tools.
|
||||
- **Independent context window:** Interactions with a sub-agent happen in a
|
||||
separate context loop, which saves tokens in your main conversation history.
|
||||
separate context loop. The main agent only sees the final result, saving
|
||||
tokens in your main conversation history.
|
||||
|
||||
Sub-agents are exposed to the main agent as a tool of the same name. When the
|
||||
main agent calls the tool, it delegates the task to the sub-agent. Once the
|
||||
sub-agent completes its task, it reports back to the main agent with its
|
||||
findings.
|
||||
Sub-agents are exposed to the main agent as a tool of the same name which
|
||||
delegates to the sub-agent, when called. Once the sub-agent completes its task
|
||||
(or fails), it reports back to the main agent with its findings (usually as a
|
||||
text summary or structured report returned by the tool).
|
||||
|
||||
## Built-in sub-agents
|
||||
|
||||
Gemini CLI comes with the following built-in sub-agents:
|
||||
Gemini CLI comes with powerful built-in sub-agents.
|
||||
|
||||
### Codebase Investigator
|
||||
|
||||
- **Name:** `codebase_investigator`
|
||||
- **Purpose:** Analyze the codebase, reverse engineer, and understand complex
|
||||
dependencies.
|
||||
- **Purpose:** Deep analysis of the codebase, reverse engineering, and
|
||||
understanding complex dependencies.
|
||||
- **When to use:** "How does the authentication system work?", "Map out the
|
||||
dependencies of the `AgentRegistry` class."
|
||||
- **Configuration:** Enabled by default. You can configure it in
|
||||
@@ -66,25 +67,20 @@ Gemini CLI comes with the following built-in sub-agents:
|
||||
### CLI Help Agent
|
||||
|
||||
- **Name:** `cli_help`
|
||||
- **Purpose:** Get expert knowledge about Gemini CLI itself, its commands,
|
||||
- **Purpose:** Expert knowledge about Gemini CLI itself, its commands,
|
||||
configuration, and documentation.
|
||||
- **When to use:** "How do I configure a proxy?", "What does the `/rewind`
|
||||
command do?"
|
||||
- **Configuration:** Enabled by default.
|
||||
|
||||
### Generalist Agent
|
||||
|
||||
- **Name:** `generalist_agent`
|
||||
- **Purpose:** Route tasks to the appropriate specialized sub-agent.
|
||||
- **When to use:** Implicitly used by the main agent for routing. Not directly
|
||||
invoked by the user.
|
||||
- **Configuration:** Enabled by default. No specific configuration options.
|
||||
|
||||
## Creating custom sub-agents
|
||||
|
||||
You can create your own sub-agents to automate specific workflows or enforce
|
||||
specific personas. To use custom sub-agents, you must enable them in your
|
||||
`settings.json`:
|
||||
specific personas.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To use custom sub-agents, you must enable them in your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -127,10 +123,10 @@ vulnerabilities.
|
||||
|
||||
Focus on:
|
||||
|
||||
1. SQL Injection
|
||||
2. XSS (Cross-Site Scripting)
|
||||
3. Hardcoded credentials
|
||||
4. Unsafe file operations
|
||||
1. SQL Injection
|
||||
2. XSS (Cross-Site Scripting)
|
||||
3. Hardcoded credentials
|
||||
4. Unsafe file operations
|
||||
|
||||
When you find a vulnerability, explain it clearly and suggest a fix. Do not fix
|
||||
it yourself; just report it.
|
||||
@@ -151,16 +147,17 @@ it yourself; just report it.
|
||||
|
||||
### Optimizing your sub-agent
|
||||
|
||||
The main agent's system prompt encourages it to use an expert sub-agent when one
|
||||
is available. It decides whether an agent is a relevant expert based on the
|
||||
agent's description. You can improve the reliability with which an agent is used
|
||||
by updating the description to more clearly indicate:
|
||||
The main agent system prompt contains language that encourages use of an expert
|
||||
sub-agent when one is available for the task at hand. It decides whether an
|
||||
agent is a relevant expert based on the agent's description. You can improve the
|
||||
reliability with which an agent is used by updating the description to more
|
||||
clearly indicate:
|
||||
|
||||
- Its area of expertise.
|
||||
- When it should be used.
|
||||
- Some example scenarios.
|
||||
|
||||
For example, the following sub-agent description should be called fairly
|
||||
For example: the following sub-agent description should be called fairly
|
||||
consistently for Git operations.
|
||||
|
||||
> Git expert agent which should be used for all local and remote git operations.
|
||||
@@ -174,13 +171,11 @@ 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) (experimental)
|
||||
## Remote subagents (Agent2Agent)
|
||||
|
||||
Gemini CLI can also delegate tasks to remote sub-agents using the Agent-to-Agent
|
||||
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,10 +244,6 @@ 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.
|
||||
@@ -873,6 +869,11 @@ 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).
|
||||
@@ -1185,10 +1186,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- **Description:** The path to your Google Application Credentials JSON file.
|
||||
- **Example:**
|
||||
`export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
|
||||
- **`GOOGLE_GENAI_API_VERSION`**:
|
||||
- Specifies the API version to use for Gemini API requests.
|
||||
- When set, overrides the default API version used by the SDK.
|
||||
- Example: `export GOOGLE_GENAI_API_VERSION="v1"`
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
|
||||
+2
-14
@@ -7,8 +7,7 @@
|
||||
{ "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": "CLI Reference", "slug": "docs/cli/cli-reference" }
|
||||
{ "label": "Gemini 3 (preview)", "slug": "docs/get-started/gemini-3" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -79,10 +78,6 @@
|
||||
"label": "Ecosystem and extensibility",
|
||||
"items": [
|
||||
{ "label": "Agent skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Creating Agent skills",
|
||||
"slug": "docs/cli/creating-skills"
|
||||
},
|
||||
{
|
||||
"label": "Sub-agents (experimental)",
|
||||
"slug": "docs/core/subagents"
|
||||
@@ -123,14 +118,7 @@
|
||||
"items": [
|
||||
{ "label": "FAQ", "slug": "docs/faq" },
|
||||
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
|
||||
{
|
||||
"label": "Releases",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
]
|
||||
},
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
|
||||
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
|
||||
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
|
||||
|
||||
@@ -739,21 +739,10 @@ The MCP integration tracks several states:
|
||||
cautiously and only for servers you completely control
|
||||
- **Access tokens:** Be security-aware when configuring environment variables
|
||||
containing API keys or tokens
|
||||
- **Environment variable redaction:** By default, the Gemini CLI redacts
|
||||
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
|
||||
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
|
||||
spawning MCP servers using the `stdio` transport. This prevents unintended
|
||||
exposure of your credentials to third-party servers.
|
||||
- **Explicit environment variables:** If you need to pass a specific environment
|
||||
variable to an MCP server, you should define it explicitly in the `env`
|
||||
property of the server configuration in `settings.json`.
|
||||
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
||||
available within the sandbox environment.
|
||||
available within the sandbox environment
|
||||
- **Private data:** Using broadly scoped personal access tokens can lead to
|
||||
information leakage between repositories.
|
||||
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
|
||||
untrusted or third-party sources. Malicious servers could attempt to
|
||||
exfiltrate data or perform unauthorized actions through the tools they expose.
|
||||
information leakage between repositories
|
||||
|
||||
### Performance and resource management
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ describe('generalist_agent', () => {
|
||||
prompt:
|
||||
'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success',
|
||||
assert: async (rig) => {
|
||||
// 1) Verify the generalist agent was invoked
|
||||
// 1) Verify the generalist agent was invoked via delegate_to_agent
|
||||
const foundToolCall = await rig.waitForToolCall('generalist');
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a tool call for generalist agent',
|
||||
'Expected to find a delegate_to_agent tool call for generalist agent',
|
||||
).toBeTruthy();
|
||||
|
||||
// 2) Verify the file was created as expected
|
||||
|
||||
+10
-23
@@ -34,10 +34,6 @@ 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);
|
||||
|
||||
@@ -66,9 +62,6 @@ 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 =
|
||||
@@ -80,16 +73,9 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
}
|
||||
|
||||
await evalCase.assert(rig, result);
|
||||
isSuccess = true;
|
||||
} finally {
|
||||
if (isSuccess) {
|
||||
await fs.promises.unlink(activityLogFile).catch((err) => {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
});
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
logFile,
|
||||
await logToFile(
|
||||
evalCase.name,
|
||||
JSON.stringify(rig.readToolLogs(), null, 2),
|
||||
);
|
||||
await rig.cleanup();
|
||||
@@ -103,13 +89,6 @@ 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>;
|
||||
@@ -118,3 +97,11 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { GitService, Storage } from '@google/gemini-cli-core';
|
||||
|
||||
describe('Checkpointing Integration', () => {
|
||||
let tmpDir: string;
|
||||
let projectRoot: string;
|
||||
let fakeHome: string;
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'gemini-checkpoint-test-'),
|
||||
);
|
||||
projectRoot = path.join(tmpDir, 'project');
|
||||
fakeHome = path.join(tmpDir, 'home');
|
||||
|
||||
await fs.mkdir(projectRoot, { recursive: true });
|
||||
await fs.mkdir(fakeHome, { recursive: true });
|
||||
|
||||
// Save original env
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Simulate environment with NO global gitconfig
|
||||
process.env['HOME'] = fakeHome;
|
||||
delete process.env['GIT_CONFIG_GLOBAL'];
|
||||
delete process.env['GIT_CONFIG_SYSTEM'];
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore env
|
||||
process.env = originalEnv;
|
||||
|
||||
// Cleanup
|
||||
try {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
console.error('Failed to cleanup temp dir', e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should successfully create and restore snapshots without global git config', async () => {
|
||||
const storage = new Storage(projectRoot);
|
||||
const gitService = new GitService(projectRoot, storage);
|
||||
|
||||
// 1. Initialize
|
||||
await gitService.initialize();
|
||||
|
||||
// Verify system config empty file creation
|
||||
// We need to access getHistoryDir logic or replicate it.
|
||||
// Since we don't have access to private getHistoryDir, we can infer it or just trust the functional test.
|
||||
|
||||
// 2. Create initial state
|
||||
await fs.writeFile(path.join(projectRoot, 'file1.txt'), 'version 1');
|
||||
await fs.writeFile(path.join(projectRoot, 'file2.txt'), 'permanent file');
|
||||
|
||||
// 3. Create Snapshot
|
||||
const snapshotHash = await gitService.createFileSnapshot('Checkpoint 1');
|
||||
expect(snapshotHash).toBeDefined();
|
||||
|
||||
// 4. Modify files
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, 'file1.txt'),
|
||||
'version 2 (BAD CHANGE)',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, 'file3.txt'),
|
||||
'new file (SHOULD BE GONE)',
|
||||
);
|
||||
await fs.rm(path.join(projectRoot, 'file2.txt'));
|
||||
|
||||
// 5. Restore
|
||||
await gitService.restoreProjectFromSnapshot(snapshotHash);
|
||||
|
||||
// 6. Verify state
|
||||
const file1Content = await fs.readFile(
|
||||
path.join(projectRoot, 'file1.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(file1Content).toBe('version 1');
|
||||
|
||||
const file2Exists = await fs
|
||||
.stat(path.join(projectRoot, 'file2.txt'))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(file2Exists).toBe(true);
|
||||
const file2Content = await fs.readFile(
|
||||
path.join(projectRoot, 'file2.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(file2Content).toBe('permanent file');
|
||||
|
||||
const file3Exists = await fs
|
||||
.stat(path.join(projectRoot, 'file3.txt'))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(file3Exists).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore user global git config and use isolated identity', async () => {
|
||||
// 1. Create a fake global gitconfig with a specific user
|
||||
const globalConfigPath = path.join(fakeHome, '.gitconfig');
|
||||
const globalConfigContent = `[user]
|
||||
name = Global User
|
||||
email = global@example.com
|
||||
`;
|
||||
await fs.writeFile(globalConfigPath, globalConfigContent);
|
||||
|
||||
// Point HOME to fakeHome so git picks up this global config (if we didn't isolate it)
|
||||
process.env['HOME'] = fakeHome;
|
||||
// Ensure GIT_CONFIG_GLOBAL is NOT set for the process initially,
|
||||
// so it would default to HOME/.gitconfig if GitService didn't override it.
|
||||
delete process.env['GIT_CONFIG_GLOBAL'];
|
||||
|
||||
const storage = new Storage(projectRoot);
|
||||
const gitService = new GitService(projectRoot, storage);
|
||||
|
||||
await gitService.initialize();
|
||||
|
||||
// 2. Create a file and snapshot
|
||||
await fs.writeFile(path.join(projectRoot, 'test.txt'), 'content');
|
||||
await gitService.createFileSnapshot('Snapshot with global config present');
|
||||
|
||||
// 3. Verify the commit author in the shadow repo
|
||||
const historyDir = storage.getHistoryDir();
|
||||
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
|
||||
const logOutput = execFileSync(
|
||||
'git',
|
||||
['log', '-1', '--pretty=format:%an <%ae>'],
|
||||
{
|
||||
cwd: historyDir,
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_DIR: path.join(historyDir, '.git'),
|
||||
GIT_CONFIG_GLOBAL: path.join(historyDir, '.gitconfig'),
|
||||
GIT_CONFIG_SYSTEM: path.join(historyDir, '.gitconfig_system_empty'),
|
||||
},
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
);
|
||||
|
||||
expect(logOutput).toBe('Gemini CLI <gemini-cli@google.com>');
|
||||
expect(logOutput).not.toContain('Global User');
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,6 @@ import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
|
||||
import { disableMouseTracking } from '@google/gemini-cli-core';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
@@ -73,11 +72,6 @@ export async function setup() {
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
// Disable mouse tracking
|
||||
if (process.stdout.isTTY) {
|
||||
disableMouseTracking();
|
||||
}
|
||||
|
||||
// Cleanup the test run directory unless KEEP_OUTPUT is set
|
||||
if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) {
|
||||
try {
|
||||
|
||||
@@ -484,19 +484,9 @@ console.log(JSON.stringify({
|
||||
'hooks-system.before-tool-selection.responses',
|
||||
),
|
||||
});
|
||||
|
||||
// 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);
|
||||
// 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']}}}))\"";
|
||||
|
||||
rig.setup('should modify tool selection with BeforeToolSelection hooks', {
|
||||
settings: {
|
||||
@@ -510,7 +500,7 @@ console.log(JSON.stringify({
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: `node "${scriptPath.replace(/\\/g, '/')}"`,
|
||||
command: hookCommand,
|
||||
timeout: 5000,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"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}]}}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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}]}}]}
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* @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
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -17999,7 +17999,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -18055,7 +18055,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
@@ -18142,7 +18142,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.8",
|
||||
@@ -18300,7 +18300,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18317,7 +18317,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.28.0-nightly.20260128.adc8e11bb"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-preview.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.28.0-nightly.20260128.adc8e11bb",
|
||||
"version": "0.27.0-preview.5",
|
||||
"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.28.0-nightly.20260128.adc8e11bb"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-preview.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"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,7 +24,6 @@ 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));
|
||||
@@ -41,7 +40,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
...actual,
|
||||
coreEvents: {
|
||||
emitConsoleLog,
|
||||
emitFeedback,
|
||||
},
|
||||
debugLogger,
|
||||
};
|
||||
@@ -86,42 +84,6 @@ 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 { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import { 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,21 +46,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
(extension) => extension.name === args.name,
|
||||
);
|
||||
if (!extension) {
|
||||
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.`,
|
||||
);
|
||||
debugLogger.log(`Extension "${args.name}" not found.`);
|
||||
return;
|
||||
}
|
||||
if (!extension.installMetadata) {
|
||||
@@ -77,6 +63,7 @@ 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,13 +128,6 @@ async function addMcpServer(
|
||||
|
||||
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
||||
|
||||
if (transport === 'stdio') {
|
||||
debugLogger.warn(
|
||||
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
|
||||
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isExistingServer) {
|
||||
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
||||
} else {
|
||||
|
||||
@@ -588,7 +588,7 @@ describe('parseArguments', () => {
|
||||
process.argv = ['node', 'script.js', 'skills', 'list'];
|
||||
// Skills command enabled by default or via experimental
|
||||
const settings = createTestMergedSettings({
|
||||
skills: { enabled: true },
|
||||
experimental: { skills: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.isCommand).toBe(true);
|
||||
|
||||
@@ -304,7 +304,7 @@ export async function parseArguments(
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings.skills?.enabled ?? true) {
|
||||
if (settings.experimental?.skills || (settings.skills?.enabled ?? true)) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
// Register hooks command if hooks are enabled
|
||||
@@ -755,7 +755,8 @@ export async function loadCliConfig(
|
||||
plan: settings.experimental?.plan,
|
||||
enableEventDrivenScheduler:
|
||||
settings.experimental?.enableEventDrivenScheduler,
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
skillsSupport:
|
||||
settings.experimental?.skills || (settings.skills?.enabled ?? true),
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
/**
|
||||
* @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,7 +69,6 @@ import {
|
||||
ExtensionSettingScope,
|
||||
} from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { themeManager } from '../ui/themes/theme-manager.js';
|
||||
|
||||
interface ExtensionManagerParams {
|
||||
enabledExtensionOverrides?: string[];
|
||||
@@ -469,20 +468,6 @@ 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.
|
||||
*/
|
||||
@@ -713,7 +698,6 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
resolvedSettings,
|
||||
skills,
|
||||
agents: agentLoadResult.agents,
|
||||
themes: config.themes,
|
||||
};
|
||||
this.loadedExtensions = [...this.loadedExtensions, extension];
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type {
|
||||
MCPServerConfig,
|
||||
ExtensionInstallMetadata,
|
||||
CustomTheme,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -28,11 +27,6 @@ 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 {
|
||||
|
||||
@@ -85,7 +85,6 @@ export enum Command {
|
||||
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
|
||||
CLEAR_SCREEN = 'app.clearScreen',
|
||||
RESTART_APP = 'app.restart',
|
||||
SUSPEND_APP = 'app.suspend',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,15 +170,8 @@ 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', 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 },
|
||||
],
|
||||
[Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
|
||||
[Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
|
||||
|
||||
// Scrolling
|
||||
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
|
||||
@@ -273,7 +265,6 @@ 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 {
|
||||
@@ -383,7 +374,6 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
Command.UNFOCUS_SHELL_INPUT,
|
||||
Command.CLEAR_SCREEN,
|
||||
Command.RESTART_APP,
|
||||
Command.SUSPEND_APP,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -474,5 +464,4 @@ 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).',
|
||||
};
|
||||
|
||||
@@ -2156,7 +2156,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
// 2. Now, set remote admin settings.
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
strictModeDisabled: false,
|
||||
secureModeEnabled: true,
|
||||
mcpSetting: { mcpEnabled: false },
|
||||
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
|
||||
});
|
||||
@@ -2197,7 +2197,7 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
||||
|
||||
const newRemoteSettings = {
|
||||
strictModeDisabled: false,
|
||||
secureModeEnabled: true,
|
||||
mcpSetting: { mcpEnabled: false },
|
||||
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
|
||||
};
|
||||
@@ -2213,7 +2213,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
// Verify that calling setRemoteAdminSettings with partial data overwrites previous remote settings
|
||||
// and missing properties revert to schema defaults.
|
||||
loadedSettings.setRemoteAdminSettings({ strictModeDisabled: true });
|
||||
loadedSettings.setRemoteAdminSettings({ secureModeEnabled: false });
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false); // Defaulting to false if missing
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false); // Defaulting to false if missing
|
||||
@@ -2271,9 +2271,9 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
|
||||
// Set remote settings with only strictModeDisabled (false -> secureModeEnabled: true)
|
||||
// Set remote settings with only secureModeEnabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
strictModeDisabled: false,
|
||||
secureModeEnabled: true,
|
||||
});
|
||||
|
||||
// Verify secureModeEnabled is updated, others default to false
|
||||
@@ -2286,8 +2286,8 @@ describe('Settings Loading and Merging', () => {
|
||||
mcpSetting: { mcpEnabled: false },
|
||||
});
|
||||
|
||||
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled defaults to true if strictModeDisabled is missing)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled reverts to default:false)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
|
||||
@@ -2297,47 +2297,23 @@ describe('Settings Loading and Merging', () => {
|
||||
});
|
||||
|
||||
// Verify extensionsEnabled is updated, others remain defaults
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
|
||||
// Verify that missing strictModeDisabled falls back to secureModeEnabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
secureModeEnabled: false,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
secureModeEnabled: true,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
|
||||
// Verify strictModeDisabled takes precedence over secureModeEnabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
strictModeDisabled: false,
|
||||
secureModeEnabled: false,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
strictModeDisabled: true,
|
||||
secureModeEnabled: true,
|
||||
});
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||
it('should set skills based on advancedFeaturesEnabled', () => {
|
||||
const loadedSettings = loadSettings();
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
cliFeatureSetting: {
|
||||
unmanagedCapabilitiesEnabled: true,
|
||||
advancedFeaturesEnabled: true,
|
||||
},
|
||||
});
|
||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
cliFeatureSetting: {
|
||||
unmanagedCapabilitiesEnabled: false,
|
||||
advancedFeaturesEnabled: false,
|
||||
},
|
||||
});
|
||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(false);
|
||||
@@ -2361,12 +2337,12 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should force secureModeEnabled to true if undefined, overriding schema defaults', () => {
|
||||
// Mock schema to have secureModeEnabled default to false to verify the override
|
||||
it('should force secureModeEnabled to false if undefined, overriding schema defaults', () => {
|
||||
// Mock schema to have secureModeEnabled default to true to verify the override
|
||||
const originalSchema = getSettingsSchema();
|
||||
const modifiedSchema = JSON.parse(JSON.stringify(originalSchema));
|
||||
if (modifiedSchema.admin?.properties?.secureModeEnabled) {
|
||||
modifiedSchema.admin.properties.secureModeEnabled.default = false;
|
||||
modifiedSchema.admin.properties.secureModeEnabled.default = true;
|
||||
}
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(modifiedSchema);
|
||||
|
||||
@@ -2376,13 +2352,13 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Pass a non-empty object that doesn't have strictModeDisabled
|
||||
// Pass a non-empty object that doesn't have secureModeEnabled
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
mcpSetting: {},
|
||||
});
|
||||
|
||||
// It should be forced to true by the logic (default secure), overriding the mock default of false
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
// It should be forced to false by the logic, overriding the mock default of true
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
} finally {
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(originalSchema);
|
||||
}
|
||||
|
||||
@@ -338,10 +338,6 @@ export class LoadedSettings {
|
||||
}
|
||||
|
||||
setValue(scope: LoadableSettingScope, key: string, value: unknown): void {
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
'saving: ' + key + ', ' + value + ', scope: ' + scope,
|
||||
);
|
||||
const settingsFile = this.forScope(scope);
|
||||
setNestedProperty(settingsFile.settings, key, value);
|
||||
setNestedProperty(settingsFile.originalSettings, key, value);
|
||||
@@ -352,12 +348,7 @@ export class LoadedSettings {
|
||||
|
||||
setRemoteAdminSettings(remoteSettings: FetchAdminControlsResponse): void {
|
||||
const admin: Settings['admin'] = {};
|
||||
const {
|
||||
secureModeEnabled,
|
||||
strictModeDisabled,
|
||||
mcpSetting,
|
||||
cliFeatureSetting,
|
||||
} = remoteSettings;
|
||||
const { secureModeEnabled, mcpSetting, cliFeatureSetting } = remoteSettings;
|
||||
|
||||
if (Object.keys(remoteSettings).length === 0) {
|
||||
this._remoteAdminSettings = { admin };
|
||||
@@ -365,20 +356,15 @@ export class LoadedSettings {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strictModeDisabled !== undefined) {
|
||||
admin.secureModeEnabled = !strictModeDisabled;
|
||||
} else if (secureModeEnabled !== undefined) {
|
||||
admin.secureModeEnabled = secureModeEnabled;
|
||||
} else {
|
||||
admin.secureModeEnabled = true;
|
||||
}
|
||||
admin.secureModeEnabled = secureModeEnabled ?? false;
|
||||
admin.mcp = { enabled: mcpSetting?.mcpEnabled ?? false };
|
||||
admin.extensions = {
|
||||
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled ?? false,
|
||||
};
|
||||
admin.skills = {
|
||||
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
|
||||
};
|
||||
|
||||
if (cliFeatureSetting?.advancedFeaturesEnabled !== undefined) {
|
||||
admin.skills = { enabled: cliFeatureSetting.advancedFeaturesEnabled };
|
||||
}
|
||||
|
||||
this._remoteAdminSettings = { admin };
|
||||
this._merged = this.computeMergedSettings();
|
||||
@@ -731,10 +717,6 @@ export function migrateDeprecatedSettings(
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
'setting new general for scope:' + scope,
|
||||
);
|
||||
loadedSettings.setValue(scope, 'general', newGeneral);
|
||||
anyModified = true;
|
||||
}
|
||||
@@ -855,7 +837,6 @@ export function saveSettings(settingsFile: SettingsFile): void {
|
||||
settingsToSave as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
coreEvents.emitFeedback('error', 'here' + error, error);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'There was an error saving your latest settings changes.',
|
||||
|
||||
@@ -9,17 +9,19 @@
|
||||
// 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';
|
||||
|
||||
@@ -524,16 +526,6 @@ 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',
|
||||
@@ -1498,6 +1490,15 @@ 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',
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -32,10 +32,6 @@ import {
|
||||
runExitCleanup,
|
||||
registerTelemetryConfig,
|
||||
} from './utils/cleanup.js';
|
||||
import {
|
||||
cleanupToolOutputFiles,
|
||||
cleanupExpiredSessions,
|
||||
} from './utils/sessionCleanup.js';
|
||||
import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
@@ -75,6 +71,7 @@ import {
|
||||
} from './core/initializer.js';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { runZedIntegration } from './zed-integration/zedIntegration.js';
|
||||
import { cleanupExpiredSessions } from './utils/sessionCleanup.js';
|
||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
||||
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
||||
@@ -323,10 +320,7 @@ export async function main() {
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
cleanupCheckpoints(),
|
||||
cleanupToolOutputFiles(settings.merged),
|
||||
]);
|
||||
await cleanupCheckpoints();
|
||||
|
||||
const parseArgsHandle = startupProfiler.start('parse_arguments');
|
||||
const argv = await parseArguments(settings.merged);
|
||||
@@ -371,10 +365,9 @@ export async function main() {
|
||||
process.env['CLOUD_SHELL'] === 'true' ||
|
||||
process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true'
|
||||
) {
|
||||
coreEvents.emitFeedback('info', 'saving compute_adc');
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
'selectedAuthType',
|
||||
AuthType.COMPUTE_ADC,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,11 +38,6 @@ 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(),
|
||||
@@ -264,52 +259,6 @@ 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,14 +70,6 @@ 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,12 +119,11 @@ describe('BuiltinCommandLoader', () => {
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
getExtensionsEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
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;
|
||||
|
||||
@@ -261,12 +260,11 @@ describe('BuiltinCommandLoader profile', () => {
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
getExtensionsEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
|
||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||
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,13 +9,12 @@ 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',
|
||||
@@ -26,20 +25,12 @@ 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,
|
||||
themes,
|
||||
}),
|
||||
JSON.stringify({ name, version, contextFileName, mcpServers, settings }),
|
||||
);
|
||||
|
||||
if (addContextFile) {
|
||||
|
||||
@@ -21,7 +21,6 @@ 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,
|
||||
@@ -29,16 +28,8 @@ 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(() => ({
|
||||
@@ -53,11 +44,6 @@ 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() },
|
||||
@@ -87,9 +73,6 @@ 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(),
|
||||
})),
|
||||
@@ -124,11 +107,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -204,7 +185,6 @@ 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;
|
||||
@@ -279,7 +259,6 @@ describe('AppContainer State Management', () => {
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
capturedUIState = null!;
|
||||
capturedAskUserRequest = null;
|
||||
|
||||
// **Provide a default return value for EVERY mocked hook.**
|
||||
mockedUseQuotaAndFallback.mockReturnValue({
|
||||
@@ -416,7 +395,6 @@ describe('AppContainer State Management', () => {
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: false,
|
||||
hideWindowTitle: false,
|
||||
showUserIdentity: true,
|
||||
},
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
@@ -488,162 +466,6 @@ 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;
|
||||
@@ -2069,7 +1891,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
vi.advanceTimersByTime(1001);
|
||||
});
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
@@ -2114,7 +1936,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
vi.advanceTimersByTime(1001);
|
||||
});
|
||||
|
||||
pressKey({ name: 'd', ctrl: true });
|
||||
@@ -2676,41 +2498,6 @@ 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,7 +47,6 @@ import {
|
||||
getErrorMessage,
|
||||
getAllGeminiMdFilenames,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
clearCachedCredentialFile,
|
||||
type ResumedSessionData,
|
||||
recordExitFail,
|
||||
@@ -187,58 +186,11 @@ 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>('');
|
||||
@@ -1491,9 +1443,6 @@ 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,7 +92,6 @@ describe('bugCommand', () => {
|
||||
getHistory: () => [],
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -107,7 +106,6 @@ 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
|
||||
@@ -135,7 +133,6 @@ describe('bugCommand', () => {
|
||||
getHistory: () => history,
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
|
||||
storage: {
|
||||
getProjectTempDir: () => '/tmp/gemini',
|
||||
},
|
||||
@@ -181,7 +178,6 @@ describe('bugCommand', () => {
|
||||
getHistory: () => [],
|
||||
}),
|
||||
}),
|
||||
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -196,7 +192,6 @@ 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,7 +54,6 @@ export const bugCommand: SlashCommand = {
|
||||
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
|
||||
? 'Supported'
|
||||
: 'Unsupported';
|
||||
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
|
||||
|
||||
let info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
@@ -63,7 +62,6 @@ 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,9 +67,6 @@ 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,13 +57,6 @@ 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,7 +11,6 @@ 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;
|
||||
@@ -35,11 +34,6 @@ 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(() => {
|
||||
@@ -62,17 +56,6 @@ 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: {
|
||||
@@ -80,15 +63,7 @@ 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({
|
||||
@@ -297,12 +272,11 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should enable a hook and update settings', async () => {
|
||||
// Update the user settings with disabled hooks
|
||||
mockSettings.user.settings.hooksConfig.disabled = [
|
||||
// Update the context's settings with disabled hooks
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [
|
||||
'test-hook',
|
||||
'other-hook',
|
||||
];
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = [];
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
@@ -314,7 +288,7 @@ describe('hooksCommand', () => {
|
||||
const result = await enableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
expect.any(String),
|
||||
'hooksConfig.disabled',
|
||||
['other-hook'],
|
||||
);
|
||||
@@ -325,8 +299,28 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'Hook "test-hook" enabled by removing it from the disabled list in user (/mock/user.json) and workspace (/mock/workspace.json) settings.',
|
||||
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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -338,7 +332,7 @@ describe('hooksCommand', () => {
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
false, // Must be disabled for enable completion
|
||||
true,
|
||||
);
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
|
||||
@@ -410,9 +404,7 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should disable a hook and update settings', async () => {
|
||||
// Ensure not disabled anywhere
|
||||
mockSettings.workspace.settings.hooksConfig.disabled = [];
|
||||
mockSettings.user.settings.hooksConfig.disabled = [];
|
||||
mockContext.services.settings.merged.hooksConfig.disabled = [];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -423,9 +415,8 @@ describe('hooksCommand', () => {
|
||||
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
// Should default to workspace if present
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.Workspace,
|
||||
expect.any(String),
|
||||
'hooksConfig.disabled',
|
||||
['test-hook'],
|
||||
);
|
||||
@@ -436,14 +427,13 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'Hook "test-hook" disabled by adding it to the disabled list in workspace (/mock/workspace.json) settings.',
|
||||
content: 'Hook "test-hook" disabled successfully.',
|
||||
});
|
||||
});
|
||||
|
||||
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'];
|
||||
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'];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -455,29 +445,38 @@ 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" is already disabled.',
|
||||
content: 'Hook "test-hook" disabled successfully.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should complete hook names using friendly names', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
|
||||
const hookEntry = createMockHook(
|
||||
'./hooks/test.sh',
|
||||
HookEventName.BeforeTool,
|
||||
true, // Must be enabled for disable completion
|
||||
);
|
||||
hookEntry.config.name = 'friendly-name';
|
||||
if (!disableCmd?.action) {
|
||||
throw new Error('disable command must have an action');
|
||||
}
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue([hookEntry]);
|
||||
const result = await disableCmd.action(mockContext, 'test-hook');
|
||||
|
||||
const completions = disableCmd.completion!(mockContext, 'frie');
|
||||
expect(completions).toContain('friendly-name');
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to disable hook: Failed to save settings',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -514,52 +513,50 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return matching hook names based on status', () => {
|
||||
it('should return matching hook names', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('test-hook-enabled', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook-disabled', HookEventName.AfterTool, false),
|
||||
createMockHook('test-hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('test-hook-2', HookEventName.AfterTool, true),
|
||||
createMockHook('other-hook', HookEventName.AfterAgent, false),
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
)!;
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
);
|
||||
if (!enableCmd?.completion) {
|
||||
throw new Error('enable command must have completion');
|
||||
}
|
||||
|
||||
const enableResult = enableCmd.completion!(mockContext, 'test');
|
||||
expect(enableResult).toEqual(['test-hook-disabled']);
|
||||
|
||||
const disableResult = disableCmd.completion!(mockContext, 'test');
|
||||
expect(disableResult).toEqual(['test-hook-enabled']);
|
||||
const result = enableCmd.completion(mockContext, 'test');
|
||||
expect(result).toEqual(['test-hook-1', 'test-hook-2']);
|
||||
});
|
||||
|
||||
it('should return all relevant hook names when partial is empty', () => {
|
||||
it('should return all hook names when partial is empty', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('hook-enabled', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-disabled', HookEventName.AfterTool, false),
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
)!;
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
)!;
|
||||
);
|
||||
if (!enableCmd?.completion) {
|
||||
throw new Error('enable command must have completion');
|
||||
}
|
||||
|
||||
expect(enableCmd.completion!(mockContext, '')).toEqual(['hook-disabled']);
|
||||
expect(disableCmd.completion!(mockContext, '')).toEqual(['hook-enabled']);
|
||||
const result = enableCmd.completion(mockContext, '');
|
||||
expect(result).toEqual(['hook-1', 'hook-2']);
|
||||
});
|
||||
|
||||
it('should handle hooks without command name gracefully', () => {
|
||||
const mockHooks: HookRegistryEntry[] = [
|
||||
createMockHook('test-hook', HookEventName.BeforeTool, false),
|
||||
createMockHook('test-hook', HookEventName.BeforeTool, true),
|
||||
{
|
||||
...createMockHook('', HookEventName.AfterTool, false),
|
||||
...createMockHook('', HookEventName.AfterTool, true),
|
||||
config: { command: '', type: HookType.Command, timeout: 30 },
|
||||
},
|
||||
];
|
||||
@@ -639,7 +636,7 @@ describe('hooksCommand', () => {
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String), // enableAll uses legacy logic so it might return 'Workspace' or 'User' depending on ternary
|
||||
expect.any(String),
|
||||
'hooksConfig.disabled',
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -12,9 +12,7 @@ import type {
|
||||
MessageActionReturn,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { SettingScope, isLoadableSettingScope } from '../../config/settings.js';
|
||||
import { enableHook, disableHook } from '../../utils/hookSettings.js';
|
||||
import { renderHookActionFeedback } from '../../utils/hookUtils.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
|
||||
/**
|
||||
* Display a formatted list of hooks with their status
|
||||
@@ -76,23 +74,39 @@ async function enableAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const result = enableHook(settings, hookName);
|
||||
|
||||
if (result.status === 'success') {
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
}
|
||||
|
||||
const feedback = renderHookActionFeedback(
|
||||
result,
|
||||
(label, path) => `${label} (${path})`,
|
||||
const disabledHooks = settings.merged.hooksConfig.disabled;
|
||||
// Remove from disabled list if present
|
||||
const newDisabledHooks = disabledHooks.filter(
|
||||
(name: string) => name !== hookName,
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: result.status === 'error' ? 'error' : 'info',
|
||||
content: feedback,
|
||||
};
|
||||
// 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)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,31 +143,44 @@ async function disableAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const scope = settings.workspace ? SettingScope.Workspace : SettingScope.User;
|
||||
const disabledHooks = settings.merged.hooksConfig.disabled;
|
||||
// Add to disabled list if not already present
|
||||
try {
|
||||
if (!disabledHooks.includes(hookName)) {
|
||||
const newDisabledHooks = [...disabledHooks, hookName];
|
||||
|
||||
const result = disableHook(settings, hookName, scope);
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooksConfig.disabled', newDisabledHooks);
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
// 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
|
||||
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 enabled hook names (to be disabled)
|
||||
* Completion function for hook names
|
||||
*/
|
||||
function completeEnabledHookNames(
|
||||
function completeHookNames(
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): string[] {
|
||||
@@ -164,30 +191,8 @@ function completeEnabledHookNames(
|
||||
if (!hookSystem) return [];
|
||||
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
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));
|
||||
const hookNames = allHooks.map((hook) => getHookDisplayName(hook));
|
||||
return hookNames.filter((name) => name.startsWith(partialArg));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,12 +247,13 @@ async function enableAllAction(
|
||||
}
|
||||
|
||||
try {
|
||||
const scopes = [SettingScope.Workspace, SettingScope.User];
|
||||
for (const scope of scopes) {
|
||||
if (isLoadableSettingScope(scope)) {
|
||||
settings.setValue(scope, 'hooksConfig.disabled', []);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
for (const hook of disabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
@@ -319,6 +325,9 @@ 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);
|
||||
@@ -352,7 +361,7 @@ const enableCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: enableAction,
|
||||
completion: completeDisabledHookNames,
|
||||
completion: completeHookNames,
|
||||
};
|
||||
|
||||
const disableCommand: SlashCommand = {
|
||||
@@ -361,7 +370,7 @@ const disableCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: disableAction,
|
||||
completion: completeEnabledHookNames,
|
||||
completion: completeHookNames,
|
||||
};
|
||||
|
||||
const enableAllCommand: SlashCommand = {
|
||||
|
||||
@@ -12,17 +12,6 @@ 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');
|
||||
@@ -51,9 +40,6 @@ describe('statsCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.STATS,
|
||||
duration: expectedDuration,
|
||||
selectedAuthType: '',
|
||||
tier: undefined,
|
||||
userEmail: 'mock@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,10 +48,8 @@ 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, '');
|
||||
@@ -74,7 +58,6 @@ describe('statsCommand', () => {
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
quotas: mockQuota,
|
||||
tier: 'Basic',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -90,9 +73,6 @@ describe('statsCommand', () => {
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.MODEL_STATS,
|
||||
selectedAuthType: '',
|
||||
tier: undefined,
|
||||
userEmail: 'mock@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,33 +4,15 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
HistoryItemStats,
|
||||
HistoryItemModelStats,
|
||||
HistoryItemToolStats,
|
||||
} from '../types.js';
|
||||
import type { HistoryItemStats } 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;
|
||||
@@ -43,14 +25,9 @@ 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) {
|
||||
@@ -88,13 +65,9 @@ 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);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -105,7 +78,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 { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { render } 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 } = renderWithProviders(<AboutBox {...defaultProps} />);
|
||||
const { lastFrame } = render(<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 } = renderWithProviders(<AboutBox {...props} />);
|
||||
const { lastFrame } = render(<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 } = renderWithProviders(<AboutBox {...props} />);
|
||||
const { lastFrame } = render(<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 } = renderWithProviders(<AboutBox {...props} />);
|
||||
const { lastFrame } = render(<AboutBox {...props} />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('api-key');
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ 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;
|
||||
@@ -32,130 +31,123 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
ideClient,
|
||||
userEmail,
|
||||
tier,
|
||||
}) => {
|
||||
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
|
||||
}) => (
|
||||
<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
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={theme.text.link}>
|
||||
CLI Version
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||
</Box>
|
||||
{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}>
|
||||
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>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
{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}>
|
||||
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 && (
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -36,3 +36,42 @@ export const tinyAsciiLogo = `
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
`;
|
||||
|
||||
export const shortAsciiLogoIde = `
|
||||
░░░░░░░░░ ░░░░░░░░░░ ░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░
|
||||
░░░ ░░░ ░░░ ░░░░░░ ░░░░░░ ░░░ ░░░░░░ ░░░░░ ░░░
|
||||
░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░
|
||||
█████████░░██████████ ██████ ░░██████░█████░██████ ░░█████ █████░
|
||||
███░░ ███░███░░ ██████ ░██████░░███░░██████ ░█████ ███░░
|
||||
███░░ ░░███░░ ███░███ ███ ███░░███░░███░███ ███░░ ███░░
|
||||
███░░░░████░██████░░░░░███░░█████ ███░░███░░███░░███ ███░░░ ███░░░
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ██████ ███
|
||||
███ ███ ███ ███ ███ ███ ███ █████ ███
|
||||
█████████ ██████████ ███ ███ █████ ███ █████ █████
|
||||
`;
|
||||
|
||||
export const longAsciiLogoIde = `
|
||||
░░░ ░░░░░░░░░ ░░░░░░░░░░ ░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░
|
||||
░░░ ░░░ ░░░ ░░░ ░░░░░░ ░░░░░░ ░░░ ░░░░░░ ░░░░░ ░░░
|
||||
░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░ ░░░
|
||||
███ ░░░ █████████░░██████████ ██████ ░░██████░█████░██████ ░░█████ █████░
|
||||
███ ░░░ ███░ ███░███░░ ██████ ░██████░░███░░██████ ░█████ ███░░
|
||||
███ ███░░░ ░░███░░ ███░███ ███ ███░░███░░███░███ ███░░ ███░░
|
||||
░░░ ███ ███ ░░░█████░██████░░░░░███░░█████ ███░░███░░███░░███ ███░░░ ███░░░
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ██████ ███
|
||||
███ ███ ███ ███ ███ ███ ███ ███ █████ ███
|
||||
███ █████████ ██████████ ███ ███ █████ ███ █████ █████
|
||||
`;
|
||||
|
||||
export const tinyAsciiLogoIde = `
|
||||
░░░ ░░░░░░░░░
|
||||
░░░ ░░░ ░░░
|
||||
░░░ ░░░
|
||||
███ ░░░ █████████░░░
|
||||
███ ░░░ ███░░ ███░░
|
||||
███ ███░░ ░░░
|
||||
░░░ ███ ███░░░░████░
|
||||
███ ███ ███
|
||||
███ ███ ███
|
||||
███ █████████
|
||||
`;
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Checklist } from './Checklist.js';
|
||||
import type { ChecklistItemData } from './ChecklistItem.js';
|
||||
|
||||
describe('<Checklist />', () => {
|
||||
const items: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'in_progress', label: 'Task 2' },
|
||||
{ status: 'pending', label: 'Task 3' },
|
||||
{ status: 'cancelled', label: 'Task 4' },
|
||||
];
|
||||
|
||||
it('renders nothing when list is empty', () => {
|
||||
const { lastFrame } = render(
|
||||
<Checklist title="Test List" items={[]} isExpanded={true} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders nothing when collapsed and no active items', () => {
|
||||
const inactiveItems: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'cancelled', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<Checklist title="Test List" items={inactiveItems} isExpanded={false} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
|
||||
it('renders summary view correctly (collapsed)', () => {
|
||||
const { lastFrame } = render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
isExpanded={false}
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expanded view correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
isExpanded={true}
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders summary view without in-progress item if none exists', () => {
|
||||
const pendingItems: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'pending', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<Checklist title="Test List" items={pendingItems} isExpanded={false} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useMemo } from 'react';
|
||||
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||
|
||||
export interface ChecklistProps {
|
||||
title: string;
|
||||
items: ChecklistItemData[];
|
||||
isExpanded: boolean;
|
||||
toggleHint?: string;
|
||||
}
|
||||
|
||||
const ChecklistTitleDisplay: React.FC<{
|
||||
title: string;
|
||||
items: ChecklistItemData[];
|
||||
toggleHint?: string;
|
||||
}> = ({ title, items, toggleHint }) => {
|
||||
const score = useMemo(() => {
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
for (const item of items) {
|
||||
if (item.status !== 'cancelled') {
|
||||
total += 1;
|
||||
if (item.status === 'completed') {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${completed}/${total} completed`;
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={2} height={1}>
|
||||
<Text color={theme.text.primary} bold aria-label={`${title} list`}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{score}
|
||||
{toggleHint ? ` (${toggleHint})` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ChecklistListDisplay: React.FC<{ items: ChecklistItemData[] }> = ({
|
||||
items,
|
||||
}) => (
|
||||
<Box flexDirection="column" aria-role="list">
|
||||
{items.map((item, index) => (
|
||||
<ChecklistItem
|
||||
item={item}
|
||||
key={`${index}-${item.label}`}
|
||||
role="listitem"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Checklist: React.FC<ChecklistProps> = ({
|
||||
title,
|
||||
items,
|
||||
isExpanded,
|
||||
toggleHint,
|
||||
}) => {
|
||||
const inProgress: ChecklistItemData | null = useMemo(
|
||||
() => items.find((item) => item.status === 'in_progress') || null,
|
||||
[items],
|
||||
);
|
||||
|
||||
const hasActiveItems = useMemo(
|
||||
() =>
|
||||
items.some(
|
||||
(item) => item.status === 'pending' || item.status === 'in_progress',
|
||||
),
|
||||
[items],
|
||||
);
|
||||
|
||||
if (items.length === 0 || (!isExpanded && !hasActiveItems)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderBottom={false}
|
||||
borderRight={false}
|
||||
borderLeft={false}
|
||||
borderColor={theme.border.default}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<Box flexDirection="column" rowGap={1}>
|
||||
<ChecklistTitleDisplay
|
||||
title={title}
|
||||
items={items}
|
||||
toggleHint={toggleHint}
|
||||
/>
|
||||
<ChecklistListDisplay items={items} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="row" columnGap={1} height={1}>
|
||||
<Box flexShrink={0} flexGrow={0}>
|
||||
<ChecklistTitleDisplay
|
||||
title={title}
|
||||
items={items}
|
||||
toggleHint={toggleHint}
|
||||
/>
|
||||
</Box>
|
||||
{inProgress && (
|
||||
<Box flexShrink={1} flexGrow={1}>
|
||||
<ChecklistItem item={inProgress} wrap="truncate" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||
import { Box } from 'ink';
|
||||
|
||||
describe('<ChecklistItem />', () => {
|
||||
it.each([
|
||||
{ status: 'pending', label: 'Do this' },
|
||||
{ status: 'in_progress', label: 'Doing this' },
|
||||
{ status: 'completed', label: 'Done this' },
|
||||
{ status: 'cancelled', label: 'Skipped this' },
|
||||
] as ChecklistItemData[])('renders %s item correctly', (item) => {
|
||||
const { lastFrame } = render(<ChecklistItem item={item} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('truncates long text when wrap="truncate"', () => {
|
||||
const item: ChecklistItemData = {
|
||||
status: 'in_progress',
|
||||
label:
|
||||
'This is a very long text that should be truncated because the wrap prop is set to truncate',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} wrap="truncate" />
|
||||
</Box>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('wraps long text by default', () => {
|
||||
const item: ChecklistItemData = {
|
||||
status: 'in_progress',
|
||||
label:
|
||||
'This is a very long text that should wrap because the default behavior is wrapping',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} />
|
||||
</Box>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { checkExhaustive } from '../../utils/checks.js';
|
||||
|
||||
export type ChecklistStatus =
|
||||
| 'pending'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'cancelled';
|
||||
|
||||
export interface ChecklistItemData {
|
||||
status: ChecklistStatus;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const ChecklistStatusDisplay: React.FC<{ status: ChecklistStatus }> = ({
|
||||
status,
|
||||
}) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return (
|
||||
<Text color={theme.status.success} aria-label="Completed">
|
||||
✓
|
||||
</Text>
|
||||
);
|
||||
case 'in_progress':
|
||||
return (
|
||||
<Text color={theme.text.accent} aria-label="In Progress">
|
||||
»
|
||||
</Text>
|
||||
);
|
||||
case 'pending':
|
||||
return (
|
||||
<Text color={theme.text.secondary} aria-label="Pending">
|
||||
☐
|
||||
</Text>
|
||||
);
|
||||
case 'cancelled':
|
||||
return (
|
||||
<Text color={theme.status.error} aria-label="Cancelled">
|
||||
✗
|
||||
</Text>
|
||||
);
|
||||
default:
|
||||
checkExhaustive(status);
|
||||
}
|
||||
};
|
||||
|
||||
export interface ChecklistItemProps {
|
||||
item: ChecklistItemData;
|
||||
wrap?: 'truncate';
|
||||
role?: 'listitem';
|
||||
}
|
||||
|
||||
export const ChecklistItem: React.FC<ChecklistItemProps> = ({
|
||||
item,
|
||||
wrap,
|
||||
role: ariaRole,
|
||||
}) => {
|
||||
const textColor = (() => {
|
||||
switch (item.status) {
|
||||
case 'in_progress':
|
||||
return theme.text.accent;
|
||||
case 'completed':
|
||||
case 'cancelled':
|
||||
return theme.text.secondary;
|
||||
case 'pending':
|
||||
return theme.text.primary;
|
||||
default:
|
||||
checkExhaustive(item.status);
|
||||
}
|
||||
})();
|
||||
const strikethrough = item.status === 'cancelled';
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={1} aria-role={ariaRole}>
|
||||
<ChecklistStatusDisplay status={item.status} />
|
||||
<Box flexShrink={1}>
|
||||
<Text color={textColor} wrap={wrap} strikethrough={strikethrough}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -19,7 +19,7 @@ import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
vi.mock('../contexts/VimModeContext.js', () => ({
|
||||
useVimMode: vi.fn(() => ({
|
||||
vimEnabled: false,
|
||||
vimMode: 'INSERT',
|
||||
vimMode: 'NORMAL',
|
||||
})),
|
||||
}));
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
@@ -54,9 +54,7 @@ vi.mock('./DetailedMessagesDisplay.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./InputPrompt.js', () => ({
|
||||
InputPrompt: ({ placeholder }: { placeholder?: string }) => (
|
||||
<Text>InputPrompt: {placeholder}</Text>
|
||||
),
|
||||
InputPrompt: () => <Text>InputPrompt</Text>,
|
||||
calculatePromptWidths: vi.fn(() => ({
|
||||
inputWidth: 80,
|
||||
suggestionsWidth: 40,
|
||||
@@ -489,40 +487,4 @@ describe('Composer', () => {
|
||||
expect(lastFrame()).not.toContain('DetailedMessagesDisplay');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vim Mode Placeholders', () => {
|
||||
it('shows correct placeholder in INSERT mode', async () => {
|
||||
const uiState = createMockUIState({ isInputActive: true });
|
||||
const { useVimMode } = await import('../contexts/VimModeContext.js');
|
||||
vi.mocked(useVimMode).mockReturnValue({
|
||||
vimEnabled: true,
|
||||
vimMode: 'INSERT',
|
||||
toggleVimEnabled: vi.fn(),
|
||||
setVimMode: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
"InputPrompt: Press 'Esc' for NORMAL mode.",
|
||||
);
|
||||
});
|
||||
|
||||
it('shows correct placeholder in NORMAL mode', async () => {
|
||||
const uiState = createMockUIState({ isInputActive: true });
|
||||
const { useVimMode } = await import('../contexts/VimModeContext.js');
|
||||
vi.mocked(useVimMode).mockReturnValue({
|
||||
vimEnabled: true,
|
||||
vimMode: 'NORMAL',
|
||||
toggleVimEnabled: vi.fn(),
|
||||
setVimMode: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
"InputPrompt: Press 'i' for INSERT mode.",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
const uiState = useUIState();
|
||||
const uiActions = useUIActions();
|
||||
const { vimEnabled, vimMode } = useVimMode();
|
||||
const { vimEnabled } = useVimMode();
|
||||
const terminalWidth = process.stdout.columns;
|
||||
const isNarrow = isNarrowWidth(terminalWidth);
|
||||
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
|
||||
@@ -143,9 +143,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
popAllMessages={uiActions.popAllMessages}
|
||||
placeholder={
|
||||
vimEnabled
|
||||
? vimMode === 'INSERT'
|
||||
? " Press 'Esc' for NORMAL mode."
|
||||
: " Press 'i' for INSERT mode."
|
||||
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
|
||||
: uiState.shellModeActive
|
||||
? ' Type your shell command'
|
||||
: ' Type your message or @path/to/file'
|
||||
|
||||
@@ -8,8 +8,9 @@ import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { Header } from './Header.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
import { longAsciiLogo } from './AsciiArt.js';
|
||||
import { longAsciiLogo, longAsciiLogoIde } from './AsciiArt.js';
|
||||
import * as semanticColors from '../semantic-colors.js';
|
||||
import * as terminalSetup from '../utils/terminalSetup.js';
|
||||
import { Text } from 'ink';
|
||||
import type React from 'react';
|
||||
|
||||
@@ -17,6 +18,9 @@ vi.mock('../hooks/useTerminalSize.js');
|
||||
vi.mock('../hooks/useSnowfall.js', () => ({
|
||||
useSnowfall: vi.fn((art) => art),
|
||||
}));
|
||||
vi.mock('../utils/terminalSetup.js', () => ({
|
||||
getTerminalProgram: vi.fn(),
|
||||
}));
|
||||
vi.mock('ink-gradient', () => {
|
||||
const MockGradient = ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
@@ -37,6 +41,7 @@ vi.mock('ink', async () => {
|
||||
describe('<Header />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('renders the long logo on a wide terminal', () => {
|
||||
@@ -53,6 +58,22 @@ describe('<Header />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the IDE logo when running in an IDE', () => {
|
||||
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue('vscode');
|
||||
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: longAsciiLogoIde,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders custom ASCII art when provided', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
render(
|
||||
@@ -66,13 +87,24 @@ describe('<Header />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders custom ASCII art as is when running in an IDE', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
vi.mocked(terminalSetup.getTerminalProgram).mockReturnValue('vscode');
|
||||
render(
|
||||
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: customArt,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the version number when nightly is true', () => {
|
||||
render(<Header version="1.0.0" nightly={true} />);
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
const versionText = Array.isArray(textCalls[1][0].children)
|
||||
? textCalls[1][0].children.join('')
|
||||
: textCalls[1][0].children;
|
||||
expect(versionText).toBe('v1.0.0');
|
||||
expect(textCalls[1][0].children.join('')).toBe('v1.0.0');
|
||||
});
|
||||
|
||||
it('does not display the version number when nightly is false', () => {
|
||||
|
||||
@@ -7,9 +7,17 @@
|
||||
import type React from 'react';
|
||||
import { Box } from 'ink';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { shortAsciiLogo, longAsciiLogo, tinyAsciiLogo } from './AsciiArt.js';
|
||||
import {
|
||||
shortAsciiLogo,
|
||||
longAsciiLogo,
|
||||
tinyAsciiLogo,
|
||||
shortAsciiLogoIde,
|
||||
longAsciiLogoIde,
|
||||
tinyAsciiLogoIde,
|
||||
} from './AsciiArt.js';
|
||||
import { getAsciiArtWidth } from '../utils/textUtils.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { getTerminalProgram } from '../utils/terminalSetup.js';
|
||||
import { useSnowfall } from '../hooks/useSnowfall.js';
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -24,6 +32,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
nightly,
|
||||
}) => {
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
const isIde = getTerminalProgram();
|
||||
let displayTitle;
|
||||
const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo);
|
||||
const widthOfShortLogo = getAsciiArtWidth(shortAsciiLogo);
|
||||
@@ -31,11 +40,11 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
if (customAsciiArt) {
|
||||
displayTitle = customAsciiArt;
|
||||
} else if (terminalWidth >= widthOfLongLogo) {
|
||||
displayTitle = longAsciiLogo;
|
||||
displayTitle = isIde ? longAsciiLogoIde : longAsciiLogo;
|
||||
} else if (terminalWidth >= widthOfShortLogo) {
|
||||
displayTitle = shortAsciiLogo;
|
||||
displayTitle = isIde ? shortAsciiLogoIde : shortAsciiLogo;
|
||||
} else {
|
||||
displayTitle = tinyAsciiLogo;
|
||||
displayTitle = isIde ? tinyAsciiLogoIde : tinyAsciiLogo;
|
||||
}
|
||||
|
||||
const artWidth = getAsciiArtWidth(displayTitle);
|
||||
|
||||
@@ -122,18 +122,9 @@ 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,8 +8,6 @@ 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';
|
||||
|
||||
@@ -22,16 +20,7 @@ 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({
|
||||
@@ -47,14 +36,6 @@ const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
useSettingsMock.mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
showUserIdentity: true,
|
||||
},
|
||||
},
|
||||
} as unknown as LoadedSettings);
|
||||
|
||||
return render(<ModelStatsDisplay />, width);
|
||||
};
|
||||
|
||||
@@ -387,74 +368,4 @@ 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,7 +15,6 @@ 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;
|
||||
@@ -25,21 +24,9 @@ interface StatRowData {
|
||||
[key: string]: string | React.ReactNode | boolean | undefined;
|
||||
}
|
||||
|
||||
interface ModelStatsDisplayProps {
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
tier?: string;
|
||||
}
|
||||
|
||||
export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
}) => {
|
||||
export const ModelStatsDisplay: React.FC = () => {
|
||||
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,
|
||||
);
|
||||
@@ -88,12 +75,10 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
return row;
|
||||
};
|
||||
|
||||
const rows: StatRowData[] = [];
|
||||
|
||||
// API Section
|
||||
rows.push({ metric: 'API', isSection: true });
|
||||
rows.push(createRow('Requests', (m) => m.api.totalRequests.toLocaleString()));
|
||||
rows.push(
|
||||
const rows: StatRowData[] = [
|
||||
// API Section
|
||||
{ metric: 'API', isSection: true },
|
||||
createRow('Requests', (m) => m.api.totalRequests.toLocaleString()),
|
||||
createRow('Errors', (m) => {
|
||||
const errorRate = calculateErrorRate(m);
|
||||
return (
|
||||
@@ -106,24 +91,18 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
</Text>
|
||||
);
|
||||
}),
|
||||
);
|
||||
rows.push(
|
||||
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
|
||||
);
|
||||
|
||||
// Spacer
|
||||
rows.push({ metric: '' });
|
||||
// Spacer
|
||||
{ metric: '' },
|
||||
|
||||
// Tokens Section
|
||||
rows.push({ metric: 'Tokens', isSection: true });
|
||||
rows.push(
|
||||
// Tokens Section
|
||||
{ metric: 'Tokens', isSection: true },
|
||||
createRow('Total', (m) => (
|
||||
<Text color={theme.text.secondary}>
|
||||
{m.tokens.total.toLocaleString()}
|
||||
</Text>
|
||||
)),
|
||||
);
|
||||
rows.push(
|
||||
createRow(
|
||||
'Input',
|
||||
(m) => (
|
||||
@@ -133,7 +112,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
),
|
||||
{ isSubtle: true },
|
||||
),
|
||||
);
|
||||
];
|
||||
|
||||
if (hasCached) {
|
||||
rows.push(
|
||||
@@ -235,31 +214,6 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
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 { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { render } 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,9 +35,7 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
return renderWithProviders(<SessionSummaryDisplay duration="1h 23m 45s" />, {
|
||||
width: 100,
|
||||
});
|
||||
return render(<SessionSummaryDisplay duration="1h 23m 45s" />);
|
||||
};
|
||||
|
||||
describe('<SessionSummaryDisplay />', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { render } 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 renderWithProviders(<StatsDisplay duration="1s" />, { width: 100 });
|
||||
return render(<StatsDisplay duration="1s" />);
|
||||
};
|
||||
|
||||
// Helper to create metrics with default zero values
|
||||
@@ -381,9 +381,8 @@ describe('<StatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame } = render(
|
||||
<StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Agent powering down. Goodbye!');
|
||||
@@ -440,9 +439,8 @@ describe('<StatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame } = render(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -486,9 +484,8 @@ describe('<StatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame } = render(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -501,64 +498,4 @@ 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,7 +25,6 @@ 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 {
|
||||
@@ -365,25 +364,17 @@ 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,
|
||||
@@ -426,22 +417,6 @@ 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,6 +109,15 @@ 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(
|
||||
@@ -116,9 +125,8 @@ export function ThemeDialog({
|
||||
);
|
||||
|
||||
// Generate theme items
|
||||
const themeItems = themeManager
|
||||
.getAvailableThemes()
|
||||
.map((theme) => {
|
||||
const themeItems = [
|
||||
...builtInThemes.map((theme) => {
|
||||
const fullTheme = themeManager.getTheme(theme.name);
|
||||
const themeBackground = fullTheme
|
||||
? resolveColor(fullTheme.colors.Background)
|
||||
@@ -132,14 +140,28 @@ export function ThemeDialog({
|
||||
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);
|
||||
});
|
||||
}),
|
||||
...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);
|
||||
});
|
||||
|
||||
// Find the index of the selected theme, but only if it exists in the list
|
||||
const initialThemeIndex = themeItems.findIndex(
|
||||
@@ -292,20 +314,9 @@ 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}>
|
||||
{themeNamePart}{' '}
|
||||
{item.themeNameDisplay}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{item.themeTypeDisplay}
|
||||
</Text>
|
||||
|
||||
@@ -137,56 +137,6 @@ 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,23 +26,17 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const {
|
||||
mainAreaWidth,
|
||||
terminalHeight,
|
||||
constrainHeight,
|
||||
availableTerminalHeight: uiAvailableHeight,
|
||||
} = useUIState();
|
||||
const { mainAreaWidth, terminalHeight, constrainHeight } = useUIState();
|
||||
const { tool, index, total } = confirmingTool;
|
||||
|
||||
// Safety check: ToolConfirmationMessage requires confirmationDetails
|
||||
if (!tool.confirmationDetails) return null;
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
// ToolConfirmationMessage needs to know the height available for its OWN content.
|
||||
// We subtract the lines used by the Queue wrapper:
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<Checklist /> > renders expanded view correctly 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Test List 1/3 completed (toggle me)
|
||||
|
||||
✓ Task 1
|
||||
» Task 2
|
||||
☐ Task 3
|
||||
✗ Task 4"
|
||||
`;
|
||||
|
||||
exports[`<Checklist /> > renders summary view correctly (collapsed) 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Test List 1/3 completed (toggle me) » Task 2"
|
||||
`;
|
||||
|
||||
exports[`<Checklist /> > renders summary view without in-progress item if none exists 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Test List 1/2 completed"
|
||||
`;
|
||||
@@ -1,17 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ChecklistItem /> > renders { status: 'cancelled', label: 'Skipped this' } item correctly 1`] = `"✗ Skipped this"`;
|
||||
|
||||
exports[`<ChecklistItem /> > renders { status: 'completed', label: 'Done this' } item correctly 1`] = `"✓ Done this"`;
|
||||
|
||||
exports[`<ChecklistItem /> > renders { status: 'in_progress', label: 'Doing this' } item correctly 1`] = `"» Doing this"`;
|
||||
|
||||
exports[`<ChecklistItem /> > renders { status: 'pending', label: 'Do this' } item correctly 1`] = `"☐ Do this"`;
|
||||
|
||||
exports[`<ChecklistItem /> > truncates long text when wrap="truncate" 1`] = `"» This is a very long text th…"`;
|
||||
|
||||
exports[`<ChecklistItem /> > wraps long text by default 1`] = `
|
||||
"» This is a very long text
|
||||
that should wrap because the
|
||||
default behavior is wrapping"
|
||||
`;
|
||||
@@ -413,3 +413,371 @@ 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,25 +1,5 @@
|
||||
// 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 │
|
||||
|
||||
@@ -5,12 +5,101 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { type TodoList } from '@google/gemini-cli-core';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
type Todo,
|
||||
type TodoList,
|
||||
type TodoStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useMemo } from 'react';
|
||||
import type { HistoryItemToolGroup } from '../../types.js';
|
||||
import { Checklist } from '../Checklist.js';
|
||||
import type { ChecklistItemData } from '../ChecklistItem.js';
|
||||
|
||||
const TodoTitleDisplay: React.FC<{ todos: TodoList }> = ({ todos }) => {
|
||||
const score = useMemo(() => {
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
for (const todo of todos.todos) {
|
||||
if (todo.status !== 'cancelled') {
|
||||
total += 1;
|
||||
if (todo.status === 'completed') {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${completed}/${total} completed`;
|
||||
}, [todos]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={2} height={1}>
|
||||
<Text color={theme.text.primary} bold aria-label="Todo list">
|
||||
Todo
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>{score} (ctrl+t to toggle)</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return (
|
||||
<Text color={theme.status.success} aria-label="Completed">
|
||||
✓
|
||||
</Text>
|
||||
);
|
||||
case 'in_progress':
|
||||
return (
|
||||
<Text color={theme.text.accent} aria-label="In Progress">
|
||||
»
|
||||
</Text>
|
||||
);
|
||||
case 'pending':
|
||||
return (
|
||||
<Text color={theme.text.secondary} aria-label="Pending">
|
||||
☐
|
||||
</Text>
|
||||
);
|
||||
case 'cancelled':
|
||||
default:
|
||||
return (
|
||||
<Text color={theme.status.error} aria-label="Cancelled">
|
||||
✗
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const TodoItemDisplay: React.FC<{
|
||||
todo: Todo;
|
||||
wrap?: 'truncate';
|
||||
role?: 'listitem';
|
||||
}> = ({ todo, wrap, role: ariaRole }) => {
|
||||
const textColor = (() => {
|
||||
switch (todo.status) {
|
||||
case 'in_progress':
|
||||
return theme.text.accent;
|
||||
case 'completed':
|
||||
case 'cancelled':
|
||||
return theme.text.secondary;
|
||||
default:
|
||||
return theme.text.primary;
|
||||
}
|
||||
})();
|
||||
const strikethrough = todo.status === 'cancelled';
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={1} aria-role={ariaRole}>
|
||||
<TodoStatusDisplay status={todo.status} />
|
||||
<Box flexShrink={1}>
|
||||
<Text color={textColor} wrap={wrap} strikethrough={strikethrough}>
|
||||
{todo.description}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const TodoTray: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
@@ -36,26 +125,68 @@ export const TodoTray: React.FC = () => {
|
||||
return null;
|
||||
}, [uiState.history]);
|
||||
|
||||
const checklistItems: ChecklistItemData[] = useMemo(() => {
|
||||
if (!todos || !todos.todos) {
|
||||
return [];
|
||||
const inProgress: Todo | null = useMemo(() => {
|
||||
if (todos === null) {
|
||||
return null;
|
||||
}
|
||||
return todos.todos.map((todo) => ({
|
||||
status: todo.status,
|
||||
label: todo.description,
|
||||
}));
|
||||
return todos.todos.find((todo) => todo.status === 'in_progress') || null;
|
||||
}, [todos]);
|
||||
|
||||
if (!todos || !todos.todos) {
|
||||
const hasActiveTodos = useMemo(() => {
|
||||
if (!todos || !todos.todos) return false;
|
||||
return todos.todos.some(
|
||||
(todo) => todo.status === 'pending' || todo.status === 'in_progress',
|
||||
);
|
||||
}, [todos]);
|
||||
|
||||
if (
|
||||
todos === null ||
|
||||
!todos.todos ||
|
||||
todos.todos.length === 0 ||
|
||||
(!uiState.showFullTodos && !hasActiveTodos)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Checklist
|
||||
title="Todo"
|
||||
items={checklistItems}
|
||||
isExpanded={uiState.showFullTodos}
|
||||
toggleHint="ctrl+t to toggle"
|
||||
/>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderBottom={false}
|
||||
borderRight={false}
|
||||
borderLeft={false}
|
||||
borderColor={theme.border.default}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
{uiState.showFullTodos ? (
|
||||
<Box flexDirection="column" rowGap={1}>
|
||||
<TodoTitleDisplay todos={todos} />
|
||||
<TodoListDisplay todos={todos} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="row" columnGap={1} height={1}>
|
||||
<Box flexShrink={0} flexGrow={0}>
|
||||
<TodoTitleDisplay todos={todos} />
|
||||
</Box>
|
||||
{inProgress && (
|
||||
<Box flexShrink={1} flexGrow={1}>
|
||||
<TodoItemDisplay todo={inProgress} wrap="truncate" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface TodoListDisplayProps {
|
||||
todos: TodoList;
|
||||
}
|
||||
|
||||
const TodoListDisplay: React.FC<TodoListDisplayProps> = ({ todos }) => (
|
||||
<Box flexDirection="column" aria-role="list">
|
||||
{todos.todos.map((todo: Todo, index: number) => (
|
||||
<TodoItemDisplay todo={todo} key={index} role="listitem" />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -8,19 +8,14 @@ import {
|
||||
renderWithProviders,
|
||||
createMockSettings,
|
||||
} from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi } 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 { ASK_USER_DISPLAY_NAME, makeFakeConfig } from '@google/gemini-cli-core';
|
||||
import os from 'node:os';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
describe('<ToolGroupMessage />', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const createToolCall = (
|
||||
overrides: Partial<IndividualToolCallDisplay> = {},
|
||||
): IndividualToolCallDisplay => ({
|
||||
@@ -40,16 +35,16 @@ describe('<ToolGroupMessage />', () => {
|
||||
isFocused: true,
|
||||
};
|
||||
|
||||
const baseMockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
debugMode: false,
|
||||
folderTrust: false,
|
||||
ideMode: false,
|
||||
enableInteractiveShell: true,
|
||||
previewFeatures: false,
|
||||
enableEventDrivenScheduler: true,
|
||||
});
|
||||
const baseMockConfig = {
|
||||
getModel: () => 'gemini-pro',
|
||||
getTargetDir: () => '/test',
|
||||
getDebugMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getEnableInteractiveShell: () => true,
|
||||
getPreviewFeatures: () => false,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', () => {
|
||||
@@ -88,11 +83,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Error,
|
||||
}),
|
||||
];
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -122,11 +116,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -184,11 +177,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Pending,
|
||||
}),
|
||||
];
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -389,11 +381,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
describe('Border Color Logic', () => {
|
||||
it('uses yellow border when tools are pending', () => {
|
||||
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -512,11 +503,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -549,11 +539,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
const settings = createMockSettings({
|
||||
security: { enablePermanentToolApproval: true },
|
||||
});
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -585,11 +574,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = makeFakeConfig({
|
||||
model: 'gemini-pro',
|
||||
targetDir: os.tmpdir(),
|
||||
enableEventDrivenScheduler: false,
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -616,7 +604,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = baseMockConfig;
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -649,7 +640,10 @@ describe('<ToolGroupMessage />', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = baseMockConfig;
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
@@ -663,98 +657,5 @@ 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,11 +116,13 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
[toolCalls, isEventDriven],
|
||||
);
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,5 @@
|
||||
// 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 │
|
||||
@@ -110,8 +80,6 @@ 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 │
|
||||
|
||||
@@ -6,21 +6,11 @@
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { HalfLinePaddedBox } from './HalfLinePaddedBox.js';
|
||||
import { Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import { Text } from 'ink';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { isITerm2 } from '../../utils/terminalUtils.js';
|
||||
|
||||
vi.mock('ink', async () => {
|
||||
const actual = await vi.importActual('ink');
|
||||
return {
|
||||
...actual,
|
||||
useIsScreenReaderEnabled: vi.fn(() => false),
|
||||
};
|
||||
});
|
||||
|
||||
describe('<HalfLinePaddedBox />', () => {
|
||||
const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled);
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -71,19 +61,4 @@ describe('<HalfLinePaddedBox />', () => {
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders nothing when screen reader is enabled', async () => {
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<HalfLinePaddedBox backgroundBaseColor="blue" backgroundOpacity={0.5}>
|
||||
<Text>Content</Text>
|
||||
</HalfLinePaddedBox>,
|
||||
{ width: 10 },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import {
|
||||
interpolateColor,
|
||||
@@ -39,8 +39,7 @@ export interface HalfLinePaddedBoxProps {
|
||||
* at the top and bottom using block characters (▀/▄).
|
||||
*/
|
||||
export const HalfLinePaddedBox: React.FC<HalfLinePaddedBoxProps> = (props) => {
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
if (props.useBackgroundColor === false || isScreenReaderEnabled) {
|
||||
if (props.useBackgroundColor === false) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ Content
|
||||
▀▀▀▀▀▀▀▀▀▀"
|
||||
`;
|
||||
|
||||
exports[`<HalfLinePaddedBox /> > renders nothing when screen reader is enabled 1`] = `"Content"`;
|
||||
|
||||
exports[`<HalfLinePaddedBox /> > renders nothing when useBackgroundColor is false 1`] = `"Content"`;
|
||||
|
||||
exports[`<HalfLinePaddedBox /> > renders standard background and blocks when not iTerm2 1`] = `
|
||||
|
||||
@@ -1418,7 +1418,7 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'h',
|
||||
shift: false,
|
||||
@@ -1427,9 +1427,9 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: 'h',
|
||||
});
|
||||
});
|
||||
void act(() =>
|
||||
}),
|
||||
);
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'i',
|
||||
shift: false,
|
||||
@@ -1447,7 +1447,7 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'return',
|
||||
shift: false,
|
||||
@@ -1456,8 +1456,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: '\r',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).lines).toEqual(['', '']);
|
||||
});
|
||||
|
||||
@@ -1465,7 +1465,7 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'j',
|
||||
shift: false,
|
||||
@@ -1474,8 +1474,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\n',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).lines).toEqual(['', '']);
|
||||
});
|
||||
|
||||
@@ -1483,7 +1483,7 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'tab',
|
||||
shift: false,
|
||||
@@ -1492,8 +1492,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\t',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('');
|
||||
});
|
||||
|
||||
@@ -1501,7 +1501,7 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'tab',
|
||||
shift: true,
|
||||
@@ -1510,8 +1510,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\u001b[9;2u',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('');
|
||||
});
|
||||
|
||||
@@ -1524,7 +1524,7 @@ describe('useTextBuffer', () => {
|
||||
}),
|
||||
);
|
||||
act(() => result.current.move('end'));
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'backspace',
|
||||
shift: false,
|
||||
@@ -1533,8 +1533,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\x7f',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('');
|
||||
});
|
||||
|
||||
@@ -1627,7 +1627,7 @@ describe('useTextBuffer', () => {
|
||||
}),
|
||||
);
|
||||
act(() => result.current.move('end')); // cursor [0,2]
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'left',
|
||||
shift: false,
|
||||
@@ -1636,10 +1636,10 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\x1b[D',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).cursor).toEqual([0, 1]);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'right',
|
||||
shift: false,
|
||||
@@ -1648,8 +1648,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\x1b[C',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).cursor).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
@@ -1659,7 +1659,7 @@ describe('useTextBuffer', () => {
|
||||
);
|
||||
const textWithAnsi = '\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m';
|
||||
// Simulate pasting by calling handleInput with a string longer than 1 char
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: '',
|
||||
shift: false,
|
||||
@@ -1668,8 +1668,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: textWithAnsi,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Hello World');
|
||||
});
|
||||
|
||||
@@ -1677,7 +1677,7 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'return',
|
||||
shift: true,
|
||||
@@ -1686,8 +1686,8 @@ describe('useTextBuffer', () => {
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: '\r',
|
||||
});
|
||||
}); // Simulates Shift+Enter in VSCode terminal
|
||||
}),
|
||||
); // Simulates Shift+Enter in VSCode terminal
|
||||
expect(getBufferState(result).lines).toEqual(['', '']);
|
||||
});
|
||||
|
||||
@@ -1927,9 +1927,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => {
|
||||
result.current.handleInput(createInput(input));
|
||||
});
|
||||
act(() => result.current.handleInput(createInput(input)));
|
||||
expect(getBufferState(result).text).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -1938,9 +1936,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const validText = 'Hello World\nThis is a test.';
|
||||
act(() => {
|
||||
result.current.handleInput(createInput(validText));
|
||||
});
|
||||
act(() => result.current.handleInput(createInput(validText)));
|
||||
expect(getBufferState(result).text).toBe(validText);
|
||||
});
|
||||
|
||||
@@ -1954,7 +1950,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
|
||||
expect(largeTextWithUnsafe.length).toBeGreaterThan(5000);
|
||||
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: '',
|
||||
shift: false,
|
||||
@@ -1963,8 +1959,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: largeTextWithUnsafe,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const resultText = getBufferState(result).text;
|
||||
expect(resultText).not.toContain('\x07');
|
||||
@@ -1989,7 +1985,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
|
||||
expect(largeTextWithAnsi.length).toBeGreaterThan(5000);
|
||||
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: '',
|
||||
shift: false,
|
||||
@@ -1998,8 +1994,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: largeTextWithAnsi,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const resultText = getBufferState(result).text;
|
||||
expect(resultText).not.toContain('\x1B[31m');
|
||||
@@ -2014,7 +2010,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const emojis = '🐍🐳🦀🦄';
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: '',
|
||||
shift: false,
|
||||
@@ -2023,8 +2019,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: emojis,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe(emojis);
|
||||
});
|
||||
});
|
||||
@@ -2206,7 +2202,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
singleLine: true,
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'return',
|
||||
shift: false,
|
||||
@@ -2215,8 +2211,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: '\r',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).lines).toEqual(['']);
|
||||
});
|
||||
|
||||
@@ -2228,7 +2224,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
singleLine: true,
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'f1',
|
||||
shift: false,
|
||||
@@ -2237,8 +2233,8 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\u001bOP',
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).lines).toEqual(['']);
|
||||
});
|
||||
|
||||
|
||||
@@ -3419,7 +3419,7 @@ export interface TextBuffer {
|
||||
/**
|
||||
* High level "handleInput" – receives what Ink gives us.
|
||||
*/
|
||||
handleInput: (key: Key) => boolean;
|
||||
handleInput: (key: Key) => void;
|
||||
/**
|
||||
* Opens the current buffer contents in the user's preferred terminal text
|
||||
* editor ($VISUAL or $EDITOR, falling back to "vi"). The method blocks
|
||||
|
||||
@@ -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="wrap">
|
||||
<Text key={i} wrap="truncate-end">
|
||||
{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="wrap">
|
||||
<Text key={i} italic wrap="truncate-end">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
@@ -949,7 +949,7 @@ Return a JSON object with:
|
||||
? 'blue'
|
||||
: undefined
|
||||
}
|
||||
wrap="wrap"
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{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="wrap">
|
||||
<Text color="gray" wrap="truncate-end">
|
||||
Reactions: {getReactionCount(c)} | {c.reason}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -997,14 +997,14 @@ Return a JSON object with:
|
||||
<Text bold color="blue">
|
||||
Analysis:{' '}
|
||||
</Text>
|
||||
<Text wrap="wrap"> {state.message}</Text>
|
||||
<Text wrap="truncate-end"> {state.message}</Text>
|
||||
</Box>
|
||||
{state.suggestedComment && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold color="gray">
|
||||
Suggested Comment:
|
||||
</Text>
|
||||
<Text italic color="gray" wrap="wrap">
|
||||
<Text italic color="gray" wrap="truncate-end">
|
||||
"{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 = 3000;
|
||||
export const WARNING_PROMPT_DURATION_MS = 1000;
|
||||
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 Cmd+Z or Alt+Z…',
|
||||
'Redo undone text edits with Shift+Cmd+Z or Shift+Alt+Z…',
|
||||
'Undo text edits in the input with Ctrl+Z…',
|
||||
'Redo undone text edits with Ctrl+Shift+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,8 +24,9 @@ interface AskUserActionsContextValue {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export const AskUserActionsContext =
|
||||
createContext<AskUserActionsContextValue | null>(null);
|
||||
const AskUserActionsContext = createContext<AskUserActionsContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export const useAskUserActions = () => {
|
||||
const context = useContext(AskUserActionsContext);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user