Compare commits

...

7 Commits

Author SHA1 Message Date
jacob314 81a1e127ed foo
foo

checkpoint
2026-03-05 15:48:42 -08:00
Jacob Richman 12957ea16a fix(cli): Polish shell autocomplete rendering to be a little more shell native feeling. (#20931) 2026-03-04 06:52:56 +00:00
Jacob Richman 1017b78157 Defensive coding to reduce the risk of Maximum update depth errors (#20940) 2026-03-04 06:32:05 +00:00
Tommaso Sciortino 7e06559db2 Consistently guard restarts against concurrent auto updates (#21016) 2026-03-04 06:18:12 +00:00
gemini-cli-robot bbcfff5cf1 chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 (#21054) 2026-03-04 05:06:31 +00:00
Sri Pasumarthi 5bd21a240d fix: model persistence for all scenarios (#21051) 2026-03-04 03:28:12 +00:00
gemini-cli-robot fdfd626f28 Changelog for v0.33.0-preview.0 (#21030)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-04 02:51:50 +00:00
213 changed files with 1517 additions and 1064 deletions
+4
View File
@@ -73,6 +73,10 @@ powerful tool for developers.
and `packages/core` (Backend logic).
- **Imports:** Use specific imports and avoid restricted relative imports
between packages (enforced by ESLint).
- **Linting:** Never suppress the `@typescript-eslint/no-unnecessary-condition`
warning (e.g., via `eslint-disable`). This rule ensures that optional chaining
and truthiness checks are only used when truly necessary according to the type
system, keeping the code clean and predictable.
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
include the Apache-2.0 license header with the current year. (e.g.,
`Copyright 2026 Google LLC`). This is enforced by ESLint.
+163 -189
View File
@@ -1,6 +1,6 @@
# Preview release: v0.32.0-preview.0
# Preview release: v0.33.0-preview.0
Released: February 27, 2026
Released: March 03, 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,196 +13,170 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including
support for modifying plans in external editors, adaptive workflows based on
task complexity, and new integration tests.
- **Agent and Core Engine Updates**: Enabled the generalist agent, introduced
`Kind.Agent` for sub-agent classification, implemented task tracking
foundation, and improved Agent-to-Agent (A2A) streaming and content
extraction.
- **CLI & User Experience**: Introduced interactive shell autocompletion, added
a new verbosity mode for cleaner error reporting, enabled parallel loading of
extensions, and improved UI hints and shortcut handling.
- **Billing and Security**: Implemented G1 AI credits overage flow with enhanced
billing telemetry, updated the authentication handshake to specification, and
added support for a policy engine in extensions.
- **Stability and Bug Fixes**: Addressed numerous issues including 100% CPU
consumption by orphaned processes, enhanced retry logic for Code Assist,
reduced intrusive MCP errors, and merged duplicate imports across packages.
- **Plan Mode Enhancements**: Added support for annotating plans with feedback
for iteration, enabling built-in research subagents in plan mode, and a new
`copy` subcommand.
- **Agent and Skill Improvements**: Introduced the new `github-issue-creator`
skill, implemented HTTP authentication support for A2A remote agents, and
added support for authenticated A2A agent card discovery.
- **CLI UX/UI Updates**: Redesigned the header to be compact with an ASCII icon,
inverted the context window display to show usage, and directly indicate auth
required state for agents.
- **Core and ACP Enhancements**: Implemented slash command handling in ACP (for
`/memory`, `/init`, `/extensions`, and `/restore`), added a set models
interface to ACP, and centralized `read_file` limits while truncating large
MCP tool output.
## What's Changed
- feat(plan): add integration tests for plan mode by @Adib234 in
[#20214](https://github.com/google-gemini/gemini-cli/pull/20214)
- fix(acp): update auth handshake to spec by @skeshive in
[#19725](https://github.com/google-gemini/gemini-cli/pull/19725)
- feat(core): implement robust A2A streaming reassembly and fix task continuity
by @adamfweidman in
[#20091](https://github.com/google-gemini/gemini-cli/pull/20091)
- feat(cli): load extensions in parallel by @scidomino in
[#20229](https://github.com/google-gemini/gemini-cli/pull/20229)
- Plumb the maxAttempts setting through Config args by @kevinjwang1 in
[#20239](https://github.com/google-gemini/gemini-cli/pull/20239)
- fix(cli): skip 404 errors in setup-github file downloads by @h30s in
[#20287](https://github.com/google-gemini/gemini-cli/pull/20287)
- fix(cli): expose model.name setting in settings dialog for persistence by
@achaljhawar in
[#19605](https://github.com/google-gemini/gemini-cli/pull/19605)
- docs: remove legacy cmd examples in favor of powershell by @scidomino in
[#20323](https://github.com/google-gemini/gemini-cli/pull/20323)
- feat(core): Enable model steering in workspace. by @joshualitt in
[#20343](https://github.com/google-gemini/gemini-cli/pull/20343)
- fix: remove trailing comma in issue triage workflow settings json by @Nixxx19
in [#20265](https://github.com/google-gemini/gemini-cli/pull/20265)
- feat(core): implement task tracker foundation and service by @anj-s in
[#19464](https://github.com/google-gemini/gemini-cli/pull/19464)
- test: support tests that include color information by @jacob314 in
[#20220](https://github.com/google-gemini/gemini-cli/pull/20220)
- feat(core): introduce Kind.Agent for sub-agent classification by @abhipatel12
in [#20369](https://github.com/google-gemini/gemini-cli/pull/20369)
- Changelog for v0.30.0 by @gemini-cli-robot in
[#20252](https://github.com/google-gemini/gemini-cli/pull/20252)
- Update changelog workflow to reject nightly builds by @g-samroberts in
[#20248](https://github.com/google-gemini/gemini-cli/pull/20248)
- Changelog for v0.31.0-preview.0 by @gemini-cli-robot in
[#20249](https://github.com/google-gemini/gemini-cli/pull/20249)
- feat(cli): hide workspace policy update dialog and auto-accept by default by
@Abhijit-2592 in
[#20351](https://github.com/google-gemini/gemini-cli/pull/20351)
- feat(core): rename grep_search include parameter to include_pattern by
- Docs: Update model docs to remove Preview Features. by @jkcinouye in
[#20084](https://github.com/google-gemini/gemini-cli/pull/20084)
- docs: fix typo in installation documentation by @AdityaSharma-Git3207 in
[#20153](https://github.com/google-gemini/gemini-cli/pull/20153)
- docs: add Windows PowerShell equivalents for environments and scripting by
@scidomino in [#20333](https://github.com/google-gemini/gemini-cli/pull/20333)
- fix(core): parse raw ASCII buffer strings in Gaxios errors by @sehoon38 in
[#20626](https://github.com/google-gemini/gemini-cli/pull/20626)
- chore(release): bump version to 0.33.0-nightly.20260227.ba149afa0 by @galz10
in [#20637](https://github.com/google-gemini/gemini-cli/pull/20637)
- fix(github): use robot PAT for automated PRs to pass CLA check by @galz10 in
[#20641](https://github.com/google-gemini/gemini-cli/pull/20641)
- chore/release: bump version to 0.33.0-nightly.20260228.1ca5c05d0 by
@gemini-cli-robot in
[#20644](https://github.com/google-gemini/gemini-cli/pull/20644)
- Changelog for v0.31.0 by @gemini-cli-robot in
[#20634](https://github.com/google-gemini/gemini-cli/pull/20634)
- fix: use full paths for ACP diff payloads by @JagjeevanAK in
[#19539](https://github.com/google-gemini/gemini-cli/pull/19539)
- Changelog for v0.32.0-preview.0 by @gemini-cli-robot in
[#20627](https://github.com/google-gemini/gemini-cli/pull/20627)
- fix: acp/zed race condition between MCP initialisation and prompt by
@kartikangiras in
[#20205](https://github.com/google-gemini/gemini-cli/pull/20205)
- fix(cli): reset themeManager between tests to ensure isolation by
@NTaylorMullen in
[#20598](https://github.com/google-gemini/gemini-cli/pull/20598)
- refactor(core): Extract tool parameter names as constants by @SandyTao520 in
[#20460](https://github.com/google-gemini/gemini-cli/pull/20460)
- fix(cli): resolve autoThemeSwitching when background hasn't changed but theme
mismatches by @sehoon38 in
[#20706](https://github.com/google-gemini/gemini-cli/pull/20706)
- feat(skills): add github-issue-creator skill by @sehoon38 in
[#20709](https://github.com/google-gemini/gemini-cli/pull/20709)
- fix(cli): allow sub-agent confirmation requests in UI while preventing
background flicker by @abhipatel12 in
[#20722](https://github.com/google-gemini/gemini-cli/pull/20722)
- Merge User and Agent Card Descriptions #20849 by @adamfweidman in
[#20850](https://github.com/google-gemini/gemini-cli/pull/20850)
- fix(core): reduce LLM-based loop detection false positives by @SandyTao520 in
[#20701](https://github.com/google-gemini/gemini-cli/pull/20701)
- fix(plan): deflake plan mode integration tests by @Adib234 in
[#20477](https://github.com/google-gemini/gemini-cli/pull/20477)
- Add /unassign support by @scidomino in
[#20864](https://github.com/google-gemini/gemini-cli/pull/20864)
- feat(core): implement HTTP authentication support for A2A remote agents by
@SandyTao520 in
[#20328](https://github.com/google-gemini/gemini-cli/pull/20328)
- feat(plan): support opening and modifying plan in external editor by @Adib234
in [#20348](https://github.com/google-gemini/gemini-cli/pull/20348)
- feat(cli): implement interactive shell autocompletion by @mrpmohiburrahman in
[#20082](https://github.com/google-gemini/gemini-cli/pull/20082)
- fix(core): allow /memory add to work in plan mode by @Jefftree in
[#20353](https://github.com/google-gemini/gemini-cli/pull/20353)
- feat(core): add HTTP 499 to retryable errors and map to RetryableQuotaError by
@bdmorgan in [#20432](https://github.com/google-gemini/gemini-cli/pull/20432)
- feat(core): Enable generalist agent by @joshualitt in
[#19665](https://github.com/google-gemini/gemini-cli/pull/19665)
- Updated tests in TableRenderer.test.tsx to use SVG snapshots by @devr0306 in
[#20450](https://github.com/google-gemini/gemini-cli/pull/20450)
- Refactor Github Action per b/485167538 by @google-admin in
[#19443](https://github.com/google-gemini/gemini-cli/pull/19443)
- fix(github): resolve actionlint and yamllint regressions from #19443 by @jerop
in [#20467](https://github.com/google-gemini/gemini-cli/pull/20467)
- fix: action var usage by @galz10 in
[#20492](https://github.com/google-gemini/gemini-cli/pull/20492)
- feat(core): improve A2A content extraction by @adamfweidman in
[#20487](https://github.com/google-gemini/gemini-cli/pull/20487)
- fix(cli): support quota error fallbacks for all authentication types by
@sehoon38 in [#20475](https://github.com/google-gemini/gemini-cli/pull/20475)
- fix(core): flush transcript for pure tool-call responses to ensure BeforeTool
hooks see complete state by @krishdef7 in
[#20419](https://github.com/google-gemini/gemini-cli/pull/20419)
- feat(plan): adapt planning workflow based on complexity of task by @jerop in
[#20465](https://github.com/google-gemini/gemini-cli/pull/20465)
- fix: prevent orphaned processes from consuming 100% CPU when terminal closes
by @yuvrajangadsingh in
[#16965](https://github.com/google-gemini/gemini-cli/pull/16965)
- feat(core): increase fetch timeout and fix [object Object] error
stringification by @bdmorgan in
[#20441](https://github.com/google-gemini/gemini-cli/pull/20441)
- [Gemma x Gemini CLI] Add an Experimental Gemma Router that uses a LiteRT-LM
shim into the Composite Model Classifier Strategy by @sidwan02 in
[#17231](https://github.com/google-gemini/gemini-cli/pull/17231)
- docs(plan): update documentation regarding supporting editing of plan files
during plan approval by @Adib234 in
[#20452](https://github.com/google-gemini/gemini-cli/pull/20452)
- test(cli): fix flaky ToolResultDisplay overflow test by @jwhelangoog in
[#20518](https://github.com/google-gemini/gemini-cli/pull/20518)
- ui(cli): reduce length of Ctrl+O hint by @jwhelangoog in
[#20490](https://github.com/google-gemini/gemini-cli/pull/20490)
- fix(ui): correct styled table width calculations by @devr0306 in
[#20042](https://github.com/google-gemini/gemini-cli/pull/20042)
- Avoid overaggressive unescaping by @scidomino in
[#20520](https://github.com/google-gemini/gemini-cli/pull/20520)
- feat(telemetry) Instrument traces with more attributes and make them available
to OTEL users by @heaventourist in
[#20237](https://github.com/google-gemini/gemini-cli/pull/20237)
- Add support for policy engine in extensions by @chrstnb in
[#20049](https://github.com/google-gemini/gemini-cli/pull/20049)
- Docs: Update to Terms of Service & FAQ by @jkcinouye in
[#20488](https://github.com/google-gemini/gemini-cli/pull/20488)
- Fix bottom border rendering for search and add a regression test. by @jacob314
in [#20517](https://github.com/google-gemini/gemini-cli/pull/20517)
- fix(core): apply retry logic to CodeAssistServer for all users by @bdmorgan in
[#20507](https://github.com/google-gemini/gemini-cli/pull/20507)
- Fix extension MCP server env var loading by @chrstnb in
[#20374](https://github.com/google-gemini/gemini-cli/pull/20374)
- feat(ui): add 'ctrl+o' hint to truncated content message by @jerop in
[#20529](https://github.com/google-gemini/gemini-cli/pull/20529)
- Fix flicker showing message to press ctrl-O again to collapse. by @jacob314 in
[#20414](https://github.com/google-gemini/gemini-cli/pull/20414)
- fix(cli): hide shortcuts hint while model is thinking or the user has typed a
prompt + add debounce to avoid flicker by @jacob314 in
[#19389](https://github.com/google-gemini/gemini-cli/pull/19389)
- feat(plan): update planning workflow to encourage multi-select with
descriptions of options by @Adib234 in
[#20491](https://github.com/google-gemini/gemini-cli/pull/20491)
- refactor(core,cli): useAlternateBuffer read from config by @psinha40898 in
[#20346](https://github.com/google-gemini/gemini-cli/pull/20346)
- fix(cli): ensure dialogs stay scrolled to bottom in alternate buffer mode by
@jacob314 in [#20527](https://github.com/google-gemini/gemini-cli/pull/20527)
- fix(core): revert auto-save of policies to user space by @Abhijit-2592 in
[#20531](https://github.com/google-gemini/gemini-cli/pull/20531)
- Demote unreliable test. by @gundermanc in
[#20571](https://github.com/google-gemini/gemini-cli/pull/20571)
- fix(core): handle optional response fields from code assist API by @sehoon38
in [#20345](https://github.com/google-gemini/gemini-cli/pull/20345)
- fix(cli): keep thought summary when loading phrases are off by @LyalinDotCom
in [#20497](https://github.com/google-gemini/gemini-cli/pull/20497)
- feat(cli): add temporary flag to disable workspace policies by @Abhijit-2592
in [#20523](https://github.com/google-gemini/gemini-cli/pull/20523)
- Disable expensive and scheduled workflows on personal forks by @dewitt in
[#20449](https://github.com/google-gemini/gemini-cli/pull/20449)
- Moved markdown parsing logic to a separate util file by @devr0306 in
[#20526](https://github.com/google-gemini/gemini-cli/pull/20526)
- fix(plan): prevent agent from using ask_user for shell command confirmation by
@Adib234 in [#20504](https://github.com/google-gemini/gemini-cli/pull/20504)
- fix(core): disable retries for code assist streaming requests by @sehoon38 in
[#20561](https://github.com/google-gemini/gemini-cli/pull/20561)
- feat(billing): implement G1 AI credits overage flow with billing telemetry by
@gsquared94 in
[#18590](https://github.com/google-gemini/gemini-cli/pull/18590)
- feat: better error messages by @gsquared94 in
[#20577](https://github.com/google-gemini/gemini-cli/pull/20577)
- fix(ui): persist expansion in AskUser dialog when navigating options by @jerop
in [#20559](https://github.com/google-gemini/gemini-cli/pull/20559)
- fix(cli): prevent sub-agent tool calls from leaking into UI by @abhipatel12 in
[#20580](https://github.com/google-gemini/gemini-cli/pull/20580)
- fix(cli): Shell autocomplete polish by @jacob314 in
[#20411](https://github.com/google-gemini/gemini-cli/pull/20411)
- Changelog for v0.31.0-preview.1 by @gemini-cli-robot in
[#20590](https://github.com/google-gemini/gemini-cli/pull/20590)
- Add slash command for promoting behavioral evals to CI blocking by @gundermanc
in [#20575](https://github.com/google-gemini/gemini-cli/pull/20575)
- Changelog for v0.30.1 by @gemini-cli-robot in
[#20589](https://github.com/google-gemini/gemini-cli/pull/20589)
- Add low/full CLI error verbosity mode for cleaner UI by @LyalinDotCom in
[#20399](https://github.com/google-gemini/gemini-cli/pull/20399)
- Disable Gemini PR reviews on draft PRs. by @gundermanc in
[#20362](https://github.com/google-gemini/gemini-cli/pull/20362)
- Docs: FAQ update by @jkcinouye in
[#20585](https://github.com/google-gemini/gemini-cli/pull/20585)
- fix(core): reduce intrusive MCP errors and deduplicate diagnostics by
@spencer426 in
[#20232](https://github.com/google-gemini/gemini-cli/pull/20232)
- docs: fix spelling typos in installation guide by @campox747 in
[#20579](https://github.com/google-gemini/gemini-cli/pull/20579)
- Promote stable tests to CI blocking. by @gundermanc in
[#20581](https://github.com/google-gemini/gemini-cli/pull/20581)
- feat(core): enable contiguous parallel admission for Kind.Agent tools by
@abhipatel12 in
[#20583](https://github.com/google-gemini/gemini-cli/pull/20583)
- Enforce import/no-duplicates as error by @Nixxx19 in
[#19797](https://github.com/google-gemini/gemini-cli/pull/19797)
- fix: merge duplicate imports in sdk and test-utils packages (1/4) by @Nixxx19
in [#19777](https://github.com/google-gemini/gemini-cli/pull/19777)
- fix: merge duplicate imports in a2a-server package (2/4) by @Nixxx19 in
[#19781](https://github.com/google-gemini/gemini-cli/pull/19781)
[#20510](https://github.com/google-gemini/gemini-cli/pull/20510)
- feat(core): centralize read_file limits and update gemini-3 description by
@aishaneeshah in
[#20619](https://github.com/google-gemini/gemini-cli/pull/20619)
- Do not block CI on evals by @gundermanc in
[#20870](https://github.com/google-gemini/gemini-cli/pull/20870)
- document node limitation for shift+tab by @scidomino in
[#20877](https://github.com/google-gemini/gemini-cli/pull/20877)
- Add install as an option when extension is selected. by @DavidAPierce in
[#20358](https://github.com/google-gemini/gemini-cli/pull/20358)
- Update CODEOWNERS for README.md reviewers by @g-samroberts in
[#20860](https://github.com/google-gemini/gemini-cli/pull/20860)
- feat(core): truncate large MCP tool output by @SandyTao520 in
[#19365](https://github.com/google-gemini/gemini-cli/pull/19365)
- Subagent activity UX. by @gundermanc in
[#17570](https://github.com/google-gemini/gemini-cli/pull/17570)
- style(cli) : Dialog pattern for /hooks Command by @AbdulTawabJuly in
[#17930](https://github.com/google-gemini/gemini-cli/pull/17930)
- feat: redesign header to be compact with ASCII icon by @keithguerin in
[#18713](https://github.com/google-gemini/gemini-cli/pull/18713)
- fix(core): ensure subagents use qualified MCP tool names by @abhipatel12 in
[#20801](https://github.com/google-gemini/gemini-cli/pull/20801)
- feat(core): support authenticated A2A agent card discovery by @SandyTao520 in
[#20622](https://github.com/google-gemini/gemini-cli/pull/20622)
- refactor(cli): fully remove React anti patterns, improve type safety and fix
UX oversights in SettingsDialog.tsx by @psinha40898 in
[#18963](https://github.com/google-gemini/gemini-cli/pull/18963)
- Adding MCPOAuthProvider implementing the MCPSDK OAuthClientProvider by
@Nayana-Parameswarappa in
[#20121](https://github.com/google-gemini/gemini-cli/pull/20121)
- feat(core): add tool name validation in TOML policy files by @allenhutchison
in [#19281](https://github.com/google-gemini/gemini-cli/pull/19281)
- docs: fix broken markdown links in main README.md by @Hamdanbinhashim in
[#20300](https://github.com/google-gemini/gemini-cli/pull/20300)
- refactor(core): replace manual syncPlanModeTools with declarative policy rules
by @jerop in [#20596](https://github.com/google-gemini/gemini-cli/pull/20596)
- fix(core): increase default headers timeout to 5 minutes by @gundermanc in
[#20890](https://github.com/google-gemini/gemini-cli/pull/20890)
- feat(admin): enable 30 day default retention for chat history & remove warning
by @skeshive in
[#20853](https://github.com/google-gemini/gemini-cli/pull/20853)
- feat(plan): support annotating plans with feedback for iteration by @Adib234
in [#20876](https://github.com/google-gemini/gemini-cli/pull/20876)
- Add some dos and don'ts to behavioral evals README. by @gundermanc in
[#20629](https://github.com/google-gemini/gemini-cli/pull/20629)
- fix(core): skip telemetry logging for AbortError exceptions by @yunaseoul in
[#19477](https://github.com/google-gemini/gemini-cli/pull/19477)
- fix(core): restrict "System: Please continue" invalid stream retry to Gemini 2
models by @SandyTao520 in
[#20897](https://github.com/google-gemini/gemini-cli/pull/20897)
- ci(evals): only run evals in CI if prompts or tools changed by @gundermanc in
[#20898](https://github.com/google-gemini/gemini-cli/pull/20898)
- Build binary by @aswinashok44 in
[#18933](https://github.com/google-gemini/gemini-cli/pull/18933)
- Code review fixes as a pr by @jacob314 in
[#20612](https://github.com/google-gemini/gemini-cli/pull/20612)
- fix(ci): handle empty APP_ID in stale PR closer by @bdmorgan in
[#20919](https://github.com/google-gemini/gemini-cli/pull/20919)
- feat(cli): invert context window display to show usage by @keithguerin in
[#20071](https://github.com/google-gemini/gemini-cli/pull/20071)
- fix(plan): clean up session directories and plans on deletion by @jerop in
[#20914](https://github.com/google-gemini/gemini-cli/pull/20914)
- fix(core): enforce optionality for API response fields in code_assist by
@sehoon38 in [#20714](https://github.com/google-gemini/gemini-cli/pull/20714)
- feat(extensions): add support for plan directory in extension manifest by
@mahimashanware in
[#20354](https://github.com/google-gemini/gemini-cli/pull/20354)
- feat(plan): enable built-in research subagents in plan mode by @Adib234 in
[#20972](https://github.com/google-gemini/gemini-cli/pull/20972)
- feat(agents): directly indicate auth required state by @adamfweidman in
[#20986](https://github.com/google-gemini/gemini-cli/pull/20986)
- fix(cli): wait for background auto-update before relaunching by @scidomino in
[#20904](https://github.com/google-gemini/gemini-cli/pull/20904)
- fix: pre-load @scripts/copy_files.js references from external editor prompts
by @kartikangiras in
[#20963](https://github.com/google-gemini/gemini-cli/pull/20963)
- feat(evals): add behavioral evals for ask_user tool by @Adib234 in
[#20620](https://github.com/google-gemini/gemini-cli/pull/20620)
- refactor common settings logic for skills,agents by @ishaanxgupta in
[#17490](https://github.com/google-gemini/gemini-cli/pull/17490)
- Update docs-writer skill with new resource by @g-samroberts in
[#20917](https://github.com/google-gemini/gemini-cli/pull/20917)
- fix(cli): pin clipboardy to ~5.2.x by @scidomino in
[#21009](https://github.com/google-gemini/gemini-cli/pull/21009)
- feat: Implement slash command handling in ACP for
`/memory`,`/init`,`/extensions` and `/restore` by @sripasg in
[#20528](https://github.com/google-gemini/gemini-cli/pull/20528)
- Docs/add hooks reference by @AadithyaAle in
[#20961](https://github.com/google-gemini/gemini-cli/pull/20961)
- feat(plan): add copy subcommand to plan (#20491) by @ruomengz in
[#20988](https://github.com/google-gemini/gemini-cli/pull/20988)
- fix(core): sanitize and length-check MCP tool qualified names by @abhipatel12
in [#20987](https://github.com/google-gemini/gemini-cli/pull/20987)
- Format the quota/limit style guide. by @g-samroberts in
[#21017](https://github.com/google-gemini/gemini-cli/pull/21017)
- fix(core): send shell output to model on cancel by @devr0306 in
[#20501](https://github.com/google-gemini/gemini-cli/pull/20501)
- remove hardcoded tiername when missing tier by @sehoon38 in
[#21022](https://github.com/google-gemini/gemini-cli/pull/21022)
- feat(acp): add set models interface by @skeshive in
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.31.0-preview.3...v0.32.0-preview.0
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.0
+208
View File
@@ -202,6 +202,7 @@ export default tseslint.config(
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
},
},
{
@@ -377,6 +378,213 @@ export default tseslint.config(
},
// Prettier config must be last
prettierConfig,
{
// Legacy files with many @typescript-eslint/no-unnecessary-condition issues
files: [
'packages/a2a-server/src/agent/executor.ts',
'packages/a2a-server/src/agent/task.ts',
'packages/a2a-server/src/config/settings.ts',
'packages/a2a-server/src/http/app.ts',
'packages/cli/src/commands/extensions/configure.ts',
'packages/cli/src/config/config.ts',
'packages/cli/src/config/extension-manager.ts',
'packages/cli/src/config/extensions/extensionEnablement.ts',
'packages/cli/src/config/extensions/github.ts',
'packages/cli/src/config/mcp/mcpServerEnablement.ts',
'packages/cli/src/config/settings-validation.ts',
'packages/cli/src/config/settings.ts',
'packages/cli/src/config/trustedFolders.ts',
'packages/cli/src/nonInteractiveCli.ts',
'packages/cli/src/services/McpPromptLoader.ts',
'packages/cli/src/test-utils/AppRig.tsx',
'packages/cli/src/test-utils/mockConfig.ts',
'packages/cli/src/test-utils/render.tsx',
'packages/cli/src/ui/AppContainer.tsx',
'packages/cli/src/ui/commands/agentsCommand.ts',
'packages/cli/src/ui/commands/chatCommand.ts',
'packages/cli/src/ui/commands/directoryCommand.tsx',
'packages/cli/src/ui/commands/hooksCommand.ts',
'packages/cli/src/ui/commands/mcpCommand.ts',
'packages/cli/src/ui/commands/restoreCommand.ts',
'packages/cli/src/ui/commands/rewindCommand.tsx',
'packages/cli/src/ui/commands/setupGithubCommand.ts',
'packages/cli/src/ui/commands/skillsCommand.ts',
'packages/cli/src/ui/commands/statsCommand.ts',
'packages/cli/src/ui/components/AskUserDialog.tsx',
'packages/cli/src/ui/components/ColorsDisplay.tsx',
'packages/cli/src/ui/components/Composer.tsx',
'packages/cli/src/ui/components/ContextUsageDisplay.tsx',
'packages/cli/src/ui/components/DetailedMessagesDisplay.tsx',
'packages/cli/src/ui/components/DialogManager.tsx',
'packages/cli/src/ui/components/ExitPlanModeDialog.tsx',
'packages/cli/src/ui/components/FolderTrustDialog.tsx',
'packages/cli/src/ui/components/HooksDialog.tsx',
'packages/cli/src/ui/components/IdeTrustChangeDialog.tsx',
'packages/cli/src/ui/components/ModelStatsDisplay.tsx',
'packages/cli/src/ui/components/MultiFolderTrustDialog.tsx',
'packages/cli/src/ui/components/Notifications.tsx',
'packages/cli/src/ui/components/QuotaDisplay.tsx',
'packages/cli/src/ui/components/RewindViewer.tsx',
'packages/cli/src/ui/components/SessionBrowser.tsx',
'packages/cli/src/ui/components/SettingsDialog.tsx',
'packages/cli/src/ui/components/ShowMoreLines.tsx',
'packages/cli/src/ui/components/StatsDisplay.tsx',
'packages/cli/src/ui/components/ThemeDialog.tsx',
'packages/cli/src/ui/components/UserIdentity.tsx',
'packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx',
'packages/cli/src/ui/components/messages/Todo.tsx',
'packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx',
'packages/cli/src/ui/components/messages/ToolGroupMessage.tsx',
'packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx',
'packages/cli/src/ui/components/shared/EnumSelector.tsx',
'packages/cli/src/ui/components/shared/MaxSizedBox.tsx',
'packages/cli/src/ui/components/shared/Scrollable.tsx',
'packages/cli/src/ui/components/shared/VirtualizedList.tsx',
'packages/cli/src/ui/components/shared/text-buffer.ts',
'packages/cli/src/ui/components/triage/TriageDuplicates.tsx',
'packages/cli/src/ui/components/triage/TriageIssues.tsx',
'packages/cli/src/ui/components/views/McpStatus.tsx',
'packages/cli/src/ui/contexts/KeypressContext.tsx',
'packages/cli/src/ui/contexts/ScrollProvider.tsx',
'packages/cli/src/ui/contexts/SessionContext.tsx',
'packages/cli/src/ui/hooks/slashCommandProcessor.ts',
'packages/cli/src/ui/hooks/useAtCompletion.ts',
'packages/cli/src/ui/hooks/useCommandCompletion.tsx',
'packages/cli/src/ui/hooks/useConsoleMessages.ts',
'packages/cli/src/ui/hooks/useExtensionUpdates.ts',
'packages/cli/src/ui/hooks/useGeminiStream.ts',
'packages/cli/src/ui/hooks/useIncludeDirsTrust.tsx',
'packages/cli/src/ui/hooks/useInputHistory.ts',
'packages/cli/src/ui/hooks/useInputHistoryStore.ts',
'packages/cli/src/ui/hooks/usePermissionsModifyTrust.ts',
'packages/cli/src/ui/hooks/usePromptCompletion.ts',
'packages/cli/src/ui/hooks/useQuotaAndFallback.ts',
'packages/cli/src/ui/hooks/useSelectionList.ts',
'packages/cli/src/ui/hooks/useShellCompletion.ts',
'packages/cli/src/ui/hooks/useSlashCompletion.ts',
'packages/cli/src/ui/hooks/useThemeCommand.ts',
'packages/cli/src/ui/hooks/useToolScheduler.ts',
'packages/cli/src/ui/hooks/vim.ts',
'packages/cli/src/ui/themes/theme-manager.ts',
'packages/cli/src/ui/utils/CodeColorizer.tsx',
'packages/cli/src/ui/utils/MarkdownDisplay.tsx',
'packages/cli/src/ui/utils/borderStyles.ts',
'packages/cli/src/ui/utils/clipboardUtils.ts',
'packages/cli/src/ui/utils/highlight.ts',
'packages/cli/src/ui/utils/inlineThinkingMode.ts',
'packages/cli/src/ui/utils/keybindingUtils.ts',
'packages/cli/src/ui/utils/terminalCapabilityManager.ts',
'packages/cli/src/ui/utils/terminalSetup.ts',
'packages/cli/src/ui/utils/terminalUtils.ts',
'packages/cli/src/utils/activityLogger.ts',
'packages/cli/src/utils/commentJson.ts',
'packages/cli/src/utils/deepMerge.ts',
'packages/cli/src/utils/devtoolsService.ts',
'packages/cli/src/utils/envVarResolver.ts',
'packages/cli/src/utils/sandbox.ts',
'packages/cli/src/utils/sessionUtils.ts',
'packages/cli/src/utils/settingsUtils.ts',
'packages/cli/src/zed-integration/zedIntegration.ts',
'packages/core/src/agents/a2a-client-manager.ts',
'packages/core/src/agents/a2aUtils.ts',
'packages/core/src/agents/acknowledgedAgents.ts',
'packages/core/src/agents/browser/browserManager.ts',
'packages/core/src/agents/browser/mcpToolWrapper.ts',
'packages/core/src/agents/local-executor.ts',
'packages/core/src/agents/local-invocation.ts',
'packages/core/src/agents/registry.ts',
'packages/core/src/agents/subagent-tool.ts',
'packages/core/src/availability/modelAvailabilityService.ts',
'packages/core/src/availability/policyHelpers.ts',
'packages/core/src/billing/billing.ts',
'packages/core/src/code_assist/admin/mcpUtils.ts',
'packages/core/src/code_assist/converter.ts',
'packages/core/src/code_assist/oauth2.ts',
'packages/core/src/code_assist/server.ts',
'packages/core/src/code_assist/setup.ts',
'packages/core/src/code_assist/telemetry.ts',
'packages/core/src/commands/memory.ts',
'packages/core/src/config/config.ts',
'packages/core/src/confirmation-bus/message-bus.ts',
'packages/core/src/core/baseLlmClient.ts',
'packages/core/src/core/contentGenerator.ts',
'packages/core/src/core/coreToolHookTriggers.ts',
'packages/core/src/core/fakeContentGenerator.ts',
'packages/core/src/core/geminiChat.ts',
'packages/core/src/core/logger.ts',
'packages/core/src/core/loggingContentGenerator.ts',
'packages/core/src/core/turn.ts',
'packages/core/src/hooks/hookRegistry.ts',
'packages/core/src/hooks/hookRunner.ts',
'packages/core/src/hooks/trustedHooks.ts',
'packages/core/src/hooks/types.ts',
'packages/core/src/ide/ide-client.ts',
'packages/core/src/ide/ide-connection-utils.ts',
'packages/core/src/ide/process-utils.ts',
'packages/core/src/mcp/oauth-provider.ts',
'packages/core/src/mcp/token-storage/base-token-storage.ts',
'packages/core/src/policy/config.ts',
'packages/core/src/prompts/mcp-prompts.ts',
'packages/core/src/prompts/promptProvider.ts',
'packages/core/src/routing/strategies/classifierStrategy.ts',
'packages/core/src/routing/strategies/defaultStrategy.ts',
'packages/core/src/routing/strategies/fallbackStrategy.ts',
'packages/core/src/routing/strategies/numericalClassifierStrategy.ts',
'packages/core/src/routing/strategies/overrideStrategy.ts',
'packages/core/src/safety/checker-runner.ts',
'packages/core/src/safety/conseca/conseca.ts',
'packages/core/src/safety/conseca/policy-enforcer.ts',
'packages/core/src/safety/conseca/policy-generator.ts',
'packages/core/src/safety/context-builder.ts',
'packages/core/src/scheduler/confirmation.ts',
'packages/core/src/scheduler/scheduler.ts',
'packages/core/src/scheduler/state-manager.ts',
'packages/core/src/scheduler/tool-executor.ts',
'packages/core/src/services/environmentSanitization.ts',
'packages/core/src/services/modelConfigService.ts',
'packages/core/src/services/sessionSummaryUtils.ts',
'packages/core/src/services/shellExecutionService.ts',
'packages/core/src/services/toolOutputMaskingService.ts',
'packages/core/src/skills/skillLoader.ts',
'packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts',
'packages/core/src/telemetry/startupProfiler.ts',
'packages/core/src/telemetry/telemetryAttributes.ts',
'packages/core/src/telemetry/types.ts',
'packages/core/src/telemetry/uiTelemetry.ts',
'packages/core/src/tools/grep-utils.ts',
'packages/core/src/tools/mcp-client.ts',
'packages/core/src/tools/shell.ts',
'packages/core/src/utils/bfsFileSearch.ts',
'packages/core/src/utils/editCorrector.ts',
'packages/core/src/utils/errors.ts',
'packages/core/src/utils/fileDiffUtils.ts',
'packages/core/src/utils/fileUtils.ts',
'packages/core/src/utils/filesearch/crawler.ts',
'packages/core/src/utils/filesearch/fileSearch.ts',
'packages/core/src/utils/filesearch/result-cache.ts',
'packages/core/src/utils/headless.ts',
'packages/core/src/utils/ignoreFileParser.ts',
'packages/core/src/utils/ignorePatterns.ts',
'packages/core/src/utils/llm-edit-fixer.ts',
'packages/core/src/utils/nextSpeakerChecker.ts',
'packages/core/src/utils/partUtils.ts',
'packages/core/src/utils/retry.ts',
'packages/core/src/utils/safeJsonStringify.ts',
'packages/core/src/utils/sessionUtils.ts',
'packages/core/src/utils/shell-utils.ts',
'packages/sdk/src/session.ts',
],
rules: {
'@typescript-eslint/no-unnecessary-condition': 'off',
},
},
{
// Legacy files with many @typescript-eslint/no-unnecessary-type-assertion issues
files: ['packages/core/src/core/client.ts', 'packages/core/src/core/geminiChat.ts'],
rules: {
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
},
},
// extra settings for scripts that we run directly with node
{
files: ['./integration-tests/**/*.js'],
+9 -9
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"workspaces": [
"packages/*"
],
@@ -17303,7 +17303,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
@@ -17361,7 +17361,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -17444,7 +17444,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
@@ -17709,7 +17709,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -17724,7 +17724,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17741,7 +17741,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17758,7 +17758,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"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.34.0-nightly.20260303.34f0c1538"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260304.28af4e127"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -793,7 +793,7 @@ export class Task {
) {
errorEvent = event;
}
const errorMessage = errorEvent?.value?.error
const errorMessage = errorEvent?.value.error
? getErrorMessage(errorEvent.value.error)
: 'Unknown error from LLM stream';
logger.error(
@@ -802,7 +802,7 @@ export class Task {
);
let errMessage = `Unknown error from LLM stream: ${JSON.stringify(event)}`;
if (errorEvent?.value?.error) {
if (errorEvent?.value.error) {
errMessage = parseAndFormatApiError(errorEvent.value.error);
}
this.cancelPendingTools(`LLM stream error: ${errorMessage}`);
+4 -4
View File
@@ -109,9 +109,9 @@ export async function loadConfig(
};
const fileService = new FileDiscoveryService(workspaceDir, {
respectGitIgnore: configParams?.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: configParams?.fileFiltering?.respectGeminiIgnore,
customIgnoreFilePaths: configParams?.fileFiltering?.customIgnoreFilePaths,
respectGitIgnore: configParams.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: configParams.fileFiltering?.respectGeminiIgnore,
customIgnoreFilePaths: configParams.fileFiltering?.customIgnoreFilePaths,
});
const { memoryContent, fileCount, filePaths } =
await loadServerHierarchicalMemory(
@@ -212,7 +212,7 @@ export function loadEnvironment(): void {
function findEnvFile(startDir: string): string | null {
let currentDir = path.resolve(startDir);
while (true) {
for (;;) {
// prefer gemini-specific .env under GEMINI_DIR
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
if (fs.existsSync(geminiEnvPath)) {
+1 -1
View File
@@ -148,7 +148,7 @@ function isPersistedStateMetadata(
export function getPersistedState(
metadata: PersistedTaskMetadata,
): PersistedStateMetadata | undefined {
const state = metadata?.[METADATA_KEY];
const state = metadata[METADATA_KEY];
if (isPersistedStateMetadata(state)) {
return state;
}
@@ -157,7 +157,7 @@ export function assertUniqueFinalEventIsLast(
expect(finalEvent.metadata?.['coderAgent']).toMatchObject({
kind: 'state-change',
});
expect(finalEvent.status?.state).toBe('input-required');
expect(finalEvent.status.state).toBe('input-required');
expect(finalEvent.final).toBe(true);
// There is only one event with final and its the last
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.34.0-nightly.20260303.34f0c1538",
"version": "0.34.0-nightly.20260304.28af4e127",
"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.34.0-nightly.20260303.34f0c1538"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260304.28af4e127"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -45,7 +45,7 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
const { name, setting, scope } = args;
const settings = loadSettings(process.cwd()).merged;
if (!(settings.experimental?.extensionConfig ?? true)) {
if (!(settings.experimental.extensionConfig ?? true)) {
coreEvents.emitFeedback(
'error',
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
@@ -82,7 +82,7 @@ export async function handleUpdate(args: UpdateArgs) {
extensionManager,
updateState,
() => {},
settings.experimental?.extensionReloading,
settings.experimental.extensionReloading,
))!;
if (
updatedExtensionInfo.originalVersion !==
+3 -17
View File
@@ -99,12 +99,6 @@ export async function configureSpecificSetting(
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (!extensionConfig) {
logger.error(
`Could not find configuration for extension "${extensionName}".`,
);
return;
}
await updateSetting(
extensionConfig,
@@ -137,11 +131,7 @@ export async function configureExtension(
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
!extensionConfig ||
!extensionConfig.settings ||
extensionConfig.settings.length === 0
) {
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
logger.log(`Extension "${extensionName}" has no settings to configure.`);
return;
}
@@ -175,11 +165,7 @@ export async function configureAllExtensions(
const extensionConfig = await extensionManager.loadExtensionConfig(
extension.path,
);
if (
extensionConfig &&
extensionConfig.settings &&
extensionConfig.settings.length > 0
) {
if (extensionConfig.settings && extensionConfig.settings.length > 0) {
logger.log(`\nConfiguring settings for "${extension.name}"...`);
await configureExtensionSettings(
extensionConfig,
@@ -208,7 +194,7 @@ export async function configureExtensionSettings(
process.cwd(),
);
let workspaceSettings: Record<string, string> = {};
let workspaceSettings: Record<string, string | undefined> = {};
if (scope === ExtensionSettingScope.USER) {
workspaceSettings = await getScopedEnvContents(
extensionConfig,
+1 -4
View File
@@ -236,10 +236,7 @@ export async function handleMigrateFromClaude() {
const settings = loadSettings(workingDir);
// Merge migrated hooks with existing hooks
const existingHooks = (settings.merged?.hooks || {}) as Record<
string,
unknown
>;
const existingHooks = settings.merged.hooks as Record<string, unknown>;
const mergedHooks = { ...existingHooks, ...migratedHooks };
// Update settings (setValue automatically saves)
+6 -5
View File
@@ -117,7 +117,7 @@ async function addMcpServer(
const existingSettings = settings.forScope(settingsScope).settings;
const mcpServers = existingSettings.mcpServers || {};
const isExistingServer = !!mcpServers[name];
const isExistingServer = Object.hasOwn(mcpServers, name);
if (isExistingServer) {
debugLogger.log(
`MCP server "${name}" is already configured within ${scope} settings.`,
@@ -211,11 +211,12 @@ export const addCommand: CommandModule = {
})
.middleware((argv) => {
// Handle -- separator args as server args if present
if (argv['--']) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const dashArgs = argv['--'] as string[] | undefined;
if (dashArgs && dashArgs.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const existingArgs = (argv['args'] as Array<string | number>) || [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['args'] = [...existingArgs, ...(argv['--'] as string[])];
const existingArgs = argv['args'] as Array<string | number>;
argv['args'] = [...existingArgs, ...dashArgs];
}
}),
handler: async (argv) => {
@@ -43,9 +43,9 @@ async function handleEnable(args: Args): Promise<void> {
}
const result = await canLoadServer(name, {
adminMcpEnabled: settings.merged.admin?.mcp?.enabled ?? true,
allowedList: settings.merged.mcp?.allowed,
excludedList: settings.merged.mcp?.excluded,
adminMcpEnabled: settings.merged.admin.mcp.enabled,
allowedList: settings.merged.mcp.allowed,
excludedList: settings.merged.mcp.excluded,
});
if (
+5 -3
View File
@@ -39,10 +39,12 @@ export async function getMcpServersFromConfig(
requestSetting: promptForSetting,
});
const extensions = await extensionManager.loadExtensions();
const mcpServers = { ...settings.mcpServers };
const mcpServers: Record<string, MCPServerConfig> = {
...settings.mcpServers,
};
for (const extension of extensions) {
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
if (mcpServers[key]) {
if (key in mcpServers) {
return;
}
mcpServers[key] = {
@@ -52,7 +54,7 @@ export async function getMcpServersFromConfig(
});
}
const adminAllowlist = settings.admin?.mcp?.config;
const adminAllowlist = settings.admin.mcp.config;
const filteredResult = applyAdminAllowlist(mcpServers, adminAllowlist);
return filteredResult;
+1 -1
View File
@@ -24,7 +24,7 @@ async function removeMcpServer(
const existingSettings = settings.forScope(settingsScope).settings;
const mcpServers = existingSettings.mcpServers || {};
if (!mcpServers[name]) {
if (!(name in mcpServers)) {
debugLogger.log(`Server "${name}" not found in ${scope} settings.`);
return;
}
+40 -40
View File
@@ -327,11 +327,11 @@ export async function parseArguments(
return true;
});
if (settings.experimental?.extensionManagement) {
if (settings.experimental.extensionManagement) {
yargsInstance.command(extensionsCommand);
}
if (settings.skills?.enabled ?? true) {
if (settings.skills.enabled) {
yargsInstance.command(skillsCommand);
}
// Register hooks command if hooks are enabled
@@ -456,16 +456,16 @@ export async function loadCliConfig(
process.env['GEMINI_SANDBOX'] = 'true';
}
const memoryImportFormat = settings.context?.importFormat || 'tree';
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
const memoryImportFormat = settings.context.importFormat || 'tree';
const includeDirectoryTree = settings.context.includeDirectoryTree;
const ideMode = settings.ide?.enabled ?? false;
const ideMode = settings.ide.enabled;
const folderTrust =
process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true' ||
process.env['VITEST'] === 'true'
? false
: (settings.security?.folderTrust?.enabled ?? false);
: settings.security.folderTrust.enabled;
const trustedFolder =
isWorkspaceTrusted(settings, cwd, undefined, {
prompt: argv.prompt,
@@ -476,7 +476,7 @@ export async function loadCliConfig(
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
if (settings.context?.fileName) {
if (settings.context.fileName) {
setServerGeminiMdFilename(settings.context.fileName);
} else {
// Reset to default if not provided in settings.
@@ -487,15 +487,15 @@ export async function loadCliConfig(
const memoryFileFiltering = {
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
...settings.context?.fileFiltering,
...settings.context.fileFiltering,
};
const fileFiltering = {
...DEFAULT_FILE_FILTERING_OPTIONS,
...settings.context?.fileFiltering,
...settings.context.fileFiltering,
};
const includeDirectories = (settings.context?.includeDirectories || [])
const includeDirectories = settings.context.includeDirectories
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
@@ -515,7 +515,7 @@ export async function loadCliConfig(
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const experimentalJitContext = settings.experimental?.jitContext ?? false;
const experimentalJitContext = settings.experimental.jitContext;
let memoryContent: string | HierarchicalMemory = '';
let fileCount = 0;
@@ -525,7 +525,7 @@ export async function loadCliConfig(
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const result = await loadServerHierarchicalMemory(
cwd,
settings.context?.loadMemoryFromIncludeDirectories || false
settings.context.loadMemoryFromIncludeDirectories
? includeDirectories
: [],
debugMode,
@@ -534,7 +534,7 @@ export async function loadCliConfig(
trustedFolder,
memoryImportFormat,
memoryFileFiltering,
settings.context?.discoveryMaxDirs,
settings.context.discoveryMaxDirs,
);
memoryContent = result.memoryContent;
fileCount = result.fileCount;
@@ -548,8 +548,8 @@ export async function loadCliConfig(
const rawApprovalMode =
argv.approvalMode ||
(argv.yolo ? 'yolo' : undefined) ||
((settings.general?.defaultApprovalMode as string) !== 'yolo'
? settings.general?.defaultApprovalMode
((settings.general.defaultApprovalMode as string) !== 'yolo'
? settings.general.defaultApprovalMode
: undefined);
if (rawApprovalMode) {
@@ -561,7 +561,7 @@ export async function loadCliConfig(
approvalMode = ApprovalMode.AUTO_EDIT;
break;
case 'plan':
if (!(settings.experimental?.plan ?? false)) {
if (!settings.experimental.plan) {
debugLogger.warn(
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
);
@@ -583,9 +583,9 @@ export async function loadCliConfig(
}
// Override approval mode if disableYoloMode is set.
if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
if (settings.security.disableYoloMode || settings.admin.secureModeEnabled) {
if (approvalMode === ApprovalMode.YOLO) {
if (settings.admin?.secureModeEnabled) {
if (settings.admin.secureModeEnabled) {
debugLogger.error(
'YOLO mode is disabled by "secureModeEnabled" setting.',
);
@@ -636,7 +636,7 @@ export async function loadCliConfig(
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.isCommand);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedTools = argv.allowedTools || settings.tools.allowed || [];
const allowedToolsSet = new Set(allowedTools);
// In non-interactive mode, exclude tools that require a prompt.
@@ -694,7 +694,7 @@ export async function loadCliConfig(
},
mcp: {
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
allowed: argv.allowedMcpServerNames ?? settings.mcp.allowed,
},
policyPaths: argv.policy,
};
@@ -715,7 +715,7 @@ export async function loadCliConfig(
const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
const specifiedModel =
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
argv.model || process.env['GEMINI_MODEL'] || settings.model.name;
const resolvedModel =
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
@@ -729,9 +729,9 @@ export async function loadCliConfig(
const ptyInfo = await getPty();
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
const extensionsEnabled = settings.admin?.extensions?.enabled ?? true;
const adminSkillsEnabled = settings.admin?.skills?.enabled ?? true;
const mcpEnabled = settings.admin.mcp.enabled;
const extensionsEnabled = settings.admin.extensions.enabled;
const adminSkillsEnabled = settings.admin.skills.enabled;
// Create MCP enablement manager and callbacks
const mcpEnablementManager = McpServerEnablementManager.getInstance();
@@ -739,8 +739,8 @@ export async function loadCliConfig(
? mcpEnablementManager.getEnablementCallbacks()
: undefined;
const adminAllowlist = settings.admin?.mcp?.config;
let mcpServerCommand = mcpEnabled ? settings.mcp?.serverCommand : undefined;
const adminAllowlist = settings.admin.mcp.config;
let mcpServerCommand = mcpEnabled ? settings.mcp.serverCommand : undefined;
let mcpServers = mcpEnabled ? settings.mcpServers : {};
if (mcpEnabled && adminAllowlist && Object.keys(adminAllowlist).length > 0) {
@@ -748,7 +748,7 @@ export async function loadCliConfig(
mcpServers = result.mcpServers;
mcpServerCommand = undefined;
if (result.blockedServerNames && result.blockedServerNames.length > 0) {
if (result.blockedServerNames.length > 0) {
const message = getAdminBlockedMcpServersMessage(
result.blockedServerNames,
undefined,
@@ -766,17 +766,17 @@ export async function loadCliConfig(
includeDirectoryTree,
includeDirectories,
loadMemoryFromIncludeDirectories:
settings.context?.loadMemoryFromIncludeDirectories || false,
settings.context.loadMemoryFromIncludeDirectories,
debugMode,
question,
coreTools: settings.tools?.core || undefined,
coreTools: settings.tools.core || undefined,
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
policyEngineConfig,
policyUpdateConfirmationRequest,
excludeTools,
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
toolDiscoveryCommand: settings.tools.discoveryCommand,
toolCallCommand: settings.tools.callCommand,
mcpServerCommand,
mcpServers,
mcpEnablementCallbacks,
@@ -785,32 +785,32 @@ export async function loadCliConfig(
agents: settings.agents,
adminSkillsEnabled,
allowedMcpServers: mcpEnabled
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
? (argv.allowedMcpServerNames ?? settings.mcp.allowed)
: undefined,
blockedMcpServers: mcpEnabled
? argv.allowedMcpServerNames
? undefined
: settings.mcp?.excluded
: settings.mcp.excluded
: undefined,
blockedEnvironmentVariables:
settings.security?.environmentVariableRedaction?.blocked,
settings.security.environmentVariableRedaction.blocked,
enableEnvironmentVariableRedaction:
settings.security?.environmentVariableRedaction?.enabled,
settings.security.environmentVariableRedaction.enabled,
userMemory: memoryContent,
geminiMdFileCount: fileCount,
geminiMdFilePaths: filePaths,
approvalMode,
disableYoloMode:
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
showMemoryUsage: settings.ui?.showMemoryUsage || false,
settings.security.disableYoloMode || settings.admin.secureModeEnabled,
showMemoryUsage: settings.ui.showMemoryUsage,
accessibility: {
...settings.ui?.accessibility,
...settings.ui.accessibility,
screenReader,
},
telemetry: telemetrySettings,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled,
usageStatisticsEnabled: settings.privacy.usageStatisticsEnabled,
fileFiltering,
checkpointing: settings.general?.checkpointing?.enabled,
checkpointing: settings.general.checkpointing.enabled,
proxy:
process.env['HTTPS_PROXY'] ||
process.env['https_proxy'] ||
+8 -14
View File
@@ -154,11 +154,8 @@ export class ExtensionManager extends ExtensionLoader {
installMetadata: ExtensionInstallMetadata,
previousExtensionConfig?: ExtensionConfig,
): Promise<GeminiCLIExtension> {
if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
const extensionAllowed = this.settings.security?.allowedExtensions.some(
if (this.settings.security.allowedExtensions.length > 0) {
const extensionAllowed = this.settings.security.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(installMetadata.source);
@@ -312,7 +309,7 @@ Would you like to attempt to install via "git clone" instead?`,
const destinationPath = new ExtensionStorage(
newExtensionName,
).getExtensionDir();
let previousSettings: Record<string, string> | undefined;
let previousSettings: Record<string, string | undefined> | undefined;
if (isUpdate) {
previousSettings = await getEnvContents(
previousExtensionConfig,
@@ -626,19 +623,16 @@ Would you like to attempt to install via "git clone" instead?`,
const installMetadata = loadInstallMetadata(extensionDir);
let effectiveExtensionPath = extensionDir;
if (
this.settings.security?.allowedExtensions &&
this.settings.security?.allowedExtensions.length > 0
) {
if (this.settings.security.allowedExtensions.length > 0) {
if (!installMetadata?.source) {
throw new Error(
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
);
}
const extensionAllowed = this.settings.security?.allowedExtensions.some(
const extensionAllowed = this.settings.security.allowedExtensions.some(
(pattern) => {
try {
return new RegExp(pattern).test(installMetadata?.source);
return new RegExp(pattern).test(installMetadata.source);
} catch (e) {
throw new Error(
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
@@ -672,8 +666,8 @@ Would you like to attempt to install via "git clone" instead?`,
const extensionId = getExtensionId(config, installMetadata);
let userSettings: Record<string, string> = {};
let workspaceSettings: Record<string, string> = {};
let userSettings: Record<string, string | undefined> = {};
let workspaceSettings: Record<string, string | undefined> = {};
if (this.settings.experimental.extensionConfig) {
userSettings = await getScopedEnvContents(
@@ -166,7 +166,7 @@ export class ExtensionEnablementManager {
const extensionConfig = config[extensionName];
// Extensions are enabled by default.
let enabled = true;
const allOverrides = extensionConfig?.overrides ?? [];
const allOverrides = extensionConfig.overrides ?? [];
for (const rule of allOverrides) {
const override = Override.fromFileRule(rule);
if (override.matchesPath(ensureLeadingAndTrailingSlash(currentPath))) {
@@ -64,7 +64,7 @@ export async function maybePromptForSettings(
extensionId: string,
requestSetting: (setting: ExtensionSetting) => Promise<string>,
previousExtensionConfig?: ExtensionConfig,
previousSettings?: Record<string, string>,
previousSettings?: Record<string, string | undefined>,
): Promise<void> {
const { name: extensionName, settings } = extensionConfig;
if (
@@ -92,7 +92,9 @@ export async function maybePromptForSettings(
previousExtensionConfig?.settings ?? [],
);
const allSettings: Record<string, string> = { ...previousSettings };
const allSettings: Record<string, string | undefined> = {
...previousSettings,
};
for (const removedEnvSetting of settingsChanges.removeEnv) {
delete allSettings[removedEnvSetting.envVar];
@@ -174,13 +176,13 @@ export async function getScopedEnvContents(
extensionId: string,
scope: ExtensionSettingScope,
workspaceDir?: string,
): Promise<Record<string, string>> {
): Promise<Record<string, string | undefined>> {
const { name: extensionName } = extensionConfig;
const keychain = new KeychainTokenStorage(
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
);
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
let customEnv: Record<string, string> = {};
let customEnv: Record<string, string | undefined> = {};
if (fsSync.existsSync(envFilePath)) {
const stat = fsSync.statSync(envFilePath);
if (!stat.isDirectory()) {
@@ -206,7 +208,7 @@ export async function getEnvContents(
extensionConfig: ExtensionConfig,
extensionId: string,
workspaceDir: string,
): Promise<Record<string, string>> {
): Promise<Record<string, string | undefined>> {
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
return Promise.resolve({});
}
+3 -3
View File
@@ -109,16 +109,16 @@ export function tryParseGithubUrl(source: string): GithubRepoInfo | null {
if (!parsedUrl) {
throw new Error(`Invalid repo URL: ${source}`);
}
if (parsedUrl?.host !== 'github.com') {
if (parsedUrl.host !== 'github.com') {
return null;
}
// The pathname should be "/owner/repo".
const parts = parsedUrl?.pathname
const parts = parsedUrl.pathname
.split('/')
// Remove the empty segments, fixes trailing and leading slashes
.filter((part) => part !== '');
if (parts?.length !== 2) {
if (parts.length !== 2) {
throw new Error(
`Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
);
+1 -1
View File
@@ -48,7 +48,7 @@ export async function updateExtension(
`Extension ${extension.name} cannot be updated, type is unknown.`,
);
}
if (installMetadata?.type === 'link') {
if (installMetadata.type === 'link') {
dispatchExtensionStateUpdate({
type: 'SET_STATE',
payload: { name: extension.name, state: ExtensionUpdateState.UP_TO_DATE },
@@ -226,7 +226,7 @@ export class McpServerEnablementManager {
async isFileEnabled(serverName: string): Promise<boolean> {
const config = await this.readConfig();
const state = config[normalizeServerId(serverName)];
return state?.enabled ?? true;
return state.enabled ?? true;
}
/**
+1 -1
View File
@@ -45,7 +45,7 @@ function getSandboxCommand(
const environmentConfiguredSandbox =
process.env['GEMINI_SANDBOX']?.toLowerCase().trim() ?? '';
sandbox =
environmentConfiguredSandbox?.length > 0
environmentConfiguredSandbox.length > 0
? environmentConfiguredSandbox
: sandbox;
if (sandbox === '1' || sandbox === 'true') sandbox = true;
+50 -50
View File
@@ -460,7 +460,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.security?.folderTrust?.enabled).toBe(false); // Workspace setting should be used
expect(settings.merged.security.folderTrust?.enabled).toBe(false); // Workspace setting should be used
});
it('should use system folderTrust over user setting', () => {
@@ -500,7 +500,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
expect(settings.merged.security.folderTrust?.enabled).toBe(true); // System setting should be used
});
it('should not allow user or workspace to override system disableYoloMode', () => {
@@ -534,7 +534,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
expect(settings.merged.security.disableYoloMode).toBe(true); // System setting should be used
});
it.each([
@@ -630,7 +630,7 @@ describe('Settings Loading and Merging', () => {
'WORKSPACE_DEBUG',
'WORKSPACE_VAR',
]);
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
expect(settings.merged.advanced.excludedEnvVars).toEqual([
'DEBUG',
'DEBUG_MODE',
'NODE_ENV',
@@ -658,7 +658,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.context?.fileName).toBeUndefined();
expect(settings.merged.context.fileName).toBeUndefined();
});
it.each([
@@ -991,7 +991,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.model?.compressionThreshold).toEqual(expected);
expect(settings.merged.model.compressionThreshold).toEqual(expected);
});
});
@@ -1018,7 +1018,7 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.model?.compressionThreshold).toEqual(0.5);
expect(settings.merged.model.compressionThreshold).toEqual(0.5);
});
it('should merge includeDirectories from all scopes', () => {
@@ -1052,7 +1052,7 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.context?.includeDirectories).toEqual([
expect(settings.merged.context.includeDirectories).toEqual([
'/system/defaults/dir',
'/user/dir1',
'/user/dir2',
@@ -1247,7 +1247,7 @@ describe('Settings Loading and Merging', () => {
expect((settings.merged as TestSettings)['workspaceOnly']).toBe(
'workspace_value',
);
expect(settings.merged.ui?.theme).toBe('light'); // workspace overrides user
expect(settings.merged.ui.theme).toBe('light'); // workspace overrides user
delete process.env['SYSTEM_VAR'];
delete process.env['USER_VAR'];
@@ -1275,7 +1275,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
expect(settings.merged.advanced.dnsResolutionOrder).toBe('verbatim');
});
it('should use user dnsResolutionOrder if workspace is not defined', () => {
@@ -1294,7 +1294,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
expect(settings.merged.advanced.dnsResolutionOrder).toBe('verbatim');
});
it('should leave unresolved environment variables as is', () => {
@@ -1599,7 +1599,7 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
// Verify the settings were loaded correctly
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
expect(settings.merged.advanced.excludedEnvVars).toEqual([
'DEBUG',
'DEBUG_MODE',
]);
@@ -1637,7 +1637,7 @@ describe('Settings Loading and Merging', () => {
'NODE_ENV',
'DEBUG',
]);
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
expect(settings.merged.advanced.excludedEnvVars).toEqual([
'DEBUG',
'DEBUG_MODE',
'NODE_ENV',
@@ -1677,7 +1677,7 @@ describe('Settings Loading and Merging', () => {
'WORKSPACE_DEBUG',
'WORKSPACE_VAR',
]);
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
expect(settings.merged.advanced.excludedEnvVars).toEqual([
'DEBUG',
'DEBUG_MODE',
'NODE_ENV',
@@ -1711,9 +1711,9 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.tools?.sandbox).toBe(true);
expect(settings.merged.context?.fileName).toBe('WORKSPACE.md');
expect(settings.merged.ui?.theme).toBe('dark');
expect(settings.merged.tools.sandbox).toBe(true);
expect(settings.merged.context.fileName).toBe('WORKSPACE.md');
expect(settings.merged.ui.theme).toBe('dark');
});
it('should NOT merge workspace settings when workspace is not trusted', () => {
@@ -1744,9 +1744,9 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
expect(settings.merged.ui?.theme).toBe('dark'); // User setting
expect(settings.merged.tools.sandbox).toBe(false); // User setting
expect(settings.merged.context.fileName).toBe('USER.md'); // User setting
expect(settings.merged.ui.theme).toBe('dark'); // User setting
});
it('should NOT merge workspace settings when workspace trust is undefined', () => {
@@ -1777,8 +1777,8 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
expect(settings.merged.tools.sandbox).toBe(false); // User setting
expect(settings.merged.context.fileName).toBe('USER.md'); // User setting
});
});
@@ -2229,7 +2229,7 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
// Verify it was migrated in the merged settings
expect(settings.merged.general?.enableAutoUpdate).toBe(false);
expect(settings.merged.general.enableAutoUpdate).toBe(false);
// Verify it was saved back to disk (via setValue calling updateSettingsFilePreservingFormat)
expect(updateSettingsFilePreservingFormat).toHaveBeenCalledWith(
@@ -2289,7 +2289,7 @@ describe('Settings Loading and Merging', () => {
).toBe(true);
// Merged should also reflect it (system overrides defaults, but both are migrated)
expect(settings.merged.general?.enableAutoUpdateNotification).toBe(false);
expect(settings.merged.general.enableAutoUpdateNotification).toBe(false);
// Verify it was NOT saved back to disk
expect(updateSettingsFilePreservingFormat).not.toHaveBeenCalledWith(
@@ -2487,10 +2487,10 @@ describe('Settings Loading and Merging', () => {
// 1. Verify that on initial load, file-based admin settings are ignored
// and schema defaults are used instead.
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false); // default: false
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true); // default: true
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true); // default: true
expect(loadedSettings.merged.ui?.theme).toBe('system-theme'); // non-admin setting should be loaded
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false); // default: false
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true); // default: true
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true); // default: true
expect(loadedSettings.merged.ui.theme).toBe('system-theme'); // non-admin setting should be loaded
// 2. Now, set remote admin settings.
loadedSettings.setRemoteAdminSettings({
@@ -2503,11 +2503,11 @@ describe('Settings Loading and Merging', () => {
});
// 3. Verify that remote admin settings take precedence.
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(false);
// non-admin setting should remain unchanged
expect(loadedSettings.merged.ui?.theme).toBe('system-theme');
expect(loadedSettings.merged.ui.theme).toBe('system-theme');
});
it('should set remote admin settings and recompute merged settings', () => {
@@ -2532,10 +2532,10 @@ describe('Settings Loading and Merging', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
// Ensure initial state from defaults (as file-based admin settings are ignored)
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
expect(loadedSettings.merged.ui.theme).toBe('initial-theme');
const newRemoteSettings = {
strictModeDisabled: false,
@@ -2549,11 +2549,11 @@ describe('Settings Loading and Merging', () => {
loadedSettings.setRemoteAdminSettings(newRemoteSettings);
// Verify that remote admin settings are applied
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(false);
// Non-admin settings should remain untouched
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
expect(loadedSettings.merged.ui.theme).toBe('initial-theme');
});
it('should correctly handle undefined remote admin settings', () => {
@@ -2573,16 +2573,16 @@ describe('Settings Loading and Merging', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
// Should have default admin settings
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
loadedSettings.setRemoteAdminSettings({}); // Set empty remote settings
// Admin settings should revert to defaults because there are no remote overrides
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
});
it('should un-nest MCP configuration from remote settings', () => {
@@ -2604,7 +2604,7 @@ describe('Settings Loading and Merging', () => {
},
});
expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers);
expect(loadedSettings.merged.admin.mcp?.config).toEqual(mcpServers);
});
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
@@ -2630,9 +2630,9 @@ describe('Settings Loading and Merging', () => {
loadedSettings.setRemoteAdminSettings({});
// Should default to schema defaults (standard defaults)
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
});
});
+17 -23
View File
@@ -54,24 +54,20 @@ import {
export function getMergeStrategyForPath(
path: string[],
): MergeStrategy | undefined {
let current: SettingDefinition | undefined = undefined;
let currentSchema: SettingsSchema | undefined = getSettingsSchema();
let parent: SettingDefinition | undefined = undefined;
for (const key of path) {
if (!currentSchema || !currentSchema[key]) {
const current: SettingDefinition | undefined = currentSchema?.[key];
if (!current) {
// Key not found in schema - check if parent has additionalProperties
if (parent?.additionalProperties?.mergeStrategy) {
return parent.additionalProperties.mergeStrategy;
}
return undefined;
return parent?.additionalProperties?.mergeStrategy;
}
parent = current;
current = currentSchema[key];
currentSchema = current.properties;
}
return current?.mergeStrategy;
return parent?.mergeStrategy;
}
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
@@ -372,19 +368,17 @@ export class LoadedSettings {
// Remote admin settings always take precedence and file-based admin settings
// are ignored.
const adminSettingSchema = getSettingsSchema().admin;
if (adminSettingSchema?.properties) {
const adminSchema = adminSettingSchema.properties;
const adminDefaults = getDefaultsFromSchema(adminSchema);
const adminSchema = adminSettingSchema.properties;
const adminDefaults = getDefaultsFromSchema(adminSchema);
// The final admin settings are the defaults overridden by remote settings.
// Any admin settings from files are ignored.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
merged.admin = customDeepMerge(
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
adminDefaults,
this._remoteAdminSettings?.admin ?? {},
) as MergedSettings['admin'];
}
// The final admin settings are the defaults overridden by remote settings.
// Any admin settings from files are ignored.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
merged.admin = customDeepMerge(
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
adminDefaults,
this._remoteAdminSettings?.admin ?? {},
) as MergedSettings['admin'];
return merged;
}
@@ -493,7 +487,7 @@ export class LoadedSettings {
function findEnvFile(startDir: string): string | null {
let currentDir = path.resolve(startDir);
while (true) {
for (;;) {
// prefer gemini-specific .env under GEMINI_DIR
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
if (fs.existsSync(geminiEnvPath)) {
@@ -583,7 +577,7 @@ export function loadEnvironment(
const parsedEnv = dotenv.parse(envFileContent);
const excludedVars =
settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
settings.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
for (const key in parsedEnv) {
@@ -1085,7 +1079,7 @@ function migrateExperimentalSettings(
};
const agentsOverrides = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...((agentsSettings['overrides'] as Record<string, unknown>) || {}),
...(agentsSettings['overrides'] as Record<string, unknown>),
};
let modified = false;
@@ -195,7 +195,7 @@ describe('Settings Repro', () => {
// If it doesn't throw, check if it merged correctly.
// The model.compressionThreshold should be present.
// And model.name should probably be undefined or default, but certainly NOT { compressionThreshold: 0.8 }
expect(settings.merged.model?.compressionThreshold).toBe(0.8);
expect(typeof settings.merged.model?.name).not.toBe('object');
expect(settings.merged.model.compressionThreshold).toBe(0.8);
expect(typeof settings.merged.model.name).not.toBe('object');
});
});
+3 -3
View File
@@ -33,7 +33,7 @@ export async function runDeferredCommand(settings: MergedSettings) {
const adminSettings = settings.admin;
const commandName = deferredCommand.commandName;
if (commandName === 'mcp' && adminSettings?.mcp?.enabled === false) {
if (commandName === 'mcp' && adminSettings.mcp.enabled === false) {
coreEvents.emitFeedback(
'error',
getAdminErrorMessage('MCP', undefined /* config */),
@@ -44,7 +44,7 @@ export async function runDeferredCommand(settings: MergedSettings) {
if (
commandName === 'extensions' &&
adminSettings?.extensions?.enabled === false
adminSettings.extensions.enabled === false
) {
coreEvents.emitFeedback(
'error',
@@ -54,7 +54,7 @@ export async function runDeferredCommand(settings: MergedSettings) {
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
}
if (commandName === 'skills' && adminSettings?.skills?.enabled === false) {
if (commandName === 'skills' && adminSettings.skills.enabled === false) {
coreEvents.emitFeedback(
'error',
getAdminErrorMessage('Agent skills', undefined /* config */),
+5 -5
View File
@@ -378,7 +378,7 @@ export async function main() {
if (
(argv.allowedTools && argv.allowedTools.length > 0) ||
(settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0)
(settings.merged.tools.allowed && settings.merged.tools.allowed.length > 0)
) {
coreEvents.emitFeedback(
'warning',
@@ -387,7 +387,7 @@ export async function main() {
}
if (
settings.merged.tools?.exclude &&
settings.merged.tools.exclude &&
settings.merged.tools.exclude.length > 0
) {
coreEvents.emitFeedback(
@@ -748,7 +748,7 @@ export async function main() {
? SessionStartSource.Resume
: SessionStartSource.Startup;
const hookSystem = config?.getHookSystem();
const hookSystem = config.getHookSystem();
if (hookSystem) {
const result = await hookSystem.fireSessionStartEvent(sessionStartSource);
@@ -784,7 +784,7 @@ export async function main() {
new UserPromptEvent(
input.length,
prompt_id,
config.getContentGeneratorConfig()?.authType,
config.getContentGeneratorConfig().authType,
input,
),
);
@@ -881,7 +881,7 @@ function setupAdminControlsListener() {
type?: string;
settings?: AdminControlsSettings;
};
if (message?.type === 'admin-settings' && message.settings) {
if (message.type === 'admin-settings' && message.settings) {
if (config) {
config.setRemoteAdminSettings(message.settings);
} else {
+4 -4
View File
@@ -135,7 +135,7 @@ export async function runNonInteractive({
key: { name?: string; ctrl?: boolean },
) => {
// Detect Ctrl+C: either ctrl+c key combo or raw character code 3
if ((key && key.ctrl && key.name === 'c') || str === '\u0003') {
if ((key.ctrl && key.name === 'c') || str === '\u0003') {
// Only handle once
if (isAborting) {
return;
@@ -289,7 +289,7 @@ export async function runNonInteractive({
let currentMessages: Content[] = [{ role: 'user', parts: query }];
let turnCount = 0;
while (true) {
for (;;) {
turnCount++;
if (
config.getMaxSessionTurns() >= 0 &&
@@ -461,8 +461,8 @@ export async function runNonInteractive({
(tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION,
);
if (stopExecutionTool && stopExecutionTool.response.error) {
const stopMessage = `Agent execution stopped: ${stopExecutionTool.response.error.message}`;
if (stopExecutionTool) {
const stopMessage = `Agent execution stopped: ${stopExecutionTool.response.error?.message}`;
if (config.getOutputFormat() === OutputFormat.TEXT) {
process.stderr.write(`${stopMessage}\n`);
@@ -56,14 +56,14 @@ export const handleSlashCommand = async (
if (commandToExecute.action) {
// Not used by custom commands but may be in the future.
const sessionStats: SessionStatsState = {
sessionId: config?.getSessionId(),
sessionId: config.getSessionId(),
sessionStartTime: new Date(),
metrics: uiTelemetryService.getMetrics(),
lastPromptTokenCount: 0,
promptCount: 1,
};
const logger = new Logger(config?.getSessionId() || '', config?.storage);
const logger = new Logger(config.getSessionId() || '', config.storage);
const context: CommandContext = {
services: {
@@ -158,7 +158,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
themeCommand,
toolsCommand,
...(this.config?.isSkillsSupportEnabled()
? this.config?.getSkillManager()?.isAdminEnabled() === false
? this.config.getSkillManager().isAdminEnabled() === false
? [
{
name: 'skills',
@@ -126,7 +126,7 @@ export class FileCommandLoader implements ICommandLoader {
if (
!signal.aborted &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(error as { code?: string })?.code !== 'ENOENT'
(error as { code?: string }).code !== 'ENOENT'
) {
coreEvents.emitFeedback(
'error',
+2 -2
View File
@@ -62,7 +62,7 @@ export class McpPromptLoader implements ICommandLoader {
let helpMessage = `Arguments for "${prompt.name}":\n\n`;
if (prompt.arguments && prompt.arguments.length > 0) {
helpMessage += `You can provide arguments by name (e.g., --argName="value") or by position.\n\n`;
helpMessage += `e.g., ${prompt.name} ${prompt.arguments?.map((_) => `"foo"`)} is equivalent to ${prompt.name} ${prompt.arguments?.map((arg) => `--${arg.name}="foo"`)}\n\n`;
helpMessage += `e.g., ${prompt.name} ${prompt.arguments.map((_) => `"foo"`)} is equivalent to ${prompt.name} ${prompt.arguments.map((arg) => `--${arg.name}="foo"`)}\n\n`;
}
for (const arg of prompt.arguments) {
helpMessage += ` --${arg.name}\n`;
@@ -123,7 +123,7 @@ export class McpPromptLoader implements ICommandLoader {
};
}
const maybeContent = result.messages?.[0]?.content;
const maybeContent = result.messages[0]?.content;
if (maybeContent.type !== 'text') {
return {
type: 'message',
@@ -119,7 +119,7 @@ export class ShellProcessor implements IPromptProcessor {
if (!command) continue;
if (context.session.sessionShellAllowlist?.has(command)) {
if (context.session.sessionShellAllowlist.has(command)) {
continue;
}
+4 -4
View File
@@ -312,7 +312,7 @@ export class AppRig {
const details = call.confirmationDetails;
const title = 'title' in details ? details.title : '';
const toolDisplayName =
call.tool?.displayName || title.replace(/^Confirm:\s*/, '');
call.tool.displayName || title.replace(/^Confirm:\s*/, '');
if (!this.pendingConfirmations.has(call.correlationId)) {
this.pendingConfirmations.set(call.correlationId, {
toolName: call.request.name,
@@ -469,7 +469,7 @@ export class AppRig {
} = options;
const start = Date.now();
while (true) {
for (;;) {
if (await predicate()) return;
if (Date.now() - start > timeout) {
@@ -622,7 +622,7 @@ export class AppRig {
onConfirmation?: (confirmation: PendingConfirmation) => void | boolean,
timeout = 60000,
) {
while (true) {
for (;;) {
const event = await this.waitForNextEvent(timeout);
if (event.type === 'idle') {
break;
@@ -710,7 +710,7 @@ export class AppRig {
if (this.config) {
const recordingService = this.config
.getGeminiClient()
?.getChatRecordingService();
.getChatRecordingService();
if (recordingService) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(recordingService as any).conversationFile = null;
+1 -1
View File
@@ -18,7 +18,7 @@ export async function waitFor(
): Promise<void> {
const startTime = Date.now();
while (true) {
for (;;) {
try {
await assertion();
return;
+1 -1
View File
@@ -47,7 +47,7 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
const b = v[c % 6];
const g = v[Math.floor(c / 6) % 6];
const r = v[Math.floor(c / 36) % 6];
return `#${[r, g, b].map((x) => x?.toString(16).padStart(2, '0')).join('')}`;
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
} else if (colorCode >= 232 && colorCode <= 255) {
const gray = 8 + (colorCode - 232) * 10;
const hex = gray.toString(16).padStart(2, '0');
+3 -5
View File
@@ -129,7 +129,7 @@ import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import { relaunchApp } from '../utils/processUtils.js';
import type { SessionInfo } from '../utils/sessionUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useMcpStatus } from './hooks/useMcpStatus.js';
@@ -781,13 +781,12 @@ export const AppContainer = (props: AppContainerProps) => {
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()
) {
await runExitCleanup();
writeToStdout(`
----------------------------------------------------------------
Logging in with Google... Restarting Gemini CLI to continue.
----------------------------------------------------------------
`);
process.exit(RELAUNCH_EXIT_CODE);
await relaunchApp();
}
}
setAuthState(AuthState.Authenticated);
@@ -2497,8 +2496,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
});
}
}
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
await relaunchApp();
},
handleNewAgentsSelect: async (choice: NewAgentsChoice) => {
if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) {
+3 -7
View File
@@ -73,12 +73,8 @@ export function IdeIntegrationNudge({
];
const installText = isExtensionPreInstalled
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`;
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${ideName}.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${ideName}.`;
return (
<Box
@@ -92,7 +88,7 @@ export function IdeIntegrationNudge({
<Box marginBottom={1} flexDirection="column">
<Text>
<Text color={theme.status.warning}>{'> '}</Text>
{`Do you want to connect ${ideName ?? 'your editor'} to Gemini CLI?`}
{`Do you want to connect ${ideName} to Gemini CLI?`}
</Text>
<Text color={theme.text.secondary}>{installText}</Text>
</Box>
+1 -1
View File
@@ -44,7 +44,7 @@ export function ApiAuthDialog({
const buffer = useTextBuffer({
initialText: initialApiKey || '',
initialCursorOffset: initialApiKey?.length || 0,
initialCursorOffset: initialApiKey.length || 0,
viewport: {
width: viewportWidth,
height: 4,
+2 -6
View File
@@ -21,9 +21,8 @@ import {
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js';
import { AuthState } from '../types.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { validateAuthMethodWithSettings } from './useAuth.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
import { relaunchApp } from '../../utils/processUtils.js';
interface AuthDialogProps {
config: Config;
@@ -133,10 +132,7 @@ export function AuthDialog({
config.isBrowserLaunchSuppressed()
) {
setExiting(true);
setTimeout(async () => {
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
}, 100);
setTimeout(relaunchApp, 100);
return;
}
@@ -9,7 +9,10 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
import {
RELAUNCH_EXIT_CODE,
_resetRelaunchStateForTesting,
} from '../../utils/processUtils.js';
import { type Config } from '@google/gemini-cli-core';
// Mocks
@@ -38,6 +41,7 @@ describe('LoginWithGoogleRestartDialog', () => {
vi.clearAllMocks();
exitSpy.mockClear();
vi.useRealTimers();
_resetRelaunchStateForTesting();
});
it('renders correctly', async () => {
@@ -8,8 +8,7 @@ import { type Config } from '@google/gemini-cli-core';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
import { relaunchApp } from '../../utils/processUtils.js';
interface LoginWithGoogleRestartDialogProps {
onDismiss: () => void;
@@ -36,8 +35,7 @@ export const LoginWithGoogleRestartDialog = ({
});
}
}
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
await relaunchApp();
}, 100);
return true;
}
+1 -1
View File
@@ -68,5 +68,5 @@ async function getIdeClientName(context: CommandContext) {
return '';
}
const ideClient = await IdeClient.getInstance();
return ideClient?.getDetectedIdeDisplayName() ?? '';
return ideClient.getDetectedIdeDisplayName() ?? '';
}
@@ -91,7 +91,7 @@ async function enableAction(
const allAgents = agentRegistry.getAllAgentNames();
const overrides = settings.merged.agents.overrides;
const disabledAgents = Object.keys(overrides).filter(
(name) => overrides[name]?.enabled === false,
(name) => overrides[name].enabled === false,
);
if (allAgents.includes(agentName) && !disabledAgents.includes(agentName)) {
@@ -167,7 +167,7 @@ async function disableAction(
const allAgents = agentRegistry.getAllAgentNames();
const overrides = settings.merged.agents.overrides;
const disabledAgents = Object.keys(overrides).filter(
(name) => overrides[name]?.enabled === false,
(name) => overrides[name].enabled === false,
);
if (disabledAgents.includes(agentName)) {
@@ -271,7 +271,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
const overrides = settings.merged.agents.overrides;
const disabledAgents = Object.entries(overrides)
.filter(([_, override]) => override?.enabled === false)
.filter(([_, override]) => override.enabled === false)
.map(([name]) => name);
return disabledAgents.filter((name) => name.startsWith(partialArg));
@@ -291,7 +291,7 @@ function completeAllAgents(context: CommandContext, partialArg: string) {
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
const allAgents = agentRegistry?.getAllDiscoveredAgentNames() ?? [];
const allAgents = agentRegistry.getAllDiscoveredAgentNames() ?? [];
return allAgents.filter((name: string) => name.startsWith(partialArg));
}
+1 -1
View File
@@ -37,7 +37,7 @@ const authLogoutCommand: SlashCommand = {
undefined,
);
// Strip thoughts from history instead of clearing completely
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
context.services.config?.getGeminiClient().stripThoughtsFromHistory();
// Return logout action to signal explicit state change
return {
type: 'logout',
+3 -3
View File
@@ -54,7 +54,7 @@ export const bugCommand: SlashCommand = {
const kittyProtocol = terminalCapabilityManager.isKittyProtocolEnabled()
? 'Supported'
: 'Unsupported';
const authType = config?.getContentGeneratorConfig()?.authType || 'Unknown';
const authType = config?.getContentGeneratorConfig().authType || 'Unknown';
let info = `
* **CLI Version:** ${cliVersion}
@@ -73,13 +73,13 @@ export const bugCommand: SlashCommand = {
info += `* **IDE Client:** ${ideClient}\n`;
}
const chat = config?.getGeminiClient()?.getChat();
const chat = config?.getGeminiClient().getChat();
const history = chat?.getHistory() || [];
let historyFileMessage = '';
let problemValue = bugDescription;
if (history.length > INITIAL_HISTORY_LENGTH) {
const tempDir = config?.storage?.getProjectTempDir();
const tempDir = config?.storage.getProjectTempDir();
if (tempDir) {
const historyFileName = `bug-report-history-${Date.now()}.json`;
const historyFilePath = path.join(tempDir, historyFileName);
+5 -5
View File
@@ -34,7 +34,7 @@ const getSavedChatTags = async (
mtSortDesc: boolean,
): Promise<ChatDetail[]> => {
const cfg = context.services.config;
const geminiDir = cfg?.storage?.getProjectTempDir();
const geminiDir = cfg?.storage.getProjectTempDir();
if (!geminiDir) {
return [];
}
@@ -123,7 +123,7 @@ const saveCommand: SlashCommand = {
}
}
const chat = config?.getGeminiClient()?.getChat();
const chat = config?.getGeminiClient().getChat();
if (!chat) {
return {
type: 'message',
@@ -134,7 +134,7 @@ const saveCommand: SlashCommand = {
const history = chat.getHistory();
if (history.length > INITIAL_HISTORY_LENGTH) {
const authType = config?.getContentGeneratorConfig()?.authType;
const authType = config?.getContentGeneratorConfig().authType;
await logger.saveCheckpoint({ history, authType }, tag);
return {
type: 'message',
@@ -183,7 +183,7 @@ const resumeCommand: SlashCommand = {
};
}
const currentAuthType = config?.getContentGeneratorConfig()?.authType;
const currentAuthType = config?.getContentGeneratorConfig().authType;
if (
checkpoint.authType &&
currentAuthType &&
@@ -296,7 +296,7 @@ const shareCommand: SlashCommand = {
};
}
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.config?.getGeminiClient().getChat();
if (!chat) {
return {
type: 'message',
+1 -1
View File
@@ -25,7 +25,7 @@ export const clearCommand: SlashCommand = {
const config = context.services.config;
const chatRecordingService = context.services.config
?.getGeminiClient()
?.getChat()
.getChat()
.getChatRecordingService();
// Fire SessionEnd hook before clearing
@@ -43,7 +43,7 @@ export const compressCommand: SlashCommand = {
const promptId = `compress-${Date.now()}`;
const compressed = await context.services.config
?.getGeminiClient()
?.tryCompressChat(promptId, true);
.tryCompressChat(promptId, true);
if (compressed) {
ui.addItem(
{
+1 -1
View File
@@ -15,7 +15,7 @@ export const copyCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.config?.getGeminiClient().getChat();
const history = chat?.getHistory();
// Get the last message from the AI (model role)
@@ -236,7 +236,7 @@ async function restartAction(
if (failures.length < extensionsToRestart.length) {
try {
await context.services.config?.reloadSkills();
await context.services.config?.getAgentRegistry()?.reload();
await context.services.config?.getAgentRegistry().reload();
} catch (error) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -271,7 +271,7 @@ async function exploreAction(
context: CommandContext,
): Promise<SlashCommandActionReturn | void> {
const settings = context.services.settings.merged;
const useRegistryUI = settings.experimental?.extensionRegistry;
const useRegistryUI = settings.experimental.extensionRegistry;
if (useRegistryUI) {
const extensionManager = context.services.config?.getExtensionLoader();
+2 -2
View File
@@ -45,7 +45,7 @@ function getIdeStatusMessage(ideClient: IdeClient): {
};
default: {
let content = `🔴 Disconnected`;
if (connection?.details) {
if (connection.details) {
content += `: ${connection.details}`;
}
return {
@@ -107,7 +107,7 @@ async function getIdeStatusMessageWithFiles(ideClient: IdeClient): Promise<{
};
default: {
let content = `🔴 Disconnected`;
if (connection?.details) {
if (connection.details) {
content += `: ${connection.details}`;
}
return {
+6 -6
View File
@@ -139,7 +139,7 @@ const authCommand: SlashCommand = {
}
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
if (geminiClient?.isInitialized()) {
if (geminiClient.isInitialized()) {
await geminiClient.setTools();
}
@@ -361,7 +361,7 @@ const refreshCommand: SlashCommand = {
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
if (geminiClient?.isInitialized()) {
if (geminiClient.isInitialized()) {
await geminiClient.setTools();
}
@@ -419,9 +419,9 @@ async function handleEnableDisable(
if (enable) {
const settings = loadSettings();
const result = await canLoadServer(name, {
adminMcpEnabled: settings.merged.admin?.mcp?.enabled ?? true,
allowedList: settings.merged.mcp?.allowed,
excludedList: settings.merged.mcp?.excluded,
adminMcpEnabled: settings.merged.admin.mcp.enabled ?? true,
allowedList: settings.merged.mcp.allowed,
excludedList: settings.merged.mcp.excluded,
});
if (
!result.allowed &&
@@ -465,7 +465,7 @@ async function handleEnableDisable(
);
await mcpClientManager.restart();
}
if (config.getGeminiClient()?.isInitialized())
if (config.getGeminiClient().isInitialized())
await config.getGeminiClient().setTools();
context.ui.reloadCommands();
@@ -116,7 +116,7 @@ async function restoreAction(
} else if (action.type === 'load_history' && loadHistory) {
loadHistory(action.history);
if (action.clientHistory) {
config?.getGeminiClient()?.setHistory(action.clientHistory);
config?.getGeminiClient().setHistory(action.clientHistory);
}
}
}
@@ -227,7 +227,7 @@ export const setupGithubCommand: SlashCommand = {
}
// Get the latest release tag from GitHub
const proxy = context?.services?.config?.getProxy();
const proxy = context.services.config?.getProxy();
const releaseTag = await getLatestGitHubRelease(proxy);
const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`;
+1 -1
View File
@@ -61,7 +61,7 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
<Text color={theme.text.primary}>{cliVersion}</Text>
</Box>
</Box>
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
{!['N/A'].includes(GIT_COMMIT_INFO) && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
@@ -161,25 +161,25 @@ function getFieldDefaultFromDefinition(
return !definition.experimental; // Experimental agents default to disabled
}
if (field.key === 'model') {
return definition.modelConfig?.model ?? 'inherit';
return definition.modelConfig.model ?? 'inherit';
}
if (field.key === 'temperature') {
return definition.modelConfig?.generateContentConfig?.temperature;
return definition.modelConfig.generateContentConfig?.temperature;
}
if (field.key === 'topP') {
return definition.modelConfig?.generateContentConfig?.topP;
return definition.modelConfig.generateContentConfig?.topP;
}
if (field.key === 'topK') {
return definition.modelConfig?.generateContentConfig?.topK;
return definition.modelConfig.generateContentConfig?.topK;
}
if (field.key === 'maxOutputTokens') {
return definition.modelConfig?.generateContentConfig?.maxOutputTokens;
return definition.modelConfig.generateContentConfig?.maxOutputTokens;
}
if (field.key === 'maxTimeMinutes') {
return definition.runConfig?.maxTimeMinutes;
return definition.runConfig.maxTimeMinutes;
}
if (field.key === 'maxTurns') {
return definition.runConfig?.maxTurns;
return definition.runConfig.maxTurns;
}
return field.defaultValue;
@@ -1083,7 +1083,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
const currentQuestion = questions[currentQuestionIndex];
const effectiveQuestion = useMemo(() => {
if (currentQuestion?.type === 'yesno') {
if (currentQuestion.type === 'yesno') {
return {
...currentQuestion,
options: [
@@ -13,7 +13,7 @@ export type SpinnerProps = ComponentProps<typeof Spinner>;
export const CliSpinner = (props: SpinnerProps) => {
const settings = useSettings();
const shouldShow = settings.merged.ui?.showSpinner !== false;
const shouldShow = settings.merged.ui.showSpinner !== false;
useEffect(() => {
if (shouldShow) {
+1 -1
View File
@@ -79,7 +79,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
hasPendingToolConfirmation ||
Boolean(uiState.commandConfirmationRequest) ||
Boolean(uiState.authConsentRequest) ||
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
(uiState.confirmUpdateExtensionRequests.length ?? 0) > 0 ||
Boolean(uiState.loopDetectionConfirmationRequest) ||
Boolean(uiState.quota.proQuotaRequest) ||
Boolean(uiState.quota.validationRequest) ||
@@ -40,7 +40,7 @@ export const DetailedMessagesDisplay: React.FC<
if (textWidth <= 0) {
return 1;
}
const lines = Math.ceil((msg.content?.length || 1) / textWidth);
const lines = Math.ceil((msg.content.length || 1) / textWidth);
return Math.max(1, lines);
},
[width, messages],
@@ -21,8 +21,7 @@ import { ProQuotaDialog } from './ProQuotaDialog.js';
import { ValidationDialog } from './ValidationDialog.js';
import { OverageMenuDialog } from './OverageMenuDialog.js';
import { EmptyWalletDialog } from './EmptyWalletDialog.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
import { relaunchApp } from '../../utils/processUtils.js';
import { SessionBrowser } from './SessionBrowser.js';
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
import { ModelDialog } from './ModelDialog.js';
@@ -231,10 +230,7 @@ export const DialogManager = ({
<Box flexDirection="column">
<SettingsDialog
onSelect={() => uiActions.closeSettingsDialog()}
onRestartRequest={async () => {
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
}}
onRestartRequest={relaunchApp}
availableTerminalHeight={terminalHeight - staticExtraHeight}
/>
</Box>
@@ -259,7 +255,7 @@ export const DialogManager = ({
onClose={uiActions.closeAgentConfigDialog}
onSave={async () => {
// Reload agent registry to pick up changes
const agentRegistry = config?.getAgentRegistry();
const agentRegistry = config.getAgentRegistry();
if (agentRegistry) {
await agentRegistry.reload();
}
@@ -246,7 +246,9 @@ describe('FolderTrustDialog', () => {
it('should call relaunchApp when isRestarting is true', async () => {
vi.useFakeTimers();
const relaunchApp = vi.spyOn(processUtils, 'relaunchApp');
const relaunchApp = vi
.spyOn(processUtils, 'relaunchApp')
.mockResolvedValue(undefined);
const { waitUntilReady, unmount } = renderWithProviders(
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
);
@@ -259,7 +261,9 @@ describe('FolderTrustDialog', () => {
it('should not call relaunchApp if unmounted before timeout', async () => {
vi.useFakeTimers();
const relaunchApp = vi.spyOn(processUtils, 'relaunchApp');
const relaunchApp = vi
.spyOn(processUtils, 'relaunchApp')
.mockResolvedValue(undefined);
const { waitUntilReady, unmount } = renderWithProviders(
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
);
@@ -54,9 +54,7 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (isRestarting) {
timer = setTimeout(async () => {
await relaunchApp();
}, 250);
timer = setTimeout(relaunchApp, 250);
}
return () => {
if (timer) clearTimeout(timer);
@@ -62,7 +62,9 @@ describe('IdeTrustChangeDialog', () => {
});
it('calls relaunchApp when "r" is pressed', async () => {
const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp');
const relaunchAppSpy = vi
.spyOn(processUtils, 'relaunchApp')
.mockResolvedValue(undefined);
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeTrustChangeDialog reason="NONE" />,
);
@@ -78,7 +80,9 @@ describe('IdeTrustChangeDialog', () => {
});
it('calls relaunchApp when "R" is pressed', async () => {
const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp');
const relaunchAppSpy = vi
.spyOn(processUtils, 'relaunchApp')
.mockResolvedValue(undefined);
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
);
@@ -94,7 +98,9 @@ describe('IdeTrustChangeDialog', () => {
});
it('does not call relaunchApp when another key is pressed', async () => {
const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp');
const relaunchAppSpy = vi
.spyOn(processUtils, 'relaunchApp')
.mockResolvedValue(undefined);
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
);
@@ -279,6 +279,9 @@ describe('InputPrompt', () => {
},
getCompletedText: vi.fn().mockReturnValue(null),
completionMode: CompletionMode.IDLE,
forceShowShellSuggestions: false,
setForceShowShellSuggestions: vi.fn(),
isShellSuggestionsVisible: true,
};
mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion);
+60 -33
View File
@@ -148,7 +148,7 @@ const DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS = 350;
* Returns true if a toggle action was performed or hint was shown, false otherwise.
*/
export function tryTogglePasteExpansion(buffer: TextBuffer): boolean {
if (!buffer.pastedContent || Object.keys(buffer.pastedContent).length === 0) {
if (Object.keys(buffer.pastedContent).length === 0) {
return false;
}
@@ -301,6 +301,27 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const resetCommandSearchCompletionState =
commandSearchCompletion.resetCompletionState;
const getActiveCompletion = useCallback(() => {
if (commandSearchActive) return commandSearchCompletion;
if (reverseSearchActive) return reverseSearchCompletion;
return completion;
}, [
commandSearchActive,
commandSearchCompletion,
reverseSearchActive,
reverseSearchCompletion,
completion,
]);
const activeCompletion = getActiveCompletion();
const shouldShowSuggestions = activeCompletion.showSuggestions;
const {
forceShowShellSuggestions,
setForceShowShellSuggestions,
isShellSuggestionsVisible,
} = completion;
const showCursor = focus && isShellFocused && !isEmbeddedShellFocused;
// Notify parent component about escape prompt state changes
@@ -323,13 +344,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const handleSubmitAndClear = useCallback(
(submittedValue: string) => {
let processedValue = submittedValue;
if (buffer.pastedContent) {
// Replace placeholders like [Pasted Text: 6 lines] with actual content
processedValue = processedValue.replace(
PASTED_TEXT_PLACEHOLDER_REGEX,
(match) => buffer.pastedContent[match] || match,
);
}
// Replace placeholders like [Pasted Text: 6 lines] with actual content
processedValue = processedValue.replace(
PASTED_TEXT_PLACEHOLDER_REGEX,
(match) => buffer.pastedContent[match] || match,
);
if (shellModeActive) {
shellHistory.addCommandToHistory(processedValue);
@@ -363,7 +382,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
userMessages,
onSubmit: handleSubmitAndClear,
isActive:
(!completion.showSuggestions || completion.suggestions.length === 1) &&
(!(completion.showSuggestions && isShellSuggestionsVisible) ||
completion.suggestions.length === 1) &&
!shellModeActive,
currentQuery: buffer.text,
currentCursorOffset: buffer.getOffset(),
@@ -465,7 +485,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}
}
if (settings.experimental?.useOSC52Paste) {
if (settings.experimental.useOSC52Paste) {
stdout.write('\x1b]52;c;?\x07');
} else {
const textToInsert = await clipboardy.read();
@@ -595,9 +615,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
keyMatchers[Command.END](key);
const isSuggestionsNav =
(completion.showSuggestions ||
reverseSearchCompletion.showSuggestions ||
commandSearchCompletion.showSuggestions) &&
shouldShowSuggestions &&
(keyMatchers[Command.COMPLETION_UP](key) ||
keyMatchers[Command.COMPLETION_DOWN](key) ||
keyMatchers[Command.EXPAND_SUGGESTION](key) ||
@@ -612,6 +630,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
isHistoryNav || isCursorMovement || keyMatchers[Command.ESCAPE](key),
);
hasUserNavigatedSuggestions.current = false;
if (key.name !== 'tab') {
setForceShowShellSuggestions(false);
}
}
// TODO(jacobr): this special case is likely not needed anymore.
@@ -641,15 +663,25 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const isPlainTab =
key.name === 'tab' && !key.shift && !key.alt && !key.ctrl && !key.cmd;
const hasTabCompletionInteraction =
completion.showSuggestions ||
(completion.showSuggestions && isShellSuggestionsVisible) ||
Boolean(completion.promptCompletion.text) ||
reverseSearchActive ||
commandSearchActive;
if (isPlainTab && shellModeActive) {
resetPlainTabPress();
if (!completion.showSuggestions) {
if (!shouldShowSuggestions) {
setSuppressCompletion(false);
if (completion.promptCompletion.text) {
completion.promptCompletion.accept();
return true;
} else if (
completion.suggestions.length > 0 &&
!forceShowShellSuggestions
) {
setForceShowShellSuggestions(true);
return true;
}
}
} else if (isPlainTab) {
if (!hasTabCompletionInteraction) {
@@ -752,7 +784,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
if (
key.sequence === '!' &&
buffer.text === '' &&
!completion.showSuggestions
!(completion.showSuggestions && isShellSuggestionsVisible)
) {
setShellModeActive(!shellModeActive);
buffer.setText(''); // Clear the '!' from input
@@ -791,15 +823,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return true;
}
if (shellModeActive) {
setShellModeActive(false);
if (completion.showSuggestions && isShellSuggestionsVisible) {
completion.resetCompletionState();
setExpandedSuggestionIndex(-1);
resetEscapeState();
return true;
}
if (completion.showSuggestions) {
completion.resetCompletionState();
setExpandedSuggestionIndex(-1);
if (shellModeActive) {
setShellModeActive(false);
resetEscapeState();
return true;
}
@@ -895,7 +927,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
completion.isPerfectMatch &&
keyMatchers[Command.SUBMIT](key) &&
recentUnsafePasteTime === null &&
(!completion.showSuggestions ||
(!(completion.showSuggestions && isShellSuggestionsVisible) ||
(completion.activeSuggestionIndex <= 0 &&
!hasUserNavigatedSuggestions.current))
) {
@@ -909,7 +941,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return true;
}
if (completion.showSuggestions) {
if (completion.showSuggestions && isShellSuggestionsVisible) {
if (completion.suggestions.length > 1) {
if (keyMatchers[Command.COMPLETION_UP](key)) {
completion.navigateUp();
@@ -1007,7 +1039,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
if (
key.name === 'tab' &&
!key.shift &&
!completion.showSuggestions &&
!(completion.showSuggestions && isShellSuggestionsVisible) &&
completion.promptCompletion.text
) {
completion.promptCompletion.accept();
@@ -1190,6 +1222,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
focus,
buffer,
completion,
setForceShowShellSuggestions,
shellModeActive,
setShellModeActive,
onClearScreen,
@@ -1221,6 +1254,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
registerPlainTabPress,
resetPlainTabPress,
toggleCleanUiDetailsVisible,
shouldShowSuggestions,
isShellSuggestionsVisible,
forceShowShellSuggestions,
],
);
@@ -1346,14 +1382,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
]);
const { inlineGhost, additionalLines } = getGhostTextLines();
const getActiveCompletion = () => {
if (commandSearchActive) return commandSearchCompletion;
if (reverseSearchActive) return reverseSearchCompletion;
return completion;
};
const activeCompletion = getActiveCompletion();
const shouldShowSuggestions = activeCompletion.showSuggestions;
const useBackgroundColor = config.getUseBackgroundColor();
const isLowColor = isLowColorDepth();
@@ -1507,7 +1535,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const absoluteVisualIdx =
scrollVisualRow + visualIdxInRenderedSet;
const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx];
if (!mapEntry) return null;
const cursorVisualRow =
cursorVisualRowAbsolute - scrollVisualRow;
@@ -57,7 +57,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
: currentLoadingPhrase;
const hasThoughtIndicator =
currentLoadingPhrase !== INTERACTIVE_SHELL_WAITING_PHRASE &&
Boolean(thought?.subject?.trim());
Boolean(thought?.subject.trim());
const thinkingIndicator = hasThoughtIndicator ? '💬 ' : '';
const cancelAndTimerContent =
@@ -118,7 +118,7 @@ export const MainContent = () => {
isExpandable={true}
/>
))}
{showConfirmationQueue && confirmingTool && (
{showConfirmationQueue && (
<ToolConfirmationQueue confirmingTool={confirmingTool} />
)}
</Box>
@@ -42,7 +42,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
const useGemini31 = config?.getGemini31LaunchedSync() ?? false;
const selectedAuthType = settings.merged.security.auth.selectedType;
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
@@ -254,7 +254,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
isSubtle: true,
};
activeModels.forEach(([name, metrics]) => {
const roleMetrics = metrics.roles?.[role];
const roleMetrics = metrics.roles[role];
if (roleMetrics) {
row[name] = getValue(roleMetrics);
} else {
@@ -201,7 +201,7 @@ const findTextMatches = (
const lowerContent = m.toLowerCase();
let startIndex = 0;
while (true) {
for (;;) {
const matchIndex = lowerContent.indexOf(lowerQuery, startIndex);
if (matchIndex === -1) break;
@@ -416,7 +416,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
const computed = computeSessionStats(metrics);
const settings = useSettings();
const config = useConfig();
const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini3_1 = config.getGemini31LaunchedSync() ?? false;
const useCustomToolModel =
useGemini3_1 &&
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
@@ -63,8 +63,8 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
: Math.floor(terminalHeight * 0.5);
const isRoutine =
tool.confirmationDetails?.type === 'ask_user' ||
tool.confirmationDetails?.type === 'exit_plan_mode';
tool.confirmationDetails.type === 'ask_user' ||
tool.confirmationDetails.type === 'exit_plan_mode';
const borderColor = isRoutine ? theme.status.success : theme.status.warning;
const hideToolIdentity = isRoutine;
@@ -404,7 +404,7 @@ export function BaseSettingsDialog({
}
// Number keys for quick edit on number fields
if (currentItem?.type === 'number' && /^[0-9]$/.test(key.sequence)) {
if (currentItem.type === 'number' && /^[0-9]$/.test(key.sequence)) {
startEditing(currentItem.key, key.sequence);
return true;
}
@@ -9,9 +9,19 @@ import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { MaxSizedBox } from './MaxSizedBox.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { Box, Text } from 'ink';
import { describe, it, expect } from 'vitest';
import { act } from 'react';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
describe('<MaxSizedBox />', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('renders children without truncation when they fit', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
<OverflowProvider>
@@ -22,6 +32,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain('Hello, World!');
expect(lastFrame()).toMatchSnapshot();
@@ -40,6 +53,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... first 2 lines hidden (Ctrl+O to show) ...',
@@ -60,6 +76,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... last 2 lines hidden (Ctrl+O to show) ...',
@@ -80,6 +99,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... first 2 lines hidden (Ctrl+O to show) ...',
@@ -98,6 +120,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... first 1 line hidden (Ctrl+O to show) ...',
@@ -118,6 +143,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... first 7 lines hidden (Ctrl+O to show) ...',
@@ -137,6 +165,9 @@ describe('<MaxSizedBox />', () => {
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain('This is a');
expect(lastFrame()).toMatchSnapshot();
@@ -154,6 +185,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain('Line 1');
expect(lastFrame()).toMatchSnapshot();
@@ -166,6 +200,9 @@ describe('<MaxSizedBox />', () => {
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })?.trim()).equals('');
unmount();
@@ -185,6 +222,9 @@ describe('<MaxSizedBox />', () => {
</MaxSizedBox>
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain('Line 1 from Fragment');
expect(lastFrame()).toMatchSnapshot();
@@ -206,6 +246,9 @@ describe('<MaxSizedBox />', () => {
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... first 21 lines hidden (Ctrl+O to show) ...',
@@ -229,6 +272,9 @@ describe('<MaxSizedBox />', () => {
</OverflowProvider>,
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain(
'... last 21 lines hidden (Ctrl+O to show) ...',
@@ -253,6 +299,9 @@ describe('<MaxSizedBox />', () => {
{ width: 80 },
);
await act(async () => {
vi.runAllTimers();
});
await waitUntilReady();
expect(lastFrame()).toContain('... last');
@@ -96,12 +96,15 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
} else {
removeOverflowingId?.(id);
}
return () => {
removeOverflowingId?.(id);
};
}, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
useEffect(
() => () => {
removeOverflowingId?.(id);
},
[id, removeOverflowingId],
);
if (effectiveMaxHeight === undefined) {
return (
<Box flexDirection="column" width={maxWidth}>
@@ -2041,7 +2041,7 @@ function textBufferReducerLogic(
if (visualToLogicalMap[newVisualRow]) {
const [logRow, logicalStartCol] = visualToLogicalMap[newVisualRow];
const transformedToLogicalMap =
visualLayout.transformedToLogicalMaps?.[logRow] ?? [];
visualLayout.transformedToLogicalMaps[logRow] ?? [];
let transformedStartCol = 0;
while (
transformedStartCol < transformedToLogicalMap.length &&
@@ -3324,12 +3324,11 @@ export function useTextBuffer({
if (visualToLogicalMap[clampedVisRow]) {
const [logRow] = visualToLogicalMap[clampedVisRow];
const transformedToLogicalMap =
transformedToLogicalMaps?.[logRow] ?? [];
const transformedToLogicalMap = transformedToLogicalMaps[logRow] ?? [];
// Where does this visual line begin within the transformed line?
const startColInTransformed =
visualToTransformedMap?.[clampedVisRow] ?? 0;
visualToTransformedMap[clampedVisRow] ?? 0;
// Handle wide characters: convert visual X position to character offset
const codePoints = toCodePoints(visualLine);
@@ -3397,11 +3396,10 @@ export function useTextBuffer({
}
const [logRow] = visualToLogicalMap[clampedVisRow];
const transformedToLogicalMap = transformedToLogicalMaps?.[logRow] ?? [];
const transformedToLogicalMap = transformedToLogicalMaps[logRow] ?? [];
// Where does this visual line begin within the transformed line?
const startColInTransformed =
visualToTransformedMap?.[clampedVisRow] ?? 0;
const startColInTransformed = visualToTransformedMap[clampedVisRow] ?? 0;
// Handle wide characters: convert visual X position to character offset
const codePoints = toCodePoints(visualLine);
@@ -208,7 +208,7 @@ I am triaging a GitHub issue labeled as 'possible-duplicate'. I need to decide i
<target_issue>
ID: #${issue.number}
Title: ${issue.title}
Author: ${issue.author?.login}
Author: ${issue.author.login}
Reactions: ${getReactionCount(issue)}
Body:
${issue.body.slice(0, 8000)}
@@ -221,7 +221,7 @@ ${candidates
<candidate>
ID: #${c.number}
Title: ${c.title}
Author: ${c.author?.login}
Author: ${c.author.login}
Reactions: ${getReactionCount(c)}
Body:
${c.body.slice(0, 4000)}
@@ -815,7 +815,7 @@ Return a JSON object with:
</Box>
<Text bold>{selectedCandidate.title}</Text>
<Text color="gray">
Author: {selectedCandidate.author?.login} | 👍{' '}
Author: {selectedCandidate.author.login} | 👍{' '}
{getReactionCount(selectedCandidate)}
</Text>
<Text color="gray">{selectedCandidate.url}</Text>
@@ -874,7 +874,7 @@ Return a JSON object with:
- {currentIssue.title}
</Text>
<Text color="gray">
Author: {currentIssue.author?.login} | 👍{' '}
Author: {currentIssue.author.login} | 👍{' '}
{getReactionCount(currentIssue)}
</Text>
</Box>
@@ -183,7 +183,7 @@ I am triaging GitHub issues for the Gemini CLI project. I need to identify issue
<issue>
ID: #${issue.number}
Title: ${issue.title}
Author: ${issue.author?.login}
Author: ${issue.author.login}
Labels: ${issue.labels.map((l) => l.name).join(', ')}
Body:
${issue.body.slice(0, 8000)}
@@ -566,7 +566,7 @@ Return a JSON object with:
- {currentIssue.title}
</Text>
<Text color="gray">
Author: {currentIssue.author?.login} | 👍{' '}
Author: {currentIssue.author.login} | 👍{' '}
{getReactionCount(currentIssue)}
</Text>
</Box>
@@ -214,7 +214,7 @@ function bufferBackslashEnter(
keypressHandler: KeypressHandler,
): KeypressHandler {
const bufferer = (function* (): Generator<void, void, Key | null> {
while (true) {
for (;;) {
const key = yield;
if (key == null) {
@@ -260,7 +260,7 @@ function bufferBackslashEnter(
*/
function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler {
const bufferer = (function* (): Generator<void, void, Key | null> {
while (true) {
for (;;) {
let key = yield;
if (key === null) {
@@ -271,7 +271,7 @@ function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler {
}
let buffer = '';
while (true) {
for (;;) {
const timeoutId = setTimeout(() => bufferer.next(null), PASTE_TIMEOUT);
key = yield;
clearTimeout(timeoutId);
@@ -340,7 +340,7 @@ function* emitKeys(
const lcAll = process.env['LC_ALL'] || '';
const isGreek = lang.startsWith('el') || lcAll.startsWith('el');
while (true) {
for (;;) {
let ch = yield;
let sequence = ch;
let escaped = false;
@@ -376,7 +376,7 @@ function* emitKeys(
let buffer = '';
// Read until BEL, `ESC \`, or timeout (empty string)
while (true) {
for (;;) {
const next = yield;
if (next === '' || next === '\u0007') {
break;
@@ -11,6 +11,8 @@ import {
useState,
useCallback,
useMemo,
useRef,
useEffect,
} from 'react';
export interface OverflowState {
@@ -42,31 +44,70 @@ export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
}) => {
const [overflowingIds, setOverflowingIds] = useState(new Set<string>());
const addOverflowingId = useCallback((id: string) => {
setOverflowingIds((prevIds) => {
if (prevIds.has(id)) {
return prevIds;
}
const newIds = new Set(prevIds);
newIds.add(id);
return newIds;
});
/**
* We use a ref to track the current set of overflowing IDs and a timeout to
* batch updates to the next tick. This prevents infinite render loops (layout
* oscillation) where showing an overflow hint causes a layout shift that
* hides the hint, which then restores the layout and shows the hint again.
*/
const idsRef = useRef(new Set<string>());
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const syncState = useCallback(() => {
if (timeoutRef.current) return;
// Use a microtask to batch updates and break synchronous recursive loops.
// This prevents "Maximum update depth exceeded" errors during layout shifts.
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
setOverflowingIds((prevIds) => {
// Optimization: only update state if the set has actually changed
if (
prevIds.size === idsRef.current.size &&
[...prevIds].every((id) => idsRef.current.has(id))
) {
return prevIds;
}
return new Set(idsRef.current);
});
}, 0);
}, []);
const removeOverflowingId = useCallback((id: string) => {
setOverflowingIds((prevIds) => {
if (!prevIds.has(id)) {
return prevIds;
useEffect(
() => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const newIds = new Set(prevIds);
newIds.delete(id);
return newIds;
});
}, []);
},
[],
);
const addOverflowingId = useCallback(
(id: string) => {
if (!idsRef.current.has(id)) {
idsRef.current.add(id);
syncState();
}
},
[syncState],
);
const removeOverflowingId = useCallback(
(id: string) => {
if (idsRef.current.has(id)) {
idsRef.current.delete(id);
syncState();
}
},
[syncState],
);
const reset = useCallback(() => {
setOverflowingIds(new Set());
}, []);
if (idsRef.current.size > 0) {
idsRef.current.clear();
syncState();
}
}, [syncState]);
const stateValue = useMemo(
() => ({
@@ -113,7 +113,7 @@ describe('SettingsContext', () => {
it('should trigger re-renders when settings change (external event)', () => {
const { result } = renderHook(() => useSettingsStore(), { wrapper });
expect(result.current.settings.merged.ui?.theme).toBe('default-theme');
expect(result.current.settings.merged.ui.theme).toBe('default-theme');
const newSnapshot = {
...mockSnapshot,
@@ -128,7 +128,7 @@ describe('SettingsContext', () => {
listeners.forEach((l) => l());
});
expect(result.current.settings.merged.ui?.theme).toBe('new-theme');
expect(result.current.settings.merged.ui.theme).toBe('new-theme');
});
it('should call store.setValue when setSetting is called', () => {
@@ -119,7 +119,7 @@ function categorizeAtCommands(
const resourceParts: AtCommandPart[] = [];
const fileParts: AtCommandPart[] = [];
const agentRegistry = config.getAgentRegistry?.();
const agentRegistry = config.getAgentRegistry();
const resourceRegistry = config.getResourceRegistry();
for (const part of commandParts) {
@@ -129,7 +129,7 @@ function categorizeAtCommands(
const name = part.content.substring(1);
if (agentRegistry?.getDefinition(name)) {
if (agentRegistry.getDefinition(name)) {
agentParts.push(part);
} else if (resourceRegistry.findResourceByUri(name)) {
resourceParts.push(part);
@@ -273,10 +273,6 @@ export const useSlashCommandProcessor = (
);
useEffect(() => {
if (!config) {
return;
}
const listener = () => {
reloadCommands();
};
@@ -535,7 +531,7 @@ export const useSlashCommandProcessor = (
}
}
case 'load_history': {
config?.getGeminiClient()?.setHistory(result.clientHistory);
config?.getGeminiClient().setHistory(result.clientHistory);
fullCommandContext.ui.clear();
result.history.forEach((item, index) => {
fullCommandContext.ui.addItem(item, index);
@@ -688,7 +684,7 @@ export const useSlashCommandProcessor = (
command: resolvedCommandPath[0],
subcommand,
status: SlashCommandStatus.ERROR,
extension_id: commandToExecute?.extensionId,
extension_id: commandToExecute.extensionId,
});
logSlashCommand(config, event);
}
@@ -706,7 +702,7 @@ export const useSlashCommandProcessor = (
command: resolvedCommandPath[0],
subcommand,
status: SlashCommandStatus.SUCCESS,
extension_id: commandToExecute?.extensionId,
extension_id: commandToExecute.extensionId,
});
logSlashCommand(config, event);
}
+11 -11
View File
@@ -115,7 +115,7 @@ interface ResourceSuggestionCandidate {
function buildResourceCandidates(
config?: Config,
): ResourceSuggestionCandidate[] {
const registry = config?.getResourceRegistry?.();
const registry = config?.getResourceRegistry();
if (!registry) {
return [];
}
@@ -137,7 +137,7 @@ function buildResourceCandidates(
}
function buildAgentCandidates(config?: Config): Suggestion[] {
const registry = config?.getAgentRegistry?.();
const registry = config?.getAgentRegistry();
if (!registry) {
return [];
}
@@ -232,7 +232,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
}, [cwd, config]);
useEffect(() => {
const workspaceContext = config?.getWorkspaceContext?.();
const workspaceContext = config?.getWorkspaceContext();
if (!workspaceContext) return;
const unsubscribe =
@@ -274,9 +274,9 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
const initialize = async () => {
const currentEpoch = initEpoch.current;
try {
const directories = config
?.getWorkspaceContext?.()
?.getDirectories() ?? [cwd];
const directories = config?.getWorkspaceContext().getDirectories() ?? [
cwd,
];
const initPromises: Array<Promise<void>> = [];
@@ -296,7 +296,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
config?.getEnableRecursiveFileSearch() ?? true,
enableFuzzySearch:
config?.getFileFilteringEnableFuzzySearch() ?? true,
maxFiles: config?.getFileFilteringOptions()?.maxFileCount,
maxFiles: config?.getFileFilteringOptions().maxFileCount,
});
initPromises.push(
@@ -342,7 +342,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
}, 200);
const timeoutMs =
config?.getFileFilteringOptions()?.searchTimeout ??
config?.getFileFilteringOptions().searchTimeout ??
DEFAULT_SEARCH_TIMEOUT_MS;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -358,9 +358,9 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
})();
try {
const directories = config
?.getWorkspaceContext?.()
?.getDirectories() ?? [cwd];
const directories = config?.getWorkspaceContext().getDirectories() ?? [
cwd,
];
const cwdRealpath = directories[0];
const allSearchPromises = [...fileSearchMap.current.entries()].map(
@@ -46,6 +46,7 @@ vi.mock('./useShellCompletion', () => ({
completionStart: 0,
completionEnd: 0,
query: '',
activeStart: 0,
})),
}));
@@ -57,7 +58,12 @@ const setupMocks = ({
isLoading = false,
isPerfectMatch = false,
slashCompletionRange = { completionStart: 0, completionEnd: 0 },
shellCompletionRange = { completionStart: 0, completionEnd: 0, query: '' },
shellCompletionRange = {
completionStart: 0,
completionEnd: 0,
query: '',
activeStart: 0,
},
}: {
atSuggestions?: Suggestion[];
slashSuggestions?: Suggestion[];
@@ -69,6 +75,7 @@ const setupMocks = ({
completionStart: number;
completionEnd: number;
query: string;
activeStart?: number;
};
}) => {
// Mock for @-completions
@@ -116,7 +123,10 @@ const setupMocks = ({
setSuggestions(shellSuggestions);
}
}, [enabled, setSuggestions, setIsLoadingSuggestions]);
return shellCompletionRange;
return {
...shellCompletionRange,
activeStart: shellCompletionRange.activeStart ?? 0,
};
},
);
};
@@ -139,38 +149,57 @@ describe('useCommandCompletion', () => {
});
}
let hookResult: ReturnType<typeof useCommandCompletion> & {
textBuffer: ReturnType<typeof useTextBuffer>;
};
function TestComponent({
initialText,
cursorOffset,
shellModeActive,
active,
}: {
initialText: string;
cursorOffset?: number;
shellModeActive: boolean;
active: boolean;
}) {
const textBuffer = useTextBufferForTest(initialText, cursorOffset);
const completion = useCommandCompletion({
buffer: textBuffer,
cwd: testRootDir,
slashCommands: [],
commandContext: mockCommandContext,
reverseSearchActive: false,
shellModeActive,
config: mockConfig,
active,
});
hookResult = { ...completion, textBuffer };
return null;
}
const renderCommandCompletionHook = (
initialText: string,
cursorOffset?: number,
shellModeActive = false,
active = true,
) => {
let hookResult: ReturnType<typeof useCommandCompletion> & {
textBuffer: ReturnType<typeof useTextBuffer>;
};
function TestComponent() {
const textBuffer = useTextBufferForTest(initialText, cursorOffset);
const completion = useCommandCompletion({
buffer: textBuffer,
cwd: testRootDir,
slashCommands: [],
commandContext: mockCommandContext,
reverseSearchActive: false,
shellModeActive,
config: mockConfig,
active,
});
hookResult = { ...completion, textBuffer };
return null;
}
renderWithProviders(<TestComponent />);
const renderResult = renderWithProviders(
<TestComponent
initialText={initialText}
cursorOffset={cursorOffset}
shellModeActive={shellModeActive}
active={active}
/>,
);
return {
result: {
get current() {
return hookResult;
},
},
...renderResult,
};
};
@@ -524,6 +553,129 @@ describe('useCommandCompletion', () => {
expect(result.current.textBuffer.text).toBe('@src\\components\\');
});
it('should show ghost text for a single shell completion', async () => {
const text = 'l';
setupMocks({
shellSuggestions: [{ label: 'ls', value: 'ls' }],
shellCompletionRange: {
completionStart: 0,
completionEnd: 1,
query: 'l',
activeStart: 0,
},
});
const { result } = renderCommandCompletionHook(
text,
text.length,
true, // shellModeActive
);
await waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
// Should show "ls " as ghost text (including trailing space)
expect(result.current.promptCompletion.text).toBe('ls ');
});
it('should not show ghost text if there are multiple completions', async () => {
const text = 'l';
setupMocks({
shellSuggestions: [
{ label: 'ls', value: 'ls' },
{ label: 'ln', value: 'ln' },
],
shellCompletionRange: {
completionStart: 0,
completionEnd: 1,
query: 'l',
activeStart: 0,
},
});
const { result } = renderCommandCompletionHook(
text,
text.length,
true, // shellModeActive
);
await waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
expect(result.current.promptCompletion.text).toBe('');
});
it('should not show ghost text if the typed text extends past the completion', async () => {
// "ls " is already typed.
const text = 'ls ';
const cursorOffset = text.length;
const { result } = renderCommandCompletionHook(
text,
cursorOffset,
true, // shellModeActive
);
await waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
expect(result.current.promptCompletion.text).toBe('');
});
it('should clear ghost text after user types a space when exact match ghost text was showing', async () => {
const textWithoutSpace = 'ls';
setupMocks({
shellSuggestions: [{ label: 'ls', value: 'ls' }],
shellCompletionRange: {
completionStart: 0,
completionEnd: 2,
query: 'ls',
activeStart: 0,
},
});
const { result } = renderCommandCompletionHook(
textWithoutSpace,
textWithoutSpace.length,
true, // shellModeActive
);
await waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
// Initially no ghost text because "ls" perfectly matches "ls"
expect(result.current.promptCompletion.text).toBe('');
// Now simulate typing a space.
// In the real app, shellCompletionRange.completionStart would change immediately to 3,
// but suggestions (and activeStart) would still be from the previous token for a few ms.
setupMocks({
shellSuggestions: [{ label: 'ls', value: 'ls' }], // Stale suggestions
shellCompletionRange: {
completionStart: 3, // New token position
completionEnd: 3,
query: '',
activeStart: 0, // Stale active start
},
});
act(() => {
result.current.textBuffer.setText('ls ', 'end');
});
await waitFor(() => {
expect(result.current.isLoadingSuggestions).toBe(false);
});
// Should STILL be empty because completionStart (3) !== activeStart (0)
expect(result.current.promptCompletion.text).toBe('');
});
});
describe('prompt completion filtering', () => {
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useMemo, useEffect } from 'react';
import { useCallback, useMemo, useEffect, useState } from 'react';
import type { Suggestion } from '../components/SuggestionsDisplay.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import type { TextBuffer } from '../components/shared/text-buffer.js';
@@ -37,6 +37,9 @@ export interface UseCommandCompletionReturn {
showSuggestions: boolean;
isLoadingSuggestions: boolean;
isPerfectMatch: boolean;
forceShowShellSuggestions: boolean;
setForceShowShellSuggestions: (value: boolean) => void;
isShellSuggestionsVisible: boolean;
setActiveSuggestionIndex: React.Dispatch<React.SetStateAction<number>>;
resetCompletionState: () => void;
navigateUp: () => void;
@@ -80,6 +83,9 @@ export function useCommandCompletion({
config,
active,
}: UseCommandCompletionOptions): UseCommandCompletionReturn {
const [forceShowShellSuggestions, setForceShowShellSuggestions] =
useState(false);
const {
suggestions,
activeSuggestionIndex,
@@ -93,11 +99,16 @@ export function useCommandCompletion({
setIsPerfectMatch,
setVisibleStartIndex,
resetCompletionState,
resetCompletionState: baseResetCompletionState,
navigateUp,
navigateDown,
} = useCompletion();
const resetCompletionState = useCallback(() => {
baseResetCompletionState();
setForceShowShellSuggestions(false);
}, [baseResetCompletionState]);
const cursorRow = buffer.cursor[0];
const cursorCol = buffer.cursor[1];
@@ -231,10 +242,73 @@ export function useCommandCompletion({
? shellCompletionRange.query
: memoQuery;
const promptCompletion = usePromptCompletion({
const basePromptCompletion = usePromptCompletion({
buffer,
});
const isShellSuggestionsVisible =
completionMode !== CompletionMode.SHELL || forceShowShellSuggestions;
const promptCompletion = useMemo(() => {
if (
completionMode === CompletionMode.SHELL &&
suggestions.length === 1 &&
query != null &&
shellCompletionRange.completionStart === shellCompletionRange.activeStart
) {
const suggestion = suggestions[0];
const textToInsertBase = suggestion.value;
if (
textToInsertBase.startsWith(query) &&
textToInsertBase.length > query.length
) {
const currentLine = buffer.lines[cursorRow] || '';
const start = shellCompletionRange.completionStart;
const end = shellCompletionRange.completionEnd;
let textToInsert = textToInsertBase;
const charAfterCompletion = currentLine[end];
if (
charAfterCompletion !== ' ' &&
!textToInsert.endsWith('/') &&
!textToInsert.endsWith('\\')
) {
textToInsert += ' ';
}
const newText =
currentLine.substring(0, start) +
textToInsert +
currentLine.substring(end);
return {
text: newText,
isActive: true,
isLoading: false,
accept: () => {
buffer.replaceRangeByOffset(
logicalPosToOffset(buffer.lines, cursorRow, start),
logicalPosToOffset(buffer.lines, cursorRow, end),
textToInsert,
);
},
clear: () => {},
markSelected: () => {},
};
}
}
return basePromptCompletion;
}, [
completionMode,
suggestions,
query,
basePromptCompletion,
buffer,
cursorRow,
shellCompletionRange,
]);
useEffect(() => {
setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1);
setVisibleStartIndex(0);
@@ -271,6 +345,7 @@ export function useCommandCompletion({
active &&
completionMode !== CompletionMode.IDLE &&
!reverseSearchActive &&
isShellSuggestionsVisible &&
(isLoadingSuggestions || suggestions.length > 0);
/**
@@ -395,6 +470,9 @@ export function useCommandCompletion({
showSuggestions,
isLoadingSuggestions,
isPerfectMatch,
forceShowShellSuggestions,
setForceShowShellSuggestions,
isShellSuggestionsVisible,
setActiveSuggestionIndex,
resetCompletionState,
navigateUp,
+4 -4
View File
@@ -41,13 +41,13 @@ export const useFocus = (): {
};
// Enable focus reporting
stdout?.write(ENABLE_FOCUS_REPORTING);
stdin?.on('data', handleData);
stdout.write(ENABLE_FOCUS_REPORTING);
stdin.on('data', handleData);
return () => {
// Disable focus reporting on cleanup
stdout?.write(DISABLE_FOCUS_REPORTING);
stdin?.removeListener('data', handleData);
stdout.write(DISABLE_FOCUS_REPORTING);
stdin.removeListener('data', handleData);
};
}, [stdin, stdout]);
+6 -9
View File
@@ -35,7 +35,7 @@ export const useFolderTrust = (
const [isRestarting, setIsRestarting] = useState(false);
const startupMessageSent = useRef(false);
const folderTrust = settings.merged.security.folderTrust.enabled ?? true;
const folderTrust = settings.merged.security.folderTrust.enabled;
useEffect(() => {
let isMounted = true;
@@ -68,13 +68,11 @@ export const useFolderTrust = (
};
if (isHeadlessMode()) {
if (isMounted) {
setIsTrusted(trusted);
setIsFolderTrustDialogOpen(false);
onTrustChange(true);
showUntrustedMessage();
}
} else if (isMounted) {
setIsTrusted(trusted);
setIsFolderTrustDialogOpen(false);
onTrustChange(true);
showUntrustedMessage();
} else {
setIsTrusted(trusted);
setIsFolderTrustDialogOpen(trusted === undefined);
onTrustChange(trusted);
@@ -95,7 +93,6 @@ export const useFolderTrust = (
};
const trustLevel = trustLevelMap[choice];
if (!trustLevel) return;
const cwd = process.cwd();
const trustedFolders = loadTrustedFolders();
+7 -7
View File
@@ -968,7 +968,7 @@ export const useGeminiStream = (
type: MessageType.ERROR,
text: parseAndFormatApiError(
eventValue.error,
config.getContentGeneratorConfig()?.authType,
config.getContentGeneratorConfig().authType,
undefined,
config.getModel(),
DEFAULT_GEMINI_FLASH_MODEL,
@@ -1407,7 +1407,7 @@ export const useGeminiStream = (
new UserPromptEvent(
promptText.length,
prompt_id!,
config.getContentGeneratorConfig()?.authType,
config.getContentGeneratorConfig().authType,
promptText,
),
);
@@ -1499,7 +1499,7 @@ export const useGeminiStream = (
type: MessageType.ERROR,
text: parseAndFormatApiError(
getErrorMessage(error) || 'Unknown error',
config.getContentGeneratorConfig()?.authType,
config.getContentGeneratorConfig().authType,
undefined,
config.getModel(),
DEFAULT_GEMINI_FLASH_MODEL,
@@ -1625,7 +1625,7 @@ export const useGeminiStream = (
| TrackedCompletedToolCall
| TrackedCancelledToolCall;
return (
completedOrCancelledCall.response?.responseParts !== undefined
completedOrCancelledCall.response.responseParts !== undefined
);
}
return false;
@@ -1653,7 +1653,7 @@ export const useGeminiStream = (
const isShell = t.request.name === 'run_shell_command';
// Access result from the tracked tool call response
const response = t.response as ToolResponseWithParts;
const rawData = response?.data;
const rawData = response.data;
const data = isShellToolData(rawData) ? rawData : undefined;
// Use data.pid for shell commands moved to the background.
@@ -1661,9 +1661,9 @@ export const useGeminiStream = (
if (isShell && pid) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const command = (data?.['command'] as string) ?? 'shell';
const command = (data['command'] as string) ?? 'shell';
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const initialOutput = (data?.['initialOutput'] as string) ?? '';
const initialOutput = (data['initialOutput'] as string) ?? '';
registerBackgroundShell(pid, command, initialOutput);
}
@@ -86,21 +86,21 @@ export function useHistory({
switch (itemData.type) {
case 'compression':
case 'info':
chatRecordingService?.recordMessage({
chatRecordingService.recordMessage({
model: undefined,
type: 'info',
content: itemData.text ?? '',
});
break;
case 'warning':
chatRecordingService?.recordMessage({
chatRecordingService.recordMessage({
model: undefined,
type: 'warning',
content: itemData.text ?? '',
});
break;
case 'error':
chatRecordingService?.recordMessage({
chatRecordingService.recordMessage({
model: undefined,
type: 'error',
content: itemData.text ?? '',
@@ -138,7 +138,7 @@ export function useQuotaAndFallback({
: null,
`/stats model for usage details`,
`/model to switch models.`,
contentGeneratorConfig?.authType === AuthType.LOGIN_WITH_GOOGLE
contentGeneratorConfig.authType === AuthType.LOGIN_WITH_GOOGLE
? `/auth to switch to API key.`
: null,
].filter(Boolean);
@@ -194,7 +194,7 @@ export function useQuotaAndFallback({
message,
isTerminalQuotaError,
isModelNotFoundError,
authType: contentGeneratorConfig?.authType,
authType: contentGeneratorConfig.authType,
});
},
);
@@ -104,7 +104,7 @@ export const useSessionBrowser = (
try {
const chatRecordingService = config
.getGeminiClient()
?.getChatRecordingService();
.getChatRecordingService();
if (chatRecordingService) {
chatRecordingService.deleteSession(session.file);
}
@@ -84,7 +84,7 @@ export function useSessionResume({
}
// Give the history to the Gemini client.
await config.getGeminiClient()?.resumeChat(clientHistory, resumedData);
await config.getGeminiClient().resumeChat(clientHistory, resumedData);
} catch (error) {
coreEvents.emitFeedback(
'error',

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