Compare commits

..

15 Commits

Author SHA1 Message Date
jacob314 a09f6b40ff Debug memory alloc 2026-03-19 11:30:58 -07:00
Sandy Tao 4e5dfd0cb7 feat(core): cap JIT context upward traversal at git root (#23074) 2026-03-19 17:50:58 +00:00
Jacob Richman 524b1e39a5 Fix logging and virtual list. (#23080) 2026-03-19 17:50:49 +00:00
Abhi 32a123fc54 feat(core): inject memory and JIT context into subagents (#23032) 2026-03-19 17:16:09 +00:00
Tommaso Sciortino 23264ced9a refactor(cli): integrate real config loading into async test utils (#23040) 2026-03-19 17:05:33 +00:00
cynthialong0-0 7de0616229 fix(browser-agent): enable "Allow all server tools" session policy (#22343) 2026-03-19 16:32:35 +00:00
joshualitt 39d3b0e28c feat(cli): Partial threading of AgentLoopContext. (#22978) 2026-03-19 16:02:13 +00:00
Gaurav 5acaacad96 fix(core): narrow browser agent description to prevent stealing URL tasks from web_fetch (#23086) 2026-03-19 10:45:59 +00:00
Jason Matthew Suhari a921bcd9ef fix(core): don't persist browser consent sentinel in non-interactive mode (#23073) 2026-03-19 06:47:13 +00:00
Sri Pasumarthi e9171fd792 fix: circumvent genai sdk requirement for api key when using gateway auth via ACP (#23042) 2026-03-19 04:31:02 +00:00
Adam Weidman 2009fbbd92 feat(core): add content-utils (#22984) 2026-03-19 02:30:48 +00:00
Bryan Morgan 8db2948361 fix(cli): correctly handle auto-update for standalone binaries (#23038) 2026-03-19 01:52:23 +00:00
Abhi 5fa14dbe42 feat(core): resilient subagent tool rejection with contextual feedback (#22951) 2026-03-19 01:09:37 +00:00
Suraj Sahani e6cd5d208c docs: add FAQ entry for checking Gemini CLI version (#21271) 2026-03-19 00:25:33 +00:00
Valery Teplyakov c9d48026c4 fix(acp): provide more meta in tool_call_update (#22663)
Co-authored-by: Mervap <megavaprold@gmail.com>
Co-authored-by: Sri Pasumarthi <sripas@google.com>
2026-03-19 00:02:07 +00:00
245 changed files with 4104 additions and 2433 deletions
+4 -35
View File
@@ -71,44 +71,12 @@ accessible.
tables).
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
for all images.
- **Details section:** Use the `<details>` tag to create a collapsible section.
This is useful for supplementary or data-heavy information that isn't critical
to the main flow.
Example:
<details>
<summary>Title</summary>
- First entry
- Second entry
</details>
- **Callouts**: Use GitHub-flavored markdown alerts to highlight important
information. To ensure the formatting is preserved by `npm run format`, place
an empty line, then the `<!-- prettier-ignore -->` comment directly before
the callout block. The callout type (`[!TYPE]`) should be on the first line,
followed by a newline, and then the content, with each subsequent line of
content starting with `>`. Available types are `NOTE`, `TIP`, `IMPORTANT`,
`WARNING`, and `CAUTION`.
Example:
<!-- prettier-ignore -->
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
### Structure
- **BLUF:** Start with an introduction explaining what to expect.
- **Experimental features:** If a feature is clearly noted as experimental,
add the following note immediately after the introductory paragraph:
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
add the following note immediately after the introductory paragraph:
`> **Note:** This is a preview feature currently under active development.`
- **Headings:** Use hierarchical headings to support the user journey.
- **Procedures:**
- Introduce lists of steps with a complete sentence.
@@ -117,7 +85,8 @@ accessible.
- Put conditions before instructions (e.g., "On the Settings page, click...").
- Provide clear context for where the action takes place.
- Indicate optional steps clearly (e.g., "Optional: ...").
- **Elements:** Use bullet lists, tables, details, and callouts.
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
(`> **Warning:**`).
- **Avoid using a table of contents:** If a table of contents is present, remove
it.
- **Next steps:** Conclude with a "Next steps" section if applicable.
-2
View File
@@ -1,9 +1,7 @@
name: 'Website issue'
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
title: "GeminiCLI.com Feedback: [ISSUE]"
labels:
- 'area/extensions'
- 'area/documentation'
body:
- type: 'markdown'
attributes:
+1 -3
View File
@@ -39,9 +39,7 @@ file in your project's temporary directory, typically located at
The Checkpointing feature is disabled by default. To enable it, you need to edit
your `settings.json` file.
<!-- prettier-ignore -->
> [!CAUTION]
> The `--checkpointing` command-line flag was removed in version
> **Note:** The `--checkpointing` command-line flag was removed in version
> 0.11.0. Checkpointing can now only be enabled through the `settings.json`
> configuration file.
+5 -7
View File
@@ -30,9 +30,7 @@ separator (`/` or `\`) being converted to a colon (`:`).
- A file at `<project>/.gemini/commands/git/commit.toml` becomes the namespaced
command `/git:commit`.
<!-- prettier-ignore -->
> [!TIP]
> After creating or modifying `.toml` command files, run
> [!TIP] After creating or modifying `.toml` command files, run
> `/commands reload` to pick up your changes without restarting the CLI.
## TOML file format (v1)
@@ -179,10 +177,10 @@ ensure that only intended commands can be run.
automatically shell-escaped (see
[Context-Aware Injection](#1-context-aware-injection-with-args) above).
3. **Robust parsing:** The parser correctly handles complex shell commands that
include nested braces, such as JSON payloads. The content inside `!{...}`
must have balanced braces (`{` and `}`). If you need to execute a command
containing unbalanced braces, consider wrapping it in an external script
file and calling the script within the `!{...}` block.
include nested braces, such as JSON payloads. **Note:** The content inside
`!{...}` must have balanced braces (`{` and `}`). If you need to execute a
command containing unbalanced braces, consider wrapping it in an external
script file and calling the script within the `!{...}` block.
4. **Security check and confirmation:** The CLI performs a security check on
the final, resolved command (after arguments are escaped and substituted). A
dialog will appear showing the exact command(s) to be executed.
+9 -15
View File
@@ -5,11 +5,9 @@ and managing Gemini CLI in an enterprise environment. By leveraging system-level
settings, administrators can enforce security policies, manage tool access, and
ensure a consistent experience for all users.
<!-- prettier-ignore -->
> [!WARNING]
> The patterns described in this document are intended to help
> administrators create a more controlled and secure environment for using
> Gemini CLI. However, they should not be considered a foolproof security
> **A note on security:** The patterns described in this document are intended
> to help administrators create a more controlled and secure environment for
> using Gemini CLI. However, they should not be considered a foolproof security
> boundary. A determined user with sufficient privileges on their local machine
> may still be able to circumvent these configurations. These measures are
> designed to prevent accidental misuse and enforce corporate policy in a
@@ -282,12 +280,10 @@ environment to a blocklist.
}
```
<!-- prettier-ignore -->
> [!WARNING]
> Blocklisting with `excludeTools` is less secure than
> allowlisting with `coreTools`, as it relies on blocking known-bad commands,
> and clever users may find ways to bypass simple string-based blocks.
> **Allowlisting is the recommended approach.**
**Security note:** Blocklisting with `excludeTools` is less secure than
allowlisting with `coreTools`, as it relies on blocking known-bad commands, and
clever users may find ways to bypass simple string-based blocks. **Allowlisting
is the recommended approach.**
### Disabling YOLO mode
@@ -498,10 +494,8 @@ other events. For more information, see the
}
```
<!-- prettier-ignore -->
> [!NOTE]
> Ensure that `logPrompts` is set to `false` in an enterprise setting to
> avoid collecting potentially sensitive information from user prompts.
**Note:** Ensure that `logPrompts` is set to `false` in an enterprise setting to
avoid collecting potentially sensitive information from user prompts.
## Authentication
+3 -4
View File
@@ -4,10 +4,9 @@ Model steering lets you provide real-time guidance and feedback to Gemini CLI
while it is actively executing a task. This lets you correct course, add missing
context, or skip unnecessary steps without having to stop and restart the agent.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development and
> may need to be enabled under `/settings`.
> **Note:** This is a preview feature under active development. Preview features
> may only be available in the **Preview** channel or may need to be enabled
> under `/settings`.
Model steering is particularly useful during complex [Plan Mode](./plan-mode.md)
workflows or long-running subagent executions where you want to ensure the agent
+1 -3
View File
@@ -5,9 +5,7 @@ used by Gemini CLI, giving you more control over your results. Use **Pro**
models for complex tasks and reasoning, **Flash** models for high speed results,
or the (recommended) **Auto** setting to choose the best model for your tasks.
<!-- prettier-ignore -->
> [!NOTE]
> The `/model` command (and the `--model` flag) does not override the
> **Note:** The `/model` command (and the `--model` flag) does not override the
> model used by sub-agents. Consequently, even when using the `/model` flag you
> may see other models used in your model usage reports.
+3 -4
View File
@@ -4,10 +4,9 @@ Gemini CLI can send system notifications to alert you when a session completes
or when it needs your attention, such as when it's waiting for you to approve a
tool call.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development and
> may need to be enabled under `/settings`.
> **Note:** This is a preview feature currently under active development.
> Preview features may be available on the **Preview** channel or may need to be
> enabled under `/settings`.
Notifications are particularly useful when running long-running tasks or using
[Plan Mode](./plan-mode.md), letting you switch to other windows while Gemini
+8 -8
View File
@@ -35,17 +35,19 @@ To launch Gemini CLI in Plan Mode once:
To start Plan Mode while using Gemini CLI:
- **Keyboard shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`). Plan Mode is automatically removed from
the rotation when Gemini CLI is actively processing or showing confirmation
dialogs.
(`Default` -> `Auto-Edit` -> `Plan`).
> **Note:** Plan Mode is automatically removed from the rotation when Gemini
> CLI is actively processing or showing confirmation dialogs.
- **Command:** Type `/plan` in the input box.
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI
calls the
[`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool
to switch modes. This tool is not available when Gemini CLI is in
[YOLO mode](../reference/configuration.md#command-line-arguments).
to switch modes.
> **Note:** This tool is not available when Gemini CLI is in
> [YOLO mode](../reference/configuration.md#command-line-arguments).
## How to use Plan Mode
@@ -405,9 +407,7 @@ To build a custom planning workflow, you can use:
[custom plan directories](#custom-plan-directory-and-policies) and
[custom policies](#custom-policies).
<!-- prettier-ignore -->
> [!TIP]
> Use [Conductor] as a reference when building your own custom
> **Note:** Use [Conductor] as a reference when building your own custom
> planning workflow.
By using Plan Mode as its execution environment, your custom methodology can
+3 -5
View File
@@ -253,11 +253,9 @@ $env:SANDBOX_SET_UID_GID="false" # Disable UID/GID mapping
DEBUG=1 gemini -s -p "debug command"
```
<!-- prettier-ignore -->
> [!NOTE]
> If you have `DEBUG=true` in a project's `.env` file, it won't affect
> gemini-cli due to automatic exclusion. Use `.gemini/.env` files for
> gemini-cli specific debug settings.
**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect
gemini-cli due to automatic exclusion. Use `.gemini/.env` files for gemini-cli
specific debug settings.
### Inspect sandbox
+1 -3
View File
@@ -11,9 +11,7 @@ locations:
- **User settings**: `~/.gemini/settings.json`
- **Workspace settings**: `your-project/.gemini/settings.json`
<!-- prettier-ignore -->
> [!IMPORTANT]
> Workspace settings override user settings.
Note: Workspace settings override user settings.
## Settings reference
+2 -4
View File
@@ -63,10 +63,8 @@ Use the `/skills` slash command to view and manage available expertise:
- `/skills enable <name>`: Re-enables a disabled skill.
- `/skills reload`: Refreshes the list of discovered skills from all tiers.
<!-- prettier-ignore -->
> [!NOTE]
> `/skills disable` and `/skills enable` default to the `user` scope. Use
> `--scope workspace` to manage workspace-specific settings.
_Note: `/skills disable` and `/skills enable` default to the `user` scope. Use
`--scope workspace` to manage workspace-specific settings._
### From the Terminal
+1 -3
View File
@@ -14,9 +14,7 @@ core instructions will apply unless you include them yourself.
This feature is intended for advanced users who need to enforce strict,
project-specific behavior or create a customized persona.
<!-- prettier-ignore -->
> [!TIP]
> You can export the current default system prompt to a file first, review
> Tip: You can export the current default system prompt to a file first, review
> it, and then selectively modify or replace it (see
> [“Export the default prompt”](#export-the-default-prompt-recommended)).
+3 -5
View File
@@ -125,11 +125,9 @@ You must complete several setup steps before enabling Google Cloud telemetry.
}
```
<!-- prettier-ignore -->
> [!NOTE]
> This setting requires **Direct export** (in-process exporters)
> and cannot be used when `useCollector` is `true`. If both are enabled,
> telemetry will be disabled.
> **Note:** This setting requires **Direct export** (in-process exporters)
> and cannot be used when `useCollector` is `true`. If both are enabled,
> telemetry will be disabled.
3. Ensure your account or service account has these IAM roles:
- Cloud Trace Agent
+8 -12
View File
@@ -36,11 +36,9 @@ using the `/theme` command within Gemini CLI:
preview or highlight as you select.
4. Confirm your selection to apply the theme.
<!-- prettier-ignore -->
> [!NOTE]
> If a theme is defined in your `settings.json` file (either by name or
> by a file path), you must remove the `"theme"` setting from the file before
> you can change the theme using the `/theme` command.
**Note:** If a theme is defined in your `settings.json` file (either by name or
by a file path), you must remove the `"theme"` setting from the file before you
can change the theme using the `/theme` command.
### Theme persistence
@@ -181,13 +179,11 @@ custom theme defined in `settings.json`.
}
```
<!-- prettier-ignore -->
> [!WARNING]
> For your safety, Gemini CLI will only load theme files that
> are located within your home directory. If you attempt to load a theme from
> outside your home directory, a warning will be displayed and the theme will
> not be loaded. This is to prevent loading potentially malicious theme files
> from untrusted sources.
**Security note:** For your safety, Gemini CLI will only load theme files that
are located within your home directory. If you attempt to load a theme from
outside your home directory, a warning will be displayed and the theme will not
be loaded. This is to prevent loading potentially malicious theme files from
untrusted sources.
### Example custom theme
+1 -3
View File
@@ -62,9 +62,7 @@ structure. It will return the specific path (e.g.,
`src/components/UserProfile.tsx`), which you can then use with `@` in your next
turn.
<!-- prettier-ignore -->
> [!TIP]
> You can also ask for lists of files, like "Show me all the TypeScript
> **Tip:** You can also ask for lists of files, like "Show me all the TypeScript
> configuration files in the root directory."
## How to modify code
+2 -4
View File
@@ -62,10 +62,8 @@ You tell Gemini about new servers by editing your `settings.json`.
}
```
<!-- prettier-ignore -->
> [!NOTE]
> The `command` is `docker`, and the rest are arguments passed to it. We
> map the local environment variable into the container so your secret isn't
> **Note:** The `command` is `docker`, and the rest are arguments passed to it.
> We map the local environment variable into the container so your secret isn't
> hardcoded in the config file.
## How to verify the connection
+3 -4
View File
@@ -5,10 +5,9 @@ structured environment with model steering's real-time feedback, you can guide
Gemini CLI through the research and design phases to ensure the final
implementation plan is exactly what you need.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development and
> may need to be enabled under `/settings`.
> **Note:** This is a preview feature under active development. Preview features
> may only be available in the **Preview** channel or may need to be enabled
> under `/settings`.
## Prerequisites
+3 -8
View File
@@ -10,9 +10,7 @@ agents in the following repositories:
- [ADK Samples (Python)](https://github.com/google/adk-samples/tree/main/python)
- [ADK Python Contributing Samples](https://github.com/google/adk-python/tree/main/contributing/samples)
<!-- prettier-ignore -->
> [!NOTE]
> Remote subagents are currently an experimental feature.
> **Note: Remote subagents are currently an experimental feature.**
## Configuration
@@ -84,8 +82,7 @@ Markdown file.
---
```
<!-- prettier-ignore -->
> [!NOTE] Mixed local and remote agents, or multiple local agents, are not
> **Note:** Mixed local and remote agents, or multiple local agents, are not
> supported in a single file; the list format is currently remote-only.
## Authentication
@@ -365,7 +362,5 @@ Users can manage subagents using the following commands within the Gemini CLI:
- `/agents enable <agent_name>`: Enables a specific subagent.
- `/agents disable <agent_name>`: Disables a specific subagent.
<!-- prettier-ignore -->
> [!TIP]
> You can use the `@cli_help` agent within Gemini CLI for assistance
> **Tip:** You can use the `@cli_help` agent within Gemini CLI for assistance
> with configuring subagents.
+13 -21
View File
@@ -5,18 +5,16 @@ session. They are designed to handle specific, complex tasks—like deep codebas
analysis, documentation lookup, or domain-specific reasoning—without cluttering
the main agent's context or toolset.
<!-- prettier-ignore -->
> [!NOTE]
> Subagents are currently an experimental feature.
>
To use custom subagents, you must ensure they are enabled in your
`settings.json` (enabled by default):
```json
{
"experimental": { "enableAgents": true }
}
```
> **Note: Subagents are currently an experimental feature.**
>
> To use custom subagents, you must ensure they are enabled in your
> `settings.json` (enabled by default):
>
> ```json
> {
> "experimental": { "enableAgents": true }
> }
> ```
## What are subagents?
@@ -116,9 +114,7 @@ Gemini CLI comes with the following built-in subagents:
the pricing table from this page," "Click the login button and enter my
credentials."
<!-- prettier-ignore -->
> [!NOTE]
> This is a preview feature currently under active development.
> **Note:** This is a preview feature currently under active development.
#### Prerequisites
@@ -221,9 +217,7 @@ captures a screenshot and sends it to the vision model for analysis. The model
returns coordinates and element descriptions that the browser agent uses with
the `click_at` tool for precise, coordinate-based interactions.
<!-- prettier-ignore -->
> [!NOTE]
> The visual agent requires API key or Vertex AI authentication. It is
> **Note:** The visual agent requires API key or Vertex AI authentication. It is
> not available when using "Sign in with Google".
## Creating custom subagents
@@ -411,9 +405,7 @@ that your subagent was called with a specific prompt and the given description.
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
(A2A) protocol.
<!-- prettier-ignore -->
> [!NOTE]
> Remote subagents are currently an experimental feature.
> **Note: Remote subagents are currently an experimental feature.**
See the [Remote Subagents documentation](remote-agents) for detailed
configuration, authentication, and usage instructions.
+2 -6
View File
@@ -234,9 +234,7 @@ skill definitions in a `skills/` directory. For example,
### Sub-agents
<!-- prettier-ignore -->
> [!NOTE]
> Sub-agents are a preview feature currently under active development.
> **Note:** Sub-agents are a preview feature currently under active development.
Provide [sub-agents](../core/subagents.md) that users can delegate tasks to. Add
agent definition files (`.md`) to an `agents/` directory in your extension root.
@@ -255,9 +253,7 @@ Rules contributed by extensions run in their own tier (tier 2), alongside
workspace-defined policies. This tier has higher priority than the default rules
but lower priority than user or admin policies.
<!-- prettier-ignore -->
> [!WARNING]
> For security, Gemini CLI ignores any `allow` decisions or `yolo`
> **Warning:** For security, Gemini CLI ignores any `allow` decisions or `yolo`
> mode configurations in extension policies. This ensures that an extension
> cannot automatically approve tool calls or bypass security measures without
> your confirmation.
+42 -52
View File
@@ -4,9 +4,7 @@ To use Gemini CLI, you'll need to authenticate with Google. This guide helps you
quickly find the best way to sign in based on your account type and how you're
using the CLI.
<!-- prettier-ignore -->
> [!TIP]
> Looking for a high-level comparison of all available subscriptions?
> **Note:** Looking for a high-level comparison of all available subscriptions?
> To compare features and find the right quota for your needs, see our
> [Plans page](https://geminicli.com/plans/).
@@ -45,8 +43,8 @@ is logging in with your Google account. This method requires a web browser on a
machine that can communicate with the terminal running Gemini CLI (e.g., your
local machine).
If you are a **Google AI Pro** or **Google AI Ultra** subscriber, use the Google
account associated with your subscription.
> **Important:** If you are a **Google AI Pro** or **Google AI Ultra**
> subscriber, use the Google account associated with your subscription.
To authenticate and use Gemini CLI:
@@ -109,9 +107,7 @@ To authenticate and use Gemini CLI with a Gemini API key:
4. Select **Use Gemini API key**.
<!-- prettier-ignore -->
> [!WARNING]
> Treat API keys, especially for services like Gemini, as sensitive
> **Warning:** Treat API keys, especially for services like Gemini, as sensitive
> credentials. Protect them to prevent unauthorized access and potential misuse
> of the service under your account.
@@ -154,20 +150,20 @@ To make any Vertex AI environment variable settings persistent, see
Consider this authentication method if you have Google Cloud CLI installed.
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
them to use ADC.
**macOS/Linux**
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
**Windows (PowerShell)**
```powershell
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
> **Note:** If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you
> must unset them to use ADC:
>
> **macOS/Linux**
>
> ```bash
> unset GOOGLE_API_KEY GEMINI_API_KEY
> ```
>
> **Windows (PowerShell)**
>
> ```powershell
> Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
> ```
1. Verify you have a Google Cloud project and Vertex AI API is enabled.
@@ -192,20 +188,20 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
Consider this method of authentication in non-interactive environments, CI/CD
pipelines, or if your organization restricts user-based ADC or API key creation.
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
them:
**macOS/Linux**
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
**Windows (PowerShell)**
```powershell
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
> **Note:** If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you
> must unset them:
>
> **macOS/Linux**
>
> ```bash
> unset GOOGLE_API_KEY GEMINI_API_KEY
> ```
>
> **Windows (PowerShell)**
>
> ```powershell
> Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
> ```
1. [Create a service account and key](https://cloud.google.com/iam/docs/keys-create-delete)
and download the provided JSON file. Assign the "Vertex AI User" role to the
@@ -237,11 +233,8 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
5. Select **Vertex AI**.
<!-- prettier-ignore -->
> [!WARNING]
> Protect your service account key file as it gives access to
> your resources.
> **Warning:** Protect your service account key file as it gives access to
> your resources.
#### C. Vertex AI - Google Cloud API key
@@ -264,9 +257,10 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
If you see errors like `"API keys are not supported by this API..."`, your
organization might restrict API key usage for this service. Try the other
Vertex AI authentication methods instead.
> **Note:** If you see errors like
> `"API keys are not supported by this API..."`, your organization might
> restrict API key usage for this service. Try the other Vertex AI
> authentication methods instead.
3. [Configure your Google Cloud Project](#set-gcp).
@@ -280,9 +274,7 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
## Set your Google Cloud project <a id="set-gcp"></a>
<!-- prettier-ignore -->
> [!IMPORTANT]
> Most individual Google accounts (free and paid) don't require a
> **Important:** Most individual Google accounts (free and paid) don't require a
> Google Cloud project for authentication.
When you sign in using your Google account, you may need to configure a Google
@@ -347,11 +339,9 @@ persist them with the following methods:
. $PROFILE
```
<!-- prettier-ignore -->
> [!WARNING]
> Be aware that when you export API keys or service account
> paths in your shell configuration file, any process launched from that
> shell can read them.
> **Warning:** Be aware that when you export API keys or service account
> paths in your shell configuration file, any process launched from that
> shell can read them.
2. **Use a `.env` file:** Create a `.gemini/.env` file in your project
directory or home directory. Gemini CLI automatically loads variables from
+1 -3
View File
@@ -4,9 +4,7 @@ Gemini CLI helps you automate common engineering tasks by combining AI reasoning
with local system tools. This document provides examples of how to use the CLI
for file management, code analysis, and data transformation.
<!-- prettier-ignore -->
> [!NOTE]
> These examples demonstrate potential capabilities. Your actual
> **Note:** These examples demonstrate potential capabilities. Your actual
> results can vary based on the model used and your project environment.
## Rename your photographs based on content
+3 -9
View File
@@ -2,9 +2,7 @@
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
<!-- prettier-ignore -->
> [!NOTE]
> Gemini 3.1 Pro Preview is rolling out. To determine whether you have
> **Note:** Gemini 3.1 Pro Preview is rolling out. To determine whether you have
> access to Gemini 3.1, use the `/model` command and select **Manual**. If you
> have access, you will see `gemini-3.1-pro-preview`.
>
@@ -41,9 +39,7 @@ When you encounter that limit, youll be given the option to switch to Gemini
2.5 Pro, upgrade for higher limits, or stop. Youll also be told when your usage
limit resets and Gemini 3 Pro can be used again.
<!-- prettier-ignore -->
> [!TIP]
> Looking to upgrade for higher limits? To compare subscription
> **Note:** Looking to upgrade for higher limits? To compare subscription
> options and find the right quota for your needs, see our
> [Plans page](https://geminicli.com/plans/).
@@ -56,9 +52,7 @@ There may be times when the Gemini 3 Pro model is overloaded. When that happens,
Gemini CLI will ask you to decide whether you want to keep trying Gemini 3 Pro
or fallback to Gemini 2.5 Pro.
<!-- prettier-ignore -->
> [!NOTE]
> The **Keep trying** option uses exponential backoff, in which Gemini
> **Note:** The **Keep trying** option uses exponential backoff, in which Gemini
> CLI waits longer between each retry, when the system is busy. If the retry
> doesn't happen immediately, please wait a few minutes for the request to
> process.
+1 -3
View File
@@ -143,9 +143,7 @@ Hooks are executed with a sanitized environment.
## Security and risks
<!-- prettier-ignore -->
> [!WARNING]
> Hooks execute arbitrary code with your user privileges. By
> **Warning: Hooks execute arbitrary code with your user privileges.** By
> configuring hooks, you are allowing scripts to run shell commands on your
> machine.
+3 -5
View File
@@ -132,11 +132,9 @@ to the CLI whenever the user's context changes.
}
```
<!-- prettier-ignore -->
> [!NOTE]
> The `openFiles` list should only include files that exist on disk.
> Virtual files (e.g., unsaved files without a path, editor settings pages)
> **MUST** be excluded.
**Note:** The `openFiles` list should only include files that exist on disk.
Virtual files (e.g., unsaved files without a path, editor settings pages)
**MUST** be excluded.
### How the CLI uses this context
+4 -8
View File
@@ -66,11 +66,9 @@ You can also install the extension directly from a marketplace.
Follow your editor's instructions for installing extensions from this
registry.
<!-- prettier-ignore -->
> [!NOTE]
> The "Gemini CLI Companion" extension may appear towards the bottom of
> search results. If you don't see it immediately, try scrolling down or
> sorting by "Newly Published".
> NOTE: The "Gemini CLI Companion" extension may appear towards the bottom of
> search results. If you don't see it immediately, try scrolling down or sorting
> by "Newly Published".
>
> After manually installing the extension, you must run `/ide enable` in the CLI
> to activate the integration.
@@ -105,9 +103,7 @@ IDE, run:
If connected, this command will show the IDE it's connected to and a list of
recently opened files it is aware of.
<!-- prettier-ignore -->
> [!NOTE]
> The file list is limited to 10 recently accessed files within your
> [!NOTE] The file list is limited to 10 recently accessed files within your
> workspace and only includes local files on disk.)
### Working with diffs
+1 -3
View File
@@ -14,9 +14,7 @@ feature), while the PR is the "how" (the implementation). This separation helps
us track work, prioritize features, and maintain clear historical context. Our
automation is built around this principle.
<!-- prettier-ignore -->
> [!NOTE]
> Issues tagged as "🔒Maintainers only" are reserved for project
> **Note:** Issues tagged as "🔒Maintainers only" are reserved for project
> maintainers. We will not accept pull requests related to these issues.
---
+1 -3
View File
@@ -79,9 +79,7 @@ You can view traces in the Jaeger UI for local development.
You can use an OpenTelemetry collector to forward telemetry data to Google Cloud
Trace for custom processing or routing.
<!-- prettier-ignore -->
> [!WARNING]
> Ensure you complete the
> **Warning:** Ensure you complete the
> [Google Cloud telemetry prerequisites](./cli/telemetry.md#prerequisites)
> (Project ID, authentication, IAM roles, and APIs) before using this method.
+2 -2
View File
@@ -60,8 +60,8 @@ Slash commands provide meta-level control over the CLI itself.
- `list` (selecting this opens the auto-saved session browser)
- `-- checkpoints --`
- `list`, `save`, `resume`, `delete`, `share` (manual tagged checkpoints)
- Unique prefixes (for example `/cha` or `/resu`) resolve to the same grouped
menu.
- **Note:** Unique prefixes (for example `/cha` or `/resum`) resolve to the
same grouped menu.
- **Sub-commands:**
- **`debug`**
- **Description:** Export the most recent API request as a JSON payload.
+5 -9
View File
@@ -25,9 +25,7 @@ overridden by higher numbers):
Gemini CLI uses JSON settings files for persistent configuration. There are four
locations for these files:
<!-- prettier-ignore -->
> [!TIP]
> JSON-aware editors can use autocomplete and validation by pointing to
> **Tip:** JSON-aware editors can use autocomplete and validation by pointing to
> the generated schema at `schemas/settings.schema.json` in this repository.
> When working outside the repo, reference the hosted schema at
> `https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json`.
@@ -68,9 +66,9 @@ an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like
this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
`.env` file in its directory, which will be loaded automatically.
**Note for Enterprise Users:** For guidance on deploying and managing Gemini CLI
in a corporate environment, please see the
[Enterprise Configuration](../cli/enterprise.md) documentation.
> **Note for Enterprise Users:** For guidance on deploying and managing Gemini
> CLI in a corporate environment, please see the
> [Enterprise Configuration](../cli/enterprise.md) documentation.
### The `.gemini` directory in your project
@@ -1561,9 +1559,7 @@ for compatibility. At least one of `command`, `url`, or `httpUrl` must be
provided. If multiple are specified, the order of precedence is `httpUrl`, then
`url`, then `command`.
<!-- prettier-ignore -->
> [!WARNING]
> Avoid using underscores (`_`) in your server aliases (e.g., use
> **Warning:** Avoid using underscores (`_`) in your server aliases (e.g., use
> `my-server` instead of `my_server`). The underlying policy engine parses Fully
> Qualified Names (`mcp_server_tool`) using the first underscore after the
> `mcp_` prefix. An underscore in your server alias will cause the parser to
+10 -16
View File
@@ -113,9 +113,7 @@ There are three possible decisions a rule can enforce:
- `ask_user`: The user is prompted to approve or deny the tool call. (In
non-interactive mode, this is treated as `deny`.)
<!-- prettier-ignore -->
> [!NOTE]
> The `deny` decision is the recommended way to exclude tools. The
> **Note:** The `deny` decision is the recommended way to exclude tools. The
> legacy `tools.exclude` setting in `settings.json` is deprecated in favor of
> policy rules with a `deny` decision.
@@ -241,17 +239,15 @@ directory are **ignored**.
- **Linux / macOS:** Must be owned by `root` (UID 0) and NOT writable by group
or others (e.g., `chmod 755`).
- **Windows:** Must be in `C:\ProgramData`. Standard users (`Users`, `Everyone`)
must NOT have `Write`, `Modify`, or `Full Control` permissions. If you see a
security warning, use the folder properties to remove write permissions for
non-admin groups. You may need to "Disable inheritance" in Advanced Security
Settings.
must NOT have `Write`, `Modify`, or `Full Control` permissions. _Tip: If you
see a security warning, use the folder properties to remove write permissions
for non-admin groups. You may need to "Disable inheritance" in Advanced
Security Settings._
<!-- prettier-ignore -->
> [!NOTE]
> Supplemental admin policies (provided via `--admin-policy` or
> `adminPolicyPaths` settings) are **NOT** subject to these strict ownership
> checks, as they are explicitly provided by the user or administrator in their
> current execution context.
**Note:** Supplemental admin policies (provided via `--admin-policy` or
`adminPolicyPaths` settings) are **NOT** subject to these strict ownership
checks, as they are explicitly provided by the user or administrator in their
current execution context.
### TOML rule schema
@@ -352,9 +348,7 @@ using the `mcpName` field. **This is the recommended approach** for defining MCP
policies, as it is much more robust than manually writing Fully Qualified Names
(FQNs) or string wildcards.
<!-- prettier-ignore -->
> [!WARNING]
> Do not use underscores (`_`) in your MCP server names (e.g., use
> **Warning:** Do not use underscores (`_`) in your MCP server names (e.g., use
> `my-server` rather than `my_server`). The policy parser splits Fully Qualified
> Names (`mcp_server_tool`) on the _first_ underscore following the `mcp_`
> prefix. If your server name contains an underscore, the parser will
+1 -3
View File
@@ -95,9 +95,7 @@ For developers, the tool system is designed to be extensible and robust. The
You can extend Gemini CLI with custom tools by configuring
`tools.discoveryCommand` in your settings or by connecting to MCP servers.
<!-- prettier-ignore -->
> [!NOTE]
> For a deep dive into the internal Tool API and how to implement your
> **Note:** For a deep dive into the internal Tool API and how to implement your
> own tools in the codebase, see the `packages/core/src/tools/` directory in
> GitHub.
+3 -7
View File
@@ -21,13 +21,9 @@ All workflows in `.github/workflows/ci.yml` must pass on the `main` branch (for
nightly) or the release branch (for preview/stable).
- **Platforms:** Tests must pass on **Linux and macOS**.
<!-- prettier-ignore -->
> [!NOTE]
> Windows tests currently run with `continue-on-error: true`. While a
> failure here doesn't block the release technically, it should be
> investigated.
- _Note:_ Windows tests currently run with `continue-on-error: true`. While a
failure here doesn't block the release technically, it should be
investigated.
- **Checks:**
- **Linting:** No linting errors (ESLint, Prettier, etc.).
- **Typechecking:** No TypeScript errors.
+7 -11
View File
@@ -234,12 +234,10 @@ This workflow will automatically:
Review the automatically created pull request(s) to ensure the cherry-pick was
successful and the changes are correct. Once approved, merge the pull request.
<!-- prettier-ignore -->
> [!WARNING]
> The `release/*` branches are protected by branch protection
> rules. A pull request to one of these branches requires at least one review from
> a code owner before it can be merged. This ensures that no unauthorized code is
> released.
**Security note:** The `release/*` branches are protected by branch protection
rules. A pull request to one of these branches requires at least one review from
a code owner before it can be merged. This ensures that no unauthorized code is
released.
#### 2.5. Adding multiple commits to a hotfix (advanced)
@@ -526,11 +524,9 @@ Notifications use
[GitHub for Google Chat](https://workspace.google.com/marketplace/app/github_for_google_chat/536184076190).
To modify the notifications, use `/github-settings` within the chat space.
<!-- prettier-ignore -->
> [!WARNING]
> The following instructions describe a fragile workaround that depends on the
> internal structure of the chat application's UI. It is likely to break with
> future updates.
> [!WARNING] The following instructions describe a fragile workaround that
> depends on the internal structure of the chat application's UI. It is likely
> to break with future updates.
The list of available labels is not currently populated correctly. If you want
to add a label that does not appear alphabetically in the first 30 labels in the
+13
View File
@@ -58,6 +58,19 @@ your total token usage using the `/stats` command in Gemini CLI.
## Installation and updates
### How do I check which version of Gemini CLI I'm currently running?
You can check your current Gemini CLI version using one of these methods:
- Run `gemini --version` or `gemini -v` from your terminal
- Check the globally installed version using your package manager:
- npm: `npm list -g @google/gemini-cli`
- pnpm: `pnpm list -g @google/gemini-cli`
- yarn: `yarn global list @google/gemini-cli`
- bun: `bun pm ls -g @google/gemini-cli`
- homebrew: `brew list --versions gemini-cli`
- Inside an active Gemini CLI session, use the `/about` command
### How do I update Gemini CLI to the latest version?
If you installed it globally via `npm`, update it using the command
+2 -4
View File
@@ -16,10 +16,8 @@ account.
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
Policy.
<!-- prettier-ignore -->
> [!NOTE]
> See [quotas and pricing](quota-and-pricing.md) for the quota and
> pricing details that apply to your usage of the Gemini CLI.
**Note:** See [quotas and pricing](quota-and-pricing.md) for the quota and
pricing details that apply to your usage of the Gemini CLI.
## Supported authentication methods
+1 -3
View File
@@ -187,7 +187,5 @@ guide_, consider searching the Gemini CLI
If you can't find an issue similar to yours, consider creating a new GitHub
Issue with a detailed description. Pull requests are also welcome!
<!-- prettier-ignore -->
> [!NOTE]
> Issues tagged as "🔒Maintainers only" are reserved for project
> **Note:** Issues tagged as "🔒Maintainers only" are reserved for project
> maintainers. We will not accept pull requests related to these issues.
+9 -17
View File
@@ -176,8 +176,8 @@ Each server configuration supports the following properties:
enabled by default.
- **`excludeTools`** (string[]): List of tool names to exclude from this MCP
server. Tools listed here will not be available to the model, even if they are
exposed by the server. `excludeTools` takes precedence over `includeTools`. If
a tool is in both lists, it will be excluded.
exposed by the server. **Note:** `excludeTools` takes precedence over
`includeTools` - if a tool is in both lists, it will be excluded.
- **`targetAudience`** (string): The OAuth Client ID allowlisted on the
IAP-protected application you are trying to access. Used with
`authProviderType: 'service_account_impersonation'`.
@@ -238,9 +238,7 @@ This follows the security principle that if a variable is explicitly configured
by the user for a specific server, it constitutes informed consent to share that
specific data with that server.
<!-- prettier-ignore -->
> [!NOTE]
> Even when explicitly defined, you should avoid hardcoding secrets.
> **Note:** Even when explicitly defined, you should avoid hardcoding secrets.
> Instead, use environment variable expansion (e.g., `"MY_KEY": "$MY_KEY"`) to
> securely pull the value from your host environment at runtime.
@@ -285,12 +283,10 @@ When connecting to an OAuth-enabled server:
#### Browser redirect requirements
<!-- prettier-ignore -->
> [!IMPORTANT]
> OAuth authentication requires that your local machine can:
>
> - Open a web browser for authentication
> - Receive redirects on `http://localhost:7777/oauth/callback`
**Important:** OAuth authentication requires that your local machine can:
- Open a web browser for authentication
- Receive redirects on `http://localhost:7777/oauth/callback`
This feature will not work in:
@@ -581,9 +577,7 @@ every discovered MCP tool is assigned a strict namespace.
[Special syntax for MCP tools](../reference/policy-engine.md#special-syntax-for-mcp-tools)
in the Policy Engine documentation.
<!-- prettier-ignore -->
> [!WARNING]
> Do not use underscores (`_`) in your MCP server names (e.g., use
> **Warning:** Do not use underscores (`_`) in your MCP server names (e.g., use
> `my-server` rather than `my_server`). The policy parser splits Fully Qualified
> Names (`mcp_server_tool`) on the _first_ underscore following the `mcp_`
> prefix. If your server name contains an underscore, the parser will
@@ -1122,9 +1116,7 @@ command has no flags.
gemini mcp list
```
<!-- prettier-ignore -->
> [!NOTE]
> For security, `stdio` MCP servers (those using the
> **Note on Trust:** For security, `stdio` MCP servers (those using the
> `command` property) are only tested and displayed as "Connected" if the
> current folder is trusted. If the folder is untrusted, they will show as
> "Disconnected". Use `gemini trust` to trust the current folder.
+1 -3
View File
@@ -11,9 +11,7 @@ by the agent when you ask it to "start a plan" using natural language. In this
mode, the agent is restricted to read-only tools to allow for safe exploration
and planning.
<!-- prettier-ignore -->
> [!NOTE]
> This tool is not available when the CLI is in YOLO mode.
> **Note:** This tool is not available when the CLI is in YOLO mode.
- **Tool name:** `enter_plan_mode`
- **Display name:** Enter Plan Mode
+4 -4
View File
@@ -57,8 +57,8 @@ implementation, which does not support interactive commands.
### Showing color in output
To show color in the shell output, you need to set the `tools.shell.showColor`
setting to `true`. This setting only applies when
`tools.shell.enableInteractiveShell` is enabled.
setting to `true`. **Note: This setting only applies when
`tools.shell.enableInteractiveShell` is enabled.**
**Example `settings.json`:**
@@ -75,8 +75,8 @@ setting to `true`. This setting only applies when
### Setting the pager
You can set a custom pager for the shell output by setting the
`tools.shell.pager` setting. The default pager is `cat`. This setting only
applies when `tools.shell.enableInteractiveShell` is enabled.
`tools.shell.pager` setting. The default pager is `cat`. **Note: This setting
only applies when `tools.shell.enableInteractiveShell` is enabled.**
**Example `settings.json`:**
@@ -0,0 +1,5 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you with that."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and check if there is a heading"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"new_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"success":true,"summary":"SUCCESS_POLICY_TEST_COMPLETED"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Task completed successfully. The page has the heading \"Example Domain\"."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
+178
View File
@@ -0,0 +1,178 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll } from './test-helper.js';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
import stripAnsi from 'strip-ansi';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const chromeAvailable = (() => {
try {
if (process.platform === 'darwin') {
execSync(
'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"',
{
stdio: 'ignore',
},
);
} else if (process.platform === 'linux') {
execSync(
'which google-chrome || which chromium-browser || which chromium',
{ stdio: 'ignore' },
);
} else if (process.platform === 'win32') {
const chromePaths = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
`${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`,
];
const found = chromePaths.some((p) => existsSync(p));
if (!found) {
execSync('where chrome || where chromium', { stdio: 'ignore' });
}
} else {
return false;
}
return true;
} catch {
return false;
}
})();
describe.skipIf(!chromeAvailable)('browser-policy', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
agents: {
overrides: {
browser_agent: {
enabled: true,
},
},
browser: {
headless: true,
sessionMode: 'isolated',
allowedDomains: ['example.com'],
},
},
},
});
// Manually trust the folder to avoid the dialog and enable option 3
const geminiDir = join(rig.homeDir!, '.gemini');
mkdirSync(geminiDir, { recursive: true });
// Write to trustedFolders.json
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
const trustedFolders = {
[rig.testDir!]: 'TRUST_FOLDER',
};
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
// Force confirmation for browser agent.
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
// a rule in the user tier (4.x) like the one from this TOML.
// By removing the explicit mcp rule, the first MCP tool will still prompt
// due to default approvalMode = 'default', and then "Allow all" will correctly
// bypass subsequent tools.
const policyFile = join(rig.testDir!, 'force-confirm.toml');
writeFileSync(
policyFile,
`
[[rule]]
name = "Force confirm browser_agent"
toolName = "browser_agent"
decision = "ask_user"
priority = 200
`,
);
// Update settings.json in both project and home directories to point to the policy file
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
const settingsPath = join(baseDir, '.gemini', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
settings.policyPaths = [policyFile];
// Ensure folder trust is enabled
settings.security = settings.security || {};
settings.security.folderTrust = settings.security.folderTrust || {};
settings.security.folderTrust.enabled = true;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
}
}
const run = await rig.runInteractive({
approvalMode: 'default',
env: {
GEMINI_CLI_INTEGRATION_TEST: 'true',
},
});
await run.sendKeys(
'Open https://example.com and check if there is a heading\r',
);
await run.sendKeys('\r');
// Handle confirmations.
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
await poll(
() => stripAnsi(run.output).toLowerCase().includes('action required'),
60000,
1000,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 2000));
// Handle privacy notice
await poll(
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
5000,
100,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 5000));
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
await poll(
() => {
const stripped = stripAnsi(run.output).toLowerCase();
return (
stripped.includes('new_page') &&
stripped.includes('allow all server tools for this session')
);
},
60000,
1000,
);
// Select "Allow all server tools for this session" (option 3)
await run.sendKeys('3\r');
await new Promise((r) => setTimeout(r, 30000));
const output = stripAnsi(run.output).toLowerCase();
expect(output).toContain('browser_agent');
expect(output).toContain('completed successfully');
});
});
+84
View File
@@ -177,6 +177,9 @@ describe('GeminiAgent', () => {
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
mockSettings = {
merged: {
@@ -656,6 +659,12 @@ describe('Session', () => {
getGitService: vi.fn().mockResolvedValue({} as GitService),
waitForMcpInit: vi.fn(),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
get toolRegistry() {
return mockToolRegistry;
},
} as unknown as Mocked<Config>;
mockConnection = {
sessionUpdate: vi.fn(),
@@ -894,6 +903,9 @@ describe('Session', () => {
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'completed',
title: 'Test Tool',
locations: [],
kind: 'read',
}),
}),
);
@@ -1306,6 +1318,18 @@ describe('Session', () => {
expect(path.resolve).toHaveBeenCalled();
expect(fs.stat).toHaveBeenCalled();
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'completed',
title: 'Read files',
locations: [],
kind: 'read',
}),
}),
);
// Verify ReadManyFilesTool was used (implicitly by checking if sendMessageStream was called with resolved content)
// Since we mocked ReadManyFilesTool to return specific content, we can check the args passed to sendMessageStream
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
@@ -1321,6 +1345,65 @@ describe('Session', () => {
);
});
it('should handle @path resolution error', async () => {
(path.resolve as unknown as Mock).mockReturnValue('/tmp/error.txt');
(fs.stat as unknown as Mock).mockResolvedValue({
isDirectory: () => false,
});
(isWithinRoot as unknown as Mock).mockReturnValue(true);
const MockReadManyFilesTool = ReadManyFilesTool as unknown as Mock;
MockReadManyFilesTool.mockImplementationOnce(() => ({
name: 'read_many_files',
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Read files',
toolLocations: () => [],
execute: vi.fn().mockRejectedValue(new Error('File read failed')),
}),
}));
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await expect(
session.prompt({
sessionId: 'session-1',
prompt: [
{ type: 'text', text: 'Read' },
{
type: 'resource_link',
uri: 'file://error.txt',
mimeType: 'text/plain',
name: 'error.txt',
},
],
}),
).rejects.toThrow('File read failed');
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'failed',
content: expect.arrayContaining([
expect.objectContaining({
content: expect.objectContaining({
text: expect.stringMatching(/File read failed/),
}),
}),
]),
kind: 'read',
}),
}),
);
});
it('should handle cancellation during prompt', async () => {
let streamController: ReadableStreamDefaultController<unknown>;
const stream = new ReadableStream({
@@ -1434,6 +1517,7 @@ describe('Session', () => {
content: expect.objectContaining({ text: 'Tool failed' }),
}),
]),
kind: 'read',
}),
}),
);
+51 -34
View File
@@ -47,6 +47,7 @@ import {
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
getDisplayString,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { AcpFileSystemService } from './fileSystemService.js';
@@ -104,7 +105,7 @@ export class GeminiAgent {
private customHeaders: Record<string, string> | undefined;
constructor(
private config: Config,
private context: AgentLoopContext,
private settings: LoadedSettings,
private argv: CliArgs,
private connection: acp.AgentSideConnection,
@@ -148,7 +149,7 @@ export class GeminiAgent {
},
];
await this.config.initialize();
await this.context.config.initialize();
const version = await getVersion();
return {
protocolVersion: acp.PROTOCOL_VERSION,
@@ -220,7 +221,7 @@ export class GeminiAgent {
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.config.refreshAuth(
await this.context.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
@@ -537,7 +538,7 @@ export class Session {
constructor(
private readonly id: string,
private readonly chat: GeminiChat,
private readonly config: Config,
private readonly context: AgentLoopContext,
private readonly connection: acp.AgentSideConnection,
private readonly settings: LoadedSettings,
) {}
@@ -552,13 +553,15 @@ export class Session {
}
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
const availableModes = buildAvailableModes(this.config.isPlanEnabled());
const availableModes = buildAvailableModes(
this.context.config.isPlanEnabled(),
);
const mode = availableModes.find((m) => m.id === modeId);
if (!mode) {
throw new Error(`Invalid or unavailable mode: ${modeId}`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this.config.setApprovalMode(mode.id as ApprovalMode);
this.context.config.setApprovalMode(mode.id as ApprovalMode);
return {};
}
@@ -579,7 +582,7 @@ export class Session {
}
setModel(modelId: acp.ModelId): acp.SetSessionModelResponse {
this.config.setModel(modelId);
this.context.config.setModel(modelId);
return {};
}
@@ -634,7 +637,7 @@ export class Session {
}
}
const tool = this.config.getToolRegistry().getTool(toolCall.name);
const tool = this.context.toolRegistry.getTool(toolCall.name);
await this.sendUpdate({
sessionUpdate: 'tool_call',
@@ -658,7 +661,7 @@ export class Session {
const pendingSend = new AbortController();
this.pendingPrompt = pendingSend;
await this.config.waitForMcpInit();
await this.context.config.waitForMcpInit();
const promptId = Math.random().toString(16).slice(2);
const chat = this.chat;
@@ -712,8 +715,8 @@ export class Session {
try {
const model = resolveModel(
this.config.getModel(),
(await this.config.getGemini31Launched?.()) ?? false,
this.context.config.getModel(),
(await this.context.config.getGemini31Launched?.()) ?? false,
);
const responseStream = await chat.sendMessageStream(
{ model },
@@ -804,9 +807,9 @@ export class Session {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
parts: Part[],
): Promise<boolean> {
const gitService = await this.config.getGitService();
const gitService = await this.context.config.getGitService();
const commandContext = {
config: this.config,
agentContext: this.context,
settings: this.settings,
git: gitService,
sendMessage: async (text: string) => {
@@ -842,7 +845,7 @@ export class Session {
const errorResponse = (error: Error) => {
const durationMs = Date.now() - startTime;
logToolCall(
this.config,
this.context.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -872,7 +875,7 @@ export class Session {
return errorResponse(new Error('Missing function name'));
}
const toolRegistry = this.config.getToolRegistry();
const toolRegistry = this.context.toolRegistry;
const tool = toolRegistry.getTool(fc.name);
if (!tool) {
@@ -908,7 +911,10 @@ export class Session {
const params: acp.RequestPermissionRequest = {
sessionId: this.id,
options: toPermissionOptions(confirmationDetails, this.config),
options: toPermissionOptions(
confirmationDetails,
this.context.config,
),
toolCall: {
toolCallId: callId,
status: 'pending',
@@ -966,12 +972,15 @@ export class Session {
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
locations: invocation.toolLocations(),
kind: toAcpToolKind(tool.kind),
});
const durationMs = Date.now() - startTime;
logToolCall(
this.config,
this.context.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -985,7 +994,7 @@ export class Session {
),
);
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
{
status: CoreToolCallStatus.Success,
request: {
@@ -1003,8 +1012,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
this.context.config.getActiveModel(),
this.context.config,
),
resultDisplay: toolResult.returnDisplay,
error: undefined,
@@ -1017,8 +1026,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
this.context.config.getActiveModel(),
this.context.config,
);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
@@ -1030,9 +1039,10 @@ export class Session {
content: [
{ type: 'content', content: { type: 'text', text: error.message } },
],
kind: toAcpToolKind(tool.kind),
});
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
{
status: CoreToolCallStatus.Error,
request: {
@@ -1118,18 +1128,18 @@ export class Session {
const atPathToResolvedSpecMap = new Map<string, string>();
// Get centralized file discovery service
const fileDiscovery = this.config.getFileService();
const fileDiscovery = this.context.config.getFileService();
const fileFilteringOptions: FilterFilesOptions =
this.config.getFileFilteringOptions();
this.context.config.getFileFilteringOptions();
const pathSpecsToRead: string[] = [];
const contentLabelsForDisplay: string[] = [];
const ignoredPaths: string[] = [];
const toolRegistry = this.config.getToolRegistry();
const toolRegistry = this.context.toolRegistry;
const readManyFilesTool = new ReadManyFilesTool(
this.config,
this.config.getMessageBus(),
this.context.config,
this.context.messageBus,
);
const globTool = toolRegistry.getTool('glob');
@@ -1148,8 +1158,11 @@ export class Session {
let currentPathSpec = pathName;
let resolvedSuccessfully = false;
try {
const absolutePath = path.resolve(this.config.getTargetDir(), pathName);
if (isWithinRoot(absolutePath, this.config.getTargetDir())) {
const absolutePath = path.resolve(
this.context.config.getTargetDir(),
pathName,
);
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
const stats = await fs.stat(absolutePath);
if (stats.isDirectory()) {
currentPathSpec = pathName.endsWith('/')
@@ -1169,7 +1182,7 @@ export class Session {
}
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
if (this.config.getEnableRecursiveFileSearch() && globTool) {
if (this.context.config.getEnableRecursiveFileSearch() && globTool) {
this.debug(
`Path ${pathName} not found directly, attempting glob search.`,
);
@@ -1177,7 +1190,7 @@ export class Session {
const globResult = await globTool.buildAndExecute(
{
pattern: `**/*${pathName}*`,
path: this.config.getTargetDir(),
path: this.context.config.getTargetDir(),
},
abortSignal,
);
@@ -1191,7 +1204,7 @@ export class Session {
if (lines.length > 1 && lines[1]) {
const firstMatchAbsolute = lines[1].trim();
currentPathSpec = path.relative(
this.config.getTargetDir(),
this.context.config.getTargetDir(),
firstMatchAbsolute,
);
this.debug(
@@ -1324,7 +1337,10 @@ export class Session {
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
locations: invocation.toolLocations(),
kind: toAcpToolKind(readManyFilesTool.kind),
});
if (Array.isArray(result.llmContent)) {
const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
@@ -1368,6 +1384,7 @@ export class Session {
},
},
],
kind: toAcpToolKind(readManyFilesTool.kind),
});
throw error;
@@ -1402,7 +1419,7 @@ export class Session {
}
debug(msg: string) {
if (this.config.getDebugMode()) {
if (this.context.config.getDebugMode()) {
debugLogger.warn(msg);
}
}
+6 -2
View File
@@ -97,6 +97,9 @@ describe('GeminiAgent Session Resume', () => {
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
get config() {
return this;
},
} as unknown as Mocked<Config>;
mockSettings = {
merged: {
@@ -158,9 +161,10 @@ describe('GeminiAgent Session Resume', () => {
],
};
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockConfig as any).toolRegistry = {
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
});
};
(SessionSelector as unknown as Mock).mockImplementation(() => ({
resolveSession: vi.fn().mockResolvedValue({
+10 -9
View File
@@ -53,7 +53,7 @@ export class ListExtensionsCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(context.config);
const extensions = listExtensions(context.agentContext.config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
@@ -134,7 +134,7 @@ export class EnableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
context.agentContext.config,
args,
'enable',
);
@@ -156,7 +156,8 @@ export class EnableExtensionCommand implements Command {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.config.getMcpClientManager();
const mcpClientManager =
context.agentContext.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers),
);
@@ -191,7 +192,7 @@ export class DisableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
context.agentContext.config,
args,
'disable',
);
@@ -223,7 +224,7 @@ export class InstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -268,7 +269,7 @@ export class LinkExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -313,7 +314,7 @@ export class UninstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -369,7 +370,7 @@ export class RestartExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot restart extensions.' };
}
@@ -424,7 +425,7 @@ export class UpdateExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot update extensions.' };
}
+1 -1
View File
@@ -22,7 +22,7 @@ export class InitCommand implements Command {
context: CommandContext,
_args: string[] = [],
): Promise<CommandExecutionResponse> {
const targetDir = context.config.getTargetDir();
const targetDir = context.agentContext.config.getTargetDir();
if (!targetDir) {
throw new Error('Command requires a workspace.');
}
+6 -6
View File
@@ -49,7 +49,7 @@ export class ShowMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = showMemory(context.config);
const result = showMemory(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -63,7 +63,7 @@ export class RefreshMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = await refreshMemory(context.config);
const result = await refreshMemory(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -76,7 +76,7 @@ export class ListMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = listMemoryFiles(context.config);
const result = listMemoryFiles(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -95,7 +95,7 @@ export class AddMemoryCommand implements Command {
return { name: this.name, data: result.content };
}
const toolRegistry = context.config.getToolRegistry();
const toolRegistry = context.agentContext.toolRegistry;
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
@@ -106,10 +106,10 @@ export class AddMemoryCommand implements Command {
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.config.sandboxManager,
sandboxManager: context.agentContext.sandboxManager,
},
});
await refreshMemory(context.config);
await refreshMemory(context.agentContext.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
+3 -2
View File
@@ -29,7 +29,8 @@ export class RestoreCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const { config, git: gitService } = context;
const { agentContext: agentContext, git: gitService } = context;
const { config } = agentContext;
const argsStr = args.join(' ');
try {
@@ -116,7 +117,7 @@ export class ListCheckpointsCommand implements Command {
readonly description = 'Lists all available checkpoints.';
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
const { config } = context;
const { config } = context.agentContext;
try {
if (!config.getCheckpointingEnabled()) {
+2 -2
View File
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, GitService } from '@google/gemini-cli-core';
import type { AgentLoopContext, GitService } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
export interface CommandContext {
config: Config;
agentContext: AgentLoopContext;
settings: LoadedSettings;
git?: GitService;
sendMessage: (text: string) => Promise<void>;
+8
View File
@@ -419,6 +419,14 @@ export async function main() {
}
}
if (settings.merged.advanced.autoConfigureMemory) {
const heapStats = v8.getHeapStatistics();
const currentMaxOldSpaceSizeMb = Math.floor(
heapStats.heap_size_limit / 1024 / 1024,
);
writeToStderr(`Allocated memory: ${currentMaxOldSpaceSizeMb} MB\n`);
}
// We are now past the logic handling potentially launching a child process
// to run Gemini CLI. It is now safe to perform expensive initialization that
// may have side effects.
@@ -29,7 +29,7 @@ describe('Model Steering Integration', () => {
configOverrides: { modelSteering: true },
});
await rig.initialize();
rig.render();
await rig.render();
await rig.waitForIdle();
rig.setToolPolicy('list_directory', PolicyDecision.ASK_USER);
@@ -65,9 +65,9 @@ export const handleSlashCommand = async (
const logger = new Logger(config?.getSessionId() || '', config?.storage);
const context: CommandContext = {
const commandContext: CommandContext = {
services: {
config,
agentContext: config,
settings,
git: undefined,
logger,
@@ -84,7 +84,7 @@ export const handleSlashCommand = async (
},
};
const result = await commandToExecute.action(context, args);
const result = await commandToExecute.action(commandContext, args);
if (result) {
switch (result.type) {
@@ -31,11 +31,14 @@ describe('AtFileProcessor', () => {
mockConfig = {
// The processor only passes the config through, so we don't need a full mock.
get config() {
return this;
},
} as unknown as Config;
context = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig,
},
});
@@ -60,7 +63,7 @@ describe('AtFileProcessor', () => {
const prompt: PartUnion[] = [{ text: 'Analyze @{file.txt}' }];
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
const result = await processor.process(prompt, contextWithoutConfig);
@@ -25,7 +25,7 @@ export class AtFileProcessor implements IPromptProcessor {
input: PromptPipelineContent,
context: CommandContext,
): Promise<PromptPipelineContent> {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
return input;
}
@@ -89,6 +89,9 @@ describe('ShellProcessor', () => {
getPolicyEngine: vi.fn().mockReturnValue({
check: mockPolicyEngineCheck,
}),
get config() {
return this as unknown as Config;
},
};
context = createMockCommandContext({
@@ -98,7 +101,7 @@ describe('ShellProcessor', () => {
args: 'default args',
},
services: {
config: mockConfig as Config,
agentContext: mockConfig as Config,
},
session: {
sessionShellAllowlist: new Set(),
@@ -120,7 +123,7 @@ describe('ShellProcessor', () => {
const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}');
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -74,7 +74,7 @@ export class ShellProcessor implements IPromptProcessor {
];
}
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
throw new Error(
`Security configuration not loaded. Cannot verify shell command permissions for '${this.commandName}'. Aborting.`,
+2 -8
View File
@@ -5,7 +5,6 @@
*/
import { describe, it, afterEach, expect } from 'vitest';
import { act } from 'react';
import { AppRig } from './AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -31,7 +30,7 @@ describe('AppRig', () => {
configOverrides: { modelSteering: true },
});
await rig.initialize();
rig.render();
await rig.render();
await rig.waitForIdle();
// Set breakpoints on the canonical tool names
@@ -69,12 +68,7 @@ describe('AppRig', () => {
);
rig = new AppRig({ fakeResponsesPath });
await rig.initialize();
await act(async () => {
rig!.render();
// Allow async initializations (like banners) to settle within the act boundary
await new Promise((resolve) => setTimeout(resolve, 0));
});
await rig.render();
// Wait for initial render
await rig.waitForIdle();
+5 -5
View File
@@ -11,7 +11,7 @@ import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { AppContainer } from '../ui/AppContainer.js';
import { renderWithProviders } from './render.js';
import { renderWithProviders, type RenderInstance } from './render.js';
import {
makeFakeConfig,
type Config,
@@ -155,7 +155,7 @@ export interface PendingConfirmation {
}
export class AppRig {
private renderResult: ReturnType<typeof renderWithProviders> | undefined;
private renderResult: RenderInstance | undefined;
private config: Config | undefined;
private settings: LoadedSettings | undefined;
private testDir: string;
@@ -393,12 +393,12 @@ export class AppRig {
return isAnyToolActive || isAwaitingConfirmation;
}
render() {
async render() {
if (!this.config || !this.settings)
throw new Error('AppRig not initialized');
act(() => {
this.renderResult = renderWithProviders(
await act(async () => {
this.renderResult = await renderWithProviders(
<AppContainer
config={this.config!}
version="test-version"
@@ -46,15 +46,19 @@ describe('createMockCommandContext', () => {
const overrides = {
services: {
config: mockConfig,
agentContext: { config: mockConfig },
},
};
const context = createMockCommandContext(overrides);
expect(context.services.config).toBeDefined();
expect(context.services.config?.getModel()).toBe('gemini-pro');
expect(context.services.config?.getProjectRoot()).toBe('/test/project');
expect(context.services.agentContext).toBeDefined();
expect(context.services.agentContext?.config?.getModel()).toBe(
'gemini-pro',
);
expect(context.services.agentContext?.config?.getProjectRoot()).toBe(
'/test/project',
);
// Verify a default property on the same nested object is still there
expect(context.services.logger).toBeDefined();
@@ -36,7 +36,7 @@ export const createMockCommandContext = (
args: '',
},
services: {
config: null,
agentContext: null,
settings: {
merged: defaultMergedSettings,
+33 -68
View File
@@ -16,8 +16,6 @@ import { vi } from 'vitest';
import stripAnsi from 'strip-ansi';
import type React from 'react';
import { act, useState } from 'react';
import os from 'node:os';
import path from 'node:path';
import type { LoadedSettings } from '../config/settings.js';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { SettingsContext } from '../ui/contexts/SettingsContext.js';
@@ -44,7 +42,7 @@ import {
type OverflowState,
} from '../ui/contexts/OverflowContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
import { createMockSettings } from './settings.js';
@@ -53,6 +51,7 @@ import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
import { generateSvgForTerminal } from './svg.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
export const persistentStateMock = new FakePersistentState();
@@ -66,7 +65,9 @@ if (process.env['NODE_ENV'] === 'test') {
}
vi.mock('../utils/persistentState.js', () => ({
persistentState: persistentStateMock,
get persistentState() {
return persistentStateMock;
},
}));
vi.mock('../ui/utils/terminalUtils.js', () => ({
@@ -486,50 +487,6 @@ export const simulateClick = async (
});
};
let mockConfigInternal: Config | undefined;
const getMockConfigInternal = (): Config => {
if (!mockConfigInternal) {
mockConfigInternal = makeFakeConfig({
targetDir: os.tmpdir(),
enableEventDrivenScheduler: true,
});
}
return mockConfigInternal;
};
const configProxy = new Proxy({} as Config, {
get(_target, prop) {
if (prop === 'getTargetDir') {
return () =>
path.join(
path.parse(process.cwd()).root,
'Users',
'test',
'project',
'foo',
'bar',
'and',
'some',
'more',
'directories',
'to',
'make',
'it',
'long',
);
}
if (prop === 'getUseBackgroundColor') {
return () => true;
}
const internal = getMockConfigInternal();
if (prop in internal) {
return internal[prop as keyof typeof internal];
}
throw new Error(`mockConfig does not have property ${String(prop)}`);
},
});
export const mockSettings = createMockSettings();
// A minimal mock UIState to satisfy the context provider.
@@ -639,7 +596,7 @@ const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
return <>{children}</>;
};
export const renderWithProviders = (
export const renderWithProviders = async (
component: React.ReactElement,
{
shellFocus = true,
@@ -647,8 +604,7 @@ export const renderWithProviders = (
uiState: providedUiState,
width,
mouseEventsEnabled = false,
config = configProxy as unknown as Config,
config,
uiActions,
persistentState,
appState = mockAppState,
@@ -666,13 +622,15 @@ export const renderWithProviders = (
};
appState?: AppState;
} = {},
): RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
} => {
): Promise<
RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
}
> => {
const baseState: UIState = new Proxy(
{ ...baseMockUiState, ...providedUiState },
{
@@ -701,8 +659,15 @@ export const renderWithProviders = (
persistentStateMock.mockClear();
const terminalWidth = width ?? baseState.terminalWidth;
const finalSettings = settings;
const finalConfig = config;
if (!config) {
config = await loadCliConfig(
settings.merged,
'random-session-id',
{} as unknown as CliArgs,
{ cwd: '/' },
);
}
const mainAreaWidth = terminalWidth;
@@ -732,8 +697,8 @@ export const renderWithProviders = (
const wrapWithProviders = (comp: React.ReactElement) => (
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}>
<ConfigContext.Provider value={config}>
<SettingsContext.Provider value={settings}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider>
<ShellFocusContext.Provider value={shellFocus}>
@@ -744,7 +709,7 @@ export const renderWithProviders = (
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={finalConfig}
config={config}
toolCalls={allToolCalls}
>
<AskUserActionsProvider
@@ -863,7 +828,7 @@ export function renderHook<Result, Props>(
return { result, rerender, unmount, waitUntilReady, generateSvg };
}
export function renderHookWithProviders<Result, Props>(
export async function renderHookWithProviders<Result, Props>(
renderCallback: (props: Props) => Result,
options: {
initialProps?: Props;
@@ -876,13 +841,13 @@ export function renderHookWithProviders<Result, Props>(
mouseEventsEnabled?: boolean;
config?: Config;
} = {},
): {
): Promise<{
result: { current: Result };
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
generateSvg: () => string;
} {
}> {
const result = { current: undefined as unknown as Result };
let setPropsFn: ((props: Props) => void) | undefined;
@@ -901,8 +866,8 @@ export function renderHookWithProviders<Result, Props>(
let renderResult: ReturnType<typeof render>;
act(() => {
renderResult = renderWithProviders(
await act(async () => {
renderResult = await renderWithProviders(
<Wrapper>
{}
<TestComponent initialProps={options.initialProps as Props} />
+11 -21
View File
@@ -94,11 +94,10 @@ describe('App', () => {
};
it('should render main content and composer when not quitting', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
@@ -116,11 +115,10 @@ describe('App', () => {
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: quittingUIState,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
@@ -138,11 +136,10 @@ describe('App', () => {
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: quittingUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -159,11 +156,10 @@ describe('App', () => {
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: dialogUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -187,11 +183,10 @@ describe('App', () => {
[stateKey]: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -205,11 +200,10 @@ describe('App', () => {
it('should render ScreenReaderAppLayout when screen reader is enabled', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -225,11 +219,10 @@ describe('App', () => {
it('should render DefaultAppLayout when screen reader is not enabled', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -281,7 +274,7 @@ describe('App', () => {
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: stateWithConfirmingTool,
@@ -302,11 +295,10 @@ describe('App', () => {
describe('Snapshots', () => {
it('renders default layout correctly', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -317,11 +309,10 @@ describe('App', () => {
it('renders screen reader layout correctly', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -335,11 +326,10 @@ describe('App', () => {
...mockUIState,
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<App />,
{
uiState: dialogUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
+4 -4
View File
@@ -212,7 +212,7 @@ import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
@@ -294,7 +294,7 @@ describe('AppContainer State Management', () => {
const mockedUseSettingsCommand = useSettingsCommand as Mock;
const mockedUseModelCommand = useModelCommand as Mock;
const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock;
const mockedUseConsoleMessages = useConsoleMessages as Mock;
const mockedUseConsoleMessages = useErrorCount as Mock;
const mockedUseGeminiStream = useGeminiStream as Mock;
const mockedUseVim = useVim as Mock;
const mockedUseFolderTrust = useFolderTrust as Mock;
@@ -396,9 +396,9 @@ describe('AppContainer State Management', () => {
confirmationRequest: null,
});
mockedUseConsoleMessages.mockReturnValue({
consoleMessages: [],
errorCount: 0,
handleNewMessage: vi.fn(),
clearConsoleMessages: vi.fn(),
clearErrorCount: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
+4 -23
View File
@@ -103,7 +103,7 @@ import {
useOverflowActions,
useOverflowState,
} from './contexts/OverflowContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
import { calculateMainAreaWidth } from './utils/ui-sizing.js';
@@ -552,8 +552,7 @@ export const AppContainer = (props: AppContainerProps) => {
};
}, [settings]);
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
useConsoleMessages();
const { errorCount, clearErrorCount } = useErrorCount();
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config);
// Derive widths for InputPrompt using shared helper
@@ -1372,11 +1371,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
triggerExpandHint(null);
historyManager.clearItems();
clearConsoleMessagesState();
clearErrorCount();
refreshStatic();
}, [
historyManager,
clearConsoleMessagesState,
clearErrorCount,
refreshStatic,
reset,
triggerExpandHint,
@@ -1983,22 +1982,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
};
}, [historyManager]);
const filteredConsoleMessages = useMemo(() => {
if (config.getDebugMode()) {
return consoleMessages;
}
return consoleMessages.filter((msg) => msg.type !== 'debug');
}, [consoleMessages, config]);
// Computed values
const errorCount = useMemo(
() =>
filteredConsoleMessages
.filter((msg) => msg.type === 'error')
.reduce((total, msg) => total + msg.count, 0),
[filteredConsoleMessages],
);
const nightly = props.version.includes('nightly');
const dialogsVisible =
@@ -2233,7 +2216,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
ctrlCPressedOnce: ctrlCPressCount >= 1,
@@ -2361,7 +2343,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
ctrlCPressCount,
@@ -53,7 +53,7 @@ describe('IdeIntegrationNudge', () => {
});
it('renders correctly with default options', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} />,
);
await waitUntilReady();
@@ -68,7 +68,7 @@ describe('IdeIntegrationNudge', () => {
it('handles "Yes" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = renderWithProviders(
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -89,7 +89,7 @@ describe('IdeIntegrationNudge', () => {
it('handles "No" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = renderWithProviders(
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -115,7 +115,7 @@ describe('IdeIntegrationNudge', () => {
it('handles "Dismiss" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = renderWithProviders(
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -146,7 +146,7 @@ describe('IdeIntegrationNudge', () => {
it('handles Escape key press', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = renderWithProviders(
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -173,9 +173,10 @@ describe('IdeIntegrationNudge', () => {
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp');
const onComplete = vi.fn();
const { lastFrame, stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
const { lastFrame, stdin, waitUntilReady, unmount } =
await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
+17 -17
View File
@@ -143,7 +143,7 @@ describe('AuthDialog', () => {
for (const [key, value] of Object.entries(env)) {
vi.stubEnv(key, value as string);
}
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -161,7 +161,7 @@ describe('AuthDialog', () => {
it('filters auth types when enforcedType is set', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -173,7 +173,7 @@ describe('AuthDialog', () => {
it('sets initial index to 0 when enforcedType is set', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -213,7 +213,7 @@ describe('AuthDialog', () => {
},
])('selects initial auth type $desc', async ({ setup, expected }) => {
setup();
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -226,7 +226,7 @@ describe('AuthDialog', () => {
describe('handleAuthSelect', () => {
it('calls onAuthError if validation fails', async () => {
mockedValidateAuthMethod.mockReturnValue('Invalid method');
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -245,7 +245,7 @@ describe('AuthDialog', () => {
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -261,7 +261,7 @@ describe('AuthDialog', () => {
it('sets auth context with empty object for other auth types', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -278,7 +278,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -297,7 +297,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
// props.settings.merged.security.auth.selectedType is undefined here
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -316,7 +316,7 @@ describe('AuthDialog', () => {
// process.env['GEMINI_API_KEY'] is not set
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -337,7 +337,7 @@ describe('AuthDialog', () => {
props.settings.merged.security.auth.selectedType =
AuthType.LOGIN_WITH_GOOGLE;
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -360,7 +360,7 @@ describe('AuthDialog', () => {
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -383,7 +383,7 @@ describe('AuthDialog', () => {
it('displays authError when provided', async () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -429,7 +429,7 @@ describe('AuthDialog', () => {
},
])('$desc', async ({ setup, expectations }) => {
setup();
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -442,7 +442,7 @@ describe('AuthDialog', () => {
describe('Snapshots', () => {
it('renders correctly with default props', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -452,7 +452,7 @@ describe('AuthDialog', () => {
it('renders correctly with auth error', async () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -462,7 +462,7 @@ describe('AuthDialog', () => {
it('renders correctly with enforced auth type', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -73,7 +73,7 @@ describe('BannedAccountDialog', () => {
});
it('renders the suspension message from accountSuspensionInfo', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -89,7 +89,7 @@ describe('BannedAccountDialog', () => {
});
it('renders menu options with appeal link text from response', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -109,7 +109,7 @@ describe('BannedAccountDialog', () => {
const infoWithoutUrl: AccountSuspensionInfo = {
message: 'Account suspended.',
};
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={infoWithoutUrl}
onExit={onExit}
@@ -129,7 +129,7 @@ describe('BannedAccountDialog', () => {
message: 'Account suspended.',
appealUrl: 'https://example.com/appeal',
};
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={infoWithoutLinkText}
onExit={onExit}
@@ -143,7 +143,7 @@ describe('BannedAccountDialog', () => {
});
it('opens browser when appeal option is selected', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -162,7 +162,7 @@ describe('BannedAccountDialog', () => {
it('shows URL when browser cannot be launched', async () => {
mockedShouldLaunchBrowser.mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -180,7 +180,7 @@ describe('BannedAccountDialog', () => {
});
it('calls onExit when "Exit" is selected', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -196,7 +196,7 @@ describe('BannedAccountDialog', () => {
});
it('calls onChangeAuth when "Change authentication" is selected', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -212,7 +212,7 @@ describe('BannedAccountDialog', () => {
});
it('exits on escape key', async () => {
const { waitUntilReady, unmount } = renderWithProviders(
const { waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -227,7 +227,7 @@ describe('BannedAccountDialog', () => {
});
it('renders snapshot correctly', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -36,10 +36,12 @@ describe('aboutCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
agentContext: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
},
},
settings: {
merged: {
@@ -57,9 +59,10 @@ describe('aboutCommand', () => {
} as unknown as CommandContext);
vi.mocked(getVersion).mockResolvedValue('test-version');
vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
'test-model',
);
vi.spyOn(
mockContext.services.agentContext!.config,
'getModel',
).mockReturnValue('test-model');
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
Object.defineProperty(process, 'platform', {
value: 'test-os',
@@ -160,9 +163,9 @@ describe('aboutCommand', () => {
});
it('should display the tier when getUserTierName returns a value', async () => {
vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue(
'Enterprise Tier',
);
vi.mocked(
mockContext.services.agentContext!.config.getUserTierName,
).mockReturnValue('Enterprise Tier');
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
+4 -3
View File
@@ -34,7 +34,8 @@ export const aboutCommand: SlashCommand = {
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion = context.services.config?.getModel() || 'Unknown';
const modelVersion =
context.services.agentContext?.config.getModel() || 'Unknown';
const cliVersion = await getVersion();
const selectedAuthType =
context.services.settings.merged.security.auth.selectedType || '';
@@ -48,7 +49,7 @@ export const aboutCommand: SlashCommand = {
});
const userEmail = cachedAccount ?? undefined;
const tier = context.services.config?.getUserTierName();
const tier = context.services.agentContext?.config.getUserTierName();
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
type: MessageType.ABOUT,
@@ -68,7 +69,7 @@ export const aboutCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
if (!context.services.agentContext?.config.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -26,6 +26,7 @@ describe('agentsCommand', () => {
let mockContext: ReturnType<typeof createMockCommandContext>;
let mockConfig: {
getAgentRegistry: ReturnType<typeof vi.fn>;
config: Config;
};
beforeEach(() => {
@@ -37,11 +38,14 @@ describe('agentsCommand', () => {
getAllAgentNames: vi.fn().mockReturnValue([]),
reload: vi.fn(),
}),
get config() {
return this as unknown as Config;
},
};
mockContext = createMockCommandContext({
services: {
config: mockConfig as unknown as Config,
agentContext: mockConfig as unknown as Config,
settings: {
workspace: { path: '/mock/path' },
merged: { agents: { overrides: {} } },
@@ -53,7 +57,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -226,7 +230,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for enable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const enableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'enable',
@@ -332,7 +336,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for disable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const disableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'disable',
@@ -433,7 +437,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const configCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'config',
+11 -8
View File
@@ -21,7 +21,7 @@ const agentsListCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -61,7 +61,8 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -137,7 +138,8 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -216,7 +218,7 @@ async function configAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -266,7 +268,8 @@ async function configAction(
}
function completeAgentsToEnable(context: CommandContext, partialArg: string) {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) return [];
const overrides = settings.merged.agents.overrides;
@@ -278,7 +281,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
}
function completeAgentsToDisable(context: CommandContext, partialArg: string) {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -287,7 +290,7 @@ function completeAgentsToDisable(context: CommandContext, partialArg: string) {
}
function completeAllAgents(context: CommandContext, partialArg: string) {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -328,7 +331,7 @@ const agentsReloadCommand: SlashCommand = {
description: 'Reload the agent registry',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
const agentRegistry = config?.getAgentRegistry();
if (!agentRegistry) {
return {
@@ -9,6 +9,7 @@ import { authCommand } from './authCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { SettingScope } from '../../config/settings.js';
import type { GeminiClient } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
@@ -24,8 +25,10 @@ describe('authCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: vi.fn(),
agentContext: {
geminiClient: {
stripThoughtsFromHistory: vi.fn(),
},
},
},
});
@@ -101,17 +104,19 @@ describe('authCommand', () => {
const mockStripThoughts = vi.fn();
const mockClient = {
stripThoughtsFromHistory: mockStripThoughts,
} as unknown as ReturnType<
NonNullable<typeof mockContext.services.config>['getGeminiClient']
>;
if (mockContext.services.config) {
mockContext.services.config.getGeminiClient = vi.fn(() => mockClient);
} as unknown as GeminiClient;
if (mockContext.services.agentContext?.config) {
mockContext.services.agentContext.config.getGeminiClient = vi.fn(
() => mockClient,
);
}
await logoutCommand!.action!(mockContext, '');
expect(mockStripThoughts).toHaveBeenCalled();
expect(
mockContext.services.agentContext?.geminiClient
.stripThoughtsFromHistory,
).toHaveBeenCalled();
});
it('should return logout action to signal explicit state change', async () => {
@@ -123,7 +128,7 @@ describe('authCommand', () => {
it('should handle missing config gracefully', async () => {
const logoutCommand = authCommand.subCommands?.[1];
mockContext.services.config = null;
mockContext.services.agentContext = null;
const result = await logoutCommand!.action!(mockContext, '');
+1 -1
View File
@@ -39,7 +39,7 @@ const authLogoutCommand: SlashCommand = {
undefined,
);
// Strip thoughts from history instead of clearing completely
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
context.services.agentContext?.geminiClient.stripThoughtsFromHistory();
// Return logout action to signal explicit state change
return {
type: 'logout',
+29 -23
View File
@@ -83,16 +83,18 @@ describe('bugCommand', () => {
it('should generate the default GitHub issue URL', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
geminiClient: {
getChat: () => ({
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
},
},
});
@@ -126,18 +128,20 @@ describe('bugCommand', () => {
];
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
geminiClient: {
getChat: () => ({
getHistory: () => history,
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
},
@@ -172,16 +176,18 @@ describe('bugCommand', () => {
'https://internal.bug-tracker.com/new?desc={title}&details={info}';
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getGeminiClient: () => ({
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
geminiClient: {
getChat: () => ({
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
},
},
});
+4 -4
View File
@@ -32,8 +32,8 @@ export const bugCommand: SlashCommand = {
autoExecute: false,
action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim();
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const osVersion = `${process.platform} ${process.version}`;
let sandboxEnv = 'no sandbox';
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
@@ -73,7 +73,7 @@ export const bugCommand: SlashCommand = {
info += `* **IDE Client:** ${ideClient}\n`;
}
const chat = config?.getGeminiClient()?.getChat();
const chat = agentContext?.geminiClient?.getChat();
const history = chat?.getHistory() || [];
let historyFileMessage = '';
let problemValue = bugDescription;
@@ -134,7 +134,7 @@ export const bugCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
if (!context.services.agentContext?.config.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -70,18 +70,19 @@ describe('chatCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getProjectRoot: () => '/project/root',
getGeminiClient: () =>
({
getChat: mockGetChat,
}) as unknown as GeminiClient,
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
agentContext: {
config: {
getProjectRoot: () => '/project/root',
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
},
},
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
geminiClient: {
getChat: mockGetChat,
} as unknown as GeminiClient,
},
logger: {
saveCheckpoint: mockSaveCheckpoint,
@@ -698,7 +699,11 @@ Hi there!`;
beforeEach(() => {
mockGetLatestApiRequest = vi.fn();
mockContext.services.config!.getLatestApiRequest =
if (!mockContext.services.agentContext!.config) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext!.config as any) = {};
}
mockContext.services.agentContext!.config.getLatestApiRequest =
mockGetLatestApiRequest;
vi.spyOn(process, 'cwd').mockReturnValue('/project/root');
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
+8 -6
View File
@@ -35,7 +35,7 @@ const getSavedChatTags = async (
context: CommandContext,
mtSortDesc: boolean,
): Promise<ChatDetail[]> => {
const cfg = context.services.config;
const cfg = context.services.agentContext?.config;
const geminiDir = cfg?.storage?.getProjectTempDir();
if (!geminiDir) {
return [];
@@ -103,7 +103,8 @@ const saveCommand: SlashCommand = {
};
}
const { logger, config } = context.services;
const { logger } = context.services;
const config = context.services.agentContext?.config;
await logger.initialize();
if (!context.overwriteConfirmed) {
@@ -125,7 +126,7 @@ const saveCommand: SlashCommand = {
}
}
const chat = config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
if (!chat) {
return {
type: 'message',
@@ -172,7 +173,8 @@ const resumeCheckpointCommand: SlashCommand = {
};
}
const { logger, config } = context.services;
const { logger } = context.services;
const config = context.services.agentContext?.config;
await logger.initialize();
const checkpoint = await logger.loadCheckpoint(tag);
const conversation = checkpoint.history;
@@ -298,7 +300,7 @@ const shareCommand: SlashCommand = {
};
}
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
if (!chat) {
return {
type: 'message',
@@ -344,7 +346,7 @@ export const debugCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context): Promise<MessageActionReturn> => {
const req = context.services.config?.getLatestApiRequest();
const req = context.services.agentContext?.config.getLatestApiRequest();
if (!req) {
return {
type: 'message',
@@ -36,24 +36,25 @@ describe('clearCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
}) as unknown as GeminiClient,
setSessionId: vi.fn(),
getEnableHooks: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
injectionService: {
clear: mockHintClear,
agentContext: {
config: {
getEnableHooks: vi.fn().mockReturnValue(false),
setSessionId: vi.fn(),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
injectionService: {
clear: mockHintClear,
},
},
geminiClient: {
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
} as unknown as GeminiClient,
},
},
});
@@ -98,7 +99,7 @@ describe('clearCommand', () => {
const nullConfigContext = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
+2 -2
View File
@@ -20,8 +20,8 @@ export const clearCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args) => {
const geminiClient = context.services.config?.getGeminiClient();
const config = context.services.config;
const geminiClient = context.services.agentContext?.geminiClient;
const config = context.services.agentContext?.config;
// Fire SessionEnd hook before clearing
const hookSystem = config?.getHookSystem();
@@ -22,11 +22,10 @@ describe('compressCommand', () => {
mockTryCompressChat = vi.fn();
context = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
agentContext: {
geminiClient: {
tryCompressChat: mockTryCompressChat,
} as unknown as GeminiClient,
},
},
});
@@ -39,9 +39,11 @@ export const compressCommand: SlashCommand = {
try {
ui.setPendingItem(pendingMessage);
const promptId = `compress-${Date.now()}`;
const compressed = await context.services.config
?.getGeminiClient()
?.tryCompressChat(promptId, true);
const compressed =
await context.services.agentContext?.geminiClient?.tryCompressChat(
promptId,
true,
);
if (compressed) {
ui.addItem(
{
@@ -29,10 +29,10 @@ describe('copyCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () => ({
agentContext: {
geminiClient: {
getChat: mockGetChat,
}),
},
},
},
});
@@ -301,7 +301,7 @@ describe('copyCommand', () => {
if (!copyCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const result = await copyCommand.action(nullConfigContext, '');
+1 -1
View File
@@ -18,7 +18,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.agentContext?.geminiClient?.getChat();
const history = chat?.getHistory();
// Get the last message from the AI (model role)
@@ -85,11 +85,14 @@ describe('directoryCommand', () => {
getFileFilteringOptions: () => ({ ignore: [], include: [] }),
setUserMemory: vi.fn(),
setGeminiMdFileCount: vi.fn(),
get config() {
return this;
},
} as unknown as Config;
mockContext = {
services: {
config: mockConfig,
agentContext: mockConfig,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -60,7 +60,7 @@ async function finishAddingDirectories(
}
if (added.length > 0) {
const gemini = config.getGeminiClient();
const gemini = config.geminiClient;
if (gemini) {
await gemini.addDirectoryContext();
@@ -110,9 +110,9 @@ export const directoryCommand: SlashCommand = {
// Filter out existing directories
let filteredSuggestions = suggestions;
if (context.services.config) {
if (context.services.agentContext?.config) {
const workspaceContext =
context.services.config.getWorkspaceContext();
context.services.agentContext.config.getWorkspaceContext();
const existingDirs = new Set(
workspaceContext.getDirectories().map((dir) => path.resolve(dir)),
);
@@ -144,11 +144,11 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext, args: string) => {
const {
ui: { addItem },
services: { config, settings },
services: { agentContext, settings },
} = context;
const [...rest] = args.split(' ');
if (!config) {
if (!agentContext) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
@@ -156,7 +156,7 @@ export const directoryCommand: SlashCommand = {
return;
}
if (config.isRestrictiveSandbox()) {
if (agentContext.config.isRestrictiveSandbox()) {
return {
type: 'message' as const,
messageType: 'error' as const,
@@ -181,7 +181,7 @@ export const directoryCommand: SlashCommand = {
const errors: string[] = [];
const alreadyAdded: string[] = [];
const workspaceContext = config.getWorkspaceContext();
const workspaceContext = agentContext.config.getWorkspaceContext();
const currentWorkspaceDirs = workspaceContext.getDirectories();
const pathsToProcess: string[] = [];
@@ -252,7 +252,7 @@ export const directoryCommand: SlashCommand = {
trustedDirs={added}
errors={errors}
finishAddingDirectories={finishAddingDirectories}
config={config}
config={agentContext.config}
addItem={addItem}
/>
),
@@ -264,7 +264,12 @@ export const directoryCommand: SlashCommand = {
errors.push(...result.errors);
}
await finishAddingDirectories(config, addItem, added, errors);
await finishAddingDirectories(
agentContext.config,
addItem,
added,
errors,
);
return;
},
},
@@ -275,16 +280,16 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext) => {
const {
ui: { addItem },
services: { config },
services: { agentContext },
} = context;
if (!config) {
if (!agentContext) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
const workspaceContext = config.getWorkspaceContext();
const workspaceContext = agentContext.config.getWorkspaceContext();
const directories = workspaceContext.getDirectories();
const directoryList = directories.map((dir) => `- ${dir}`).join('\n');
addItem({
@@ -161,14 +161,16 @@ describe('extensionsCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
agentContext: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
},
},
},
ui: {
@@ -917,7 +919,7 @@ describe('extensionsCommand', () => {
expect(restartAction).not.toBeNull();
mockRestartExtension = vi.fn();
mockContext.services.config!.getExtensionLoader = vi
mockContext.services.agentContext!.config.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: mockGetExtensions,
@@ -927,7 +929,7 @@ describe('extensionsCommand', () => {
});
it('should show a message if no extensions are installed', async () => {
mockContext.services.config!.getExtensionLoader = vi
mockContext.services.agentContext!.config.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: () => [],
@@ -1017,7 +1019,7 @@ describe('extensionsCommand', () => {
});
it('shows an error if no extension loader is available', async () => {
mockContext.services.config!.getExtensionLoader = vi.fn();
mockContext.services.agentContext!.config.getExtensionLoader = vi.fn();
await restartAction!(mockContext, '--all');
@@ -54,8 +54,8 @@ function showMessageIfNoExtensions(
}
async function listAction(context: CommandContext) {
const extensions = context.services.config
? listExtensions(context.services.config)
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -88,8 +88,8 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
(resolve) => (resolveUpdateComplete = resolve),
);
const extensions = context.services.config
? listExtensions(context.services.config)
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -128,7 +128,7 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
},
});
if (names?.length) {
const extensions = listExtensions(context.services.config!);
const extensions = listExtensions(context.services.agentContext!.config);
for (const name of names) {
const extension = extensions.find(
(extension) => extension.name === name,
@@ -156,7 +156,8 @@ async function restartAction(
context: CommandContext,
args: string,
): Promise<void> {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!extensionLoader) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -235,8 +236,8 @@ async function restartAction(
if (failures.length < extensionsToRestart.length) {
try {
await context.services.config?.reloadSkills();
await context.services.config?.getAgentRegistry()?.reload();
await context.services.agentContext?.config.reloadSkills();
await context.services.agentContext?.config.getAgentRegistry()?.reload();
} catch (error) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -274,7 +275,8 @@ async function exploreAction(
const useRegistryUI = settings.experimental?.extensionRegistry;
if (useRegistryUI) {
const extensionManager = context.services.config?.getExtensionLoader();
const extensionManager =
context.services.agentContext?.config.getExtensionLoader();
if (extensionManager instanceof ExtensionManager) {
return {
type: 'custom_dialog' as const,
@@ -331,7 +333,8 @@ function getEnableDisableContext(
names: string[];
scope: SettingScope;
} | null {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -431,7 +434,8 @@ async function enableAction(context: CommandContext, args: string) {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.services.config?.getMcpClientManager();
const mcpClientManager =
context.services.agentContext?.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers ?? {}),
);
@@ -463,7 +467,8 @@ async function installAction(
args: string,
requestConsentOverride?: (consent: string) => Promise<boolean>,
) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -529,7 +534,8 @@ async function installAction(
}
async function linkAction(context: CommandContext, args: string) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -593,7 +599,8 @@ async function linkAction(context: CommandContext, args: string) {
}
async function uninstallAction(context: CommandContext, args: string) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -692,7 +699,8 @@ async function configAction(context: CommandContext, args: string) {
}
}
const extensionManager = context.services.config?.getExtensionLoader();
const extensionManager =
context.services.agentContext?.config.getExtensionLoader();
if (!(extensionManager instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -729,7 +737,7 @@ export function completeExtensions(
context: CommandContext,
partialArg: string,
) {
let extensions = context.services.config?.getExtensions() ?? [];
let extensions = context.services.agentContext?.config.getExtensions() ?? [];
if (context.invocation?.name === 'enable') {
extensions = extensions.filter((ext) => !ext.isActive);
@@ -93,7 +93,7 @@ describe('hooksCommand', () => {
// Create mock context with config and settings
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: { config: mockConfig },
settings: mockSettings,
},
});
@@ -141,7 +141,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -225,7 +225,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -338,7 +338,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -470,7 +470,7 @@ describe('hooksCommand', () => {
it('should return empty array when config is not available', () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -567,7 +567,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -691,7 +691,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
+14 -7
View File
@@ -27,7 +27,8 @@ import { HooksDialog } from '../components/HooksDialog.js';
function panelAction(
context: CommandContext,
): MessageActionReturn | OpenCustomDialogActionReturn {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -55,7 +56,8 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -108,7 +110,8 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -163,7 +166,8 @@ function completeEnabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -183,7 +187,8 @@ function completeDisabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -209,7 +214,8 @@ function getHookDisplayName(hook: HookRegistryEntry): string {
async function enableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -280,7 +286,8 @@ async function enableAllAction(
async function disableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -60,10 +60,12 @@ describe('ideCommand', () => {
settings: {
setValue: vi.fn(),
},
config: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
agentContext: {
config: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
},
},
},
} as unknown as CommandContext;
+15 -5
View File
@@ -217,9 +217,13 @@ export const ideCommand = async (): Promise<SlashCommand> => {
);
// Poll for up to 5 seconds for the extension to activate.
for (let i = 0; i < 10; i++) {
await setIdeModeAndSyncConnection(context.services.config!, true, {
logToConsole: false,
});
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
{
logToConsole: false,
},
);
if (
ideClient.getConnectionStatus().status ===
IDEConnectionStatus.Connected
@@ -262,7 +266,10 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
true,
);
await setIdeModeAndSyncConnection(context.services.config!, true);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -285,7 +292,10 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
false,
);
await setIdeModeAndSyncConnection(context.services.config!, false);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
false,
);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -31,8 +31,10 @@ describe('initCommand', () => {
// Create a fresh mock context for each test
mockContext = createMockCommandContext({
services: {
config: {
getTargetDir: () => targetDir,
agentContext: {
config: {
getTargetDir: () => targetDir,
},
},
},
});
@@ -94,7 +96,7 @@ describe('initCommand', () => {
// Arrange: Create a context without config
const noConfigContext = createMockCommandContext();
if (noConfigContext.services) {
noConfigContext.services.config = null;
noConfigContext.services.agentContext = null;
}
// Act: Run the command's action
+2 -2
View File
@@ -23,14 +23,14 @@ export const initCommand: SlashCommand = {
context: CommandContext,
_args: string,
): Promise<SlashCommandActionReturn> => {
if (!context.services.config) {
if (!context.services.agentContext?.config) {
return {
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
};
}
const targetDir = context.services.config.getTargetDir();
const targetDir = context.services.agentContext.config.getTargetDir();
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
const result = performInit(fs.existsSync(geminiMdPath));
@@ -119,7 +119,10 @@ describe('mcpCommand', () => {
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: {
config: mockConfig,
toolRegistry: mockConfig.getToolRegistry(),
},
},
});
});
@@ -132,7 +135,7 @@ describe('mcpCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null,
},
});
@@ -146,7 +149,8 @@ describe('mcpCommand', () => {
});
it('should show an error if tool registry is not available', async () => {
mockConfig.getToolRegistry = vi.fn().mockReturnValue(undefined);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext as any).toolRegistry = undefined;
const result = await mcpCommand.action!(mockContext, '');
@@ -196,9 +200,13 @@ describe('mcpCommand', () => {
...mockServer3Tools,
];
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
const mockToolRegistry = {
getAllTools: vi.fn().mockReturnValue(allTools),
});
};
mockConfig.getToolRegistry = vi.fn().mockReturnValue(mockToolRegistry);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext as any).toolRegistry =
mockToolRegistry;
const resourcesByServer: Record<
string,
+17 -12
View File
@@ -42,8 +42,8 @@ const authCommand: SlashCommand = {
args: string,
): Promise<MessageActionReturn> => {
const serverName = args.trim();
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -138,7 +138,7 @@ const authCommand: SlashCommand = {
await mcpClientManager.restartServer(serverName);
}
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
const geminiClient = context.services.agentContext?.geminiClient;
if (geminiClient?.isInitialized()) {
await geminiClient.setTools();
}
@@ -162,7 +162,8 @@ const authCommand: SlashCommand = {
}
},
completion: async (context: CommandContext, partialArg: string) => {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const mcpServers = config.getMcpClientManager()?.getMcpServers() || {};
@@ -177,7 +178,8 @@ const listAction = async (
showDescriptions = false,
showSchema = false,
): Promise<void | MessageActionReturn> => {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -188,7 +190,7 @@ const listAction = async (
config.setUserInteractedWithMcp();
const toolRegistry = config.getToolRegistry();
const toolRegistry = agentContext.toolRegistry;
if (!toolRegistry) {
return {
type: 'message',
@@ -334,7 +336,8 @@ const reloadCommand: SlashCommand = {
action: async (
context: CommandContext,
): Promise<void | SlashCommandActionReturn> => {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -360,7 +363,7 @@ const reloadCommand: SlashCommand = {
await mcpClientManager.restart();
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
const geminiClient = agentContext.geminiClient;
if (geminiClient?.isInitialized()) {
await geminiClient.setTools();
}
@@ -377,7 +380,8 @@ async function handleEnableDisable(
args: string,
enable: boolean,
): Promise<MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -465,8 +469,8 @@ async function handleEnableDisable(
);
await mcpClientManager.restart();
}
if (config.getGeminiClient()?.isInitialized())
await config.getGeminiClient().setTools();
if (agentContext.geminiClient?.isInitialized())
await agentContext.geminiClient.setTools();
context.ui.reloadCommands();
return { type: 'message', messageType: 'info', content: msg };
@@ -477,7 +481,8 @@ async function getEnablementCompletion(
partialArg: string,
showEnabled: boolean,
): Promise<string[]> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const servers = Object.keys(
config.getMcpClientManager()?.getMcpServers() || {},
@@ -102,10 +102,12 @@ describe('memoryCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
agentContext: {
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
},
},
},
});
@@ -250,7 +252,7 @@ describe('memoryCommand', () => {
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: { config: mockConfig },
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -268,7 +270,7 @@ describe('memoryCommand', () => {
if (!reloadCommand.action) throw new Error('Command has no action');
// Enable JIT in mock config
const config = mockContext.services.config;
const config = mockContext.services.agentContext?.config;
if (!config) throw new Error('Config is undefined');
vi.mocked(config.isJitContextEnabled).mockReturnValue(true);
@@ -370,7 +372,7 @@ describe('memoryCommand', () => {
if (!reloadCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
await expect(
@@ -413,8 +415,10 @@ describe('memoryCommand', () => {
});
mockContext = createMockCommandContext({
services: {
config: {
getGeminiMdFilePaths: mockGetGeminiMdfilePaths,
agentContext: {
config: {
getGeminiMdFilePaths: mockGetGeminiMdfilePaths,
},
},
},
});
@@ -29,7 +29,7 @@ export const memoryCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) return;
const result = showMemory(config);
@@ -81,7 +81,7 @@ export const memoryCommand: SlashCommand = {
);
try {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (config) {
const result = await refreshMemory(config);
@@ -111,7 +111,7 @@ export const memoryCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) return;
const result = listMemoryFiles(config);
@@ -37,8 +37,11 @@ describe('modelCommand', () => {
}
const mockRefreshUserQuota = vi.fn();
mockContext.services.config = {
mockContext.services.agentContext = {
refreshUserQuota: mockRefreshUserQuota,
get config() {
return this;
},
} as unknown as Config;
await modelCommand.action(mockContext, '');
@@ -66,8 +69,11 @@ describe('modelCommand', () => {
(c) => c.name === 'manage',
);
const mockRefreshUserQuota = vi.fn();
mockContext.services.config = {
mockContext.services.agentContext = {
refreshUserQuota: mockRefreshUserQuota,
get config() {
return this;
},
} as unknown as Config;
await manageCommand!.action!(mockContext, '');
@@ -84,7 +90,7 @@ describe('modelCommand', () => {
expect(setCommand).toBeDefined();
const mockSetModel = vi.fn();
mockContext.services.config = {
mockContext.services.agentContext = {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
@@ -98,6 +104,9 @@ describe('modelCommand', () => {
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
get config() {
return this;
},
} as unknown as Config;
await setCommand!.action!(mockContext, 'gemini-pro');
@@ -116,7 +125,7 @@ describe('modelCommand', () => {
(c) => c.name === 'set',
);
const mockSetModel = vi.fn();
mockContext.services.config = {
mockContext.services.agentContext = {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
@@ -130,6 +139,9 @@ describe('modelCommand', () => {
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
get config() {
return this;
},
} as unknown as Config;
await setCommand!.action!(mockContext, 'gemini-pro --persist');

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