Compare commits

..

2 Commits

Author SHA1 Message Date
jacob314 ed74c13aed Fix code colorizer ansi escape bug. 2026-03-05 12:37:00 -08:00
jacob314 3ece48ebc0 Fix so shell calls are formatted nicely as bash code blocks 2026-03-05 09:54:18 -08:00
156 changed files with 2258 additions and 5471 deletions
+1 -4
View File
@@ -118,8 +118,6 @@ documentation.
reflects existing code.
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
adding new sections to existing pages.
- **Headers**: If you change a header, you must check for links that lead to
that header and update them.
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
sentences to make them easier for users to understand.
@@ -135,8 +133,7 @@ and that all links are functional.
technical behavior.
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
3. **Link check:** Verify all new and existing links leading to or from modified
pages. If you changed a header, ensure that any links that lead to it are
updated.
pages.
4. **Format:** Once all changes are complete, ask to execute `npm run format`
to ensure consistent formatting across the project. If the user confirms,
execute the command.
-21
View File
@@ -264,27 +264,6 @@ jobs:
run: 'npm run build'
shell: 'pwsh'
- name: 'Ensure Chrome is available'
shell: 'pwsh'
run: |
$chromePaths = @(
"${env:ProgramFiles}\Google\Chrome\Application\chrome.exe",
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
)
$chromeExists = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $chromeExists) {
Write-Host 'Chrome not found, installing via Chocolatey...'
choco install googlechrome -y --no-progress --ignore-checksums
}
$installed = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($installed) {
Write-Host "Chrome found at: $installed"
& $installed --version
} else {
Write-Error 'Chrome installation failed'
exit 1
}
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
@@ -121,7 +121,6 @@ jobs:
'area/security',
'area/platform',
'area/extensions',
'area/documentation',
'area/unknown'
];
const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
@@ -256,14 +255,6 @@ jobs:
"Issues with a specific extension."
"Feature request for the extension ecosystem."
area/documentation
- Description: Issues related to user-facing documentation and other content on the documentation website.
- Example Issues:
"A typo in a README file."
"DOCS: A command is not working as described in the documentation."
"A request for a new documentation page."
"Instructions missing for skills feature"
area/unknown
- Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.
@@ -63,7 +63,7 @@ jobs:
echo '🔍 Finding issues missing area labels...'
NO_AREA_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
--search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 100 --json number,title,body)"
--search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/unknown' --limit 100 --json number,title,body)"
echo '🔍 Finding issues missing kind labels...'
NO_KIND_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
@@ -204,7 +204,6 @@ jobs:
Categorization Guidelines (Area):
area/agent: Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality
area/core: User Interface, OS Support, Core Functionality
area/documentation: End-user and contributor-facing documentation, website-related
area/enterprise: Telemetry, Policy, Quota / Licensing
area/extensions: Gemini CLI extensions capability
area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
+25 -25
View File
@@ -344,32 +344,32 @@ Manual deletion also removes all associated artifacts:
If you use a [custom plans directory](#custom-plan-directory-and-policies),
those files are not automatically deleted and must be managed manually.
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
[`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext
[`write_file`]: /docs/tools/file-system.md#3-write_file-writefile
[`glob`]: /docs/tools/file-system.md#4-glob-findfiles
[`google_web_search`]: /docs/tools/web-search.md
[`replace`]: /docs/tools/file-system.md#6-replace-edit
[MCP tools]: /docs/tools/mcp-server.md
[`save_memory`]: /docs/tools/memory.md
[`activate_skill`]: /docs/cli/skills.md
[`codebase_investigator`]: /docs/core/subagents.md#codebase-investigator
[`cli_help`]: /docs/core/subagents.md#cli-help-agent
[subagents]: /docs/core/subagents.md
[custom subagents]: /docs/core/subagents.md#creating-custom-subagents
[policy engine]: /docs/reference/policy-engine.md
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: /docs/tools/ask-user.md
[YOLO mode]: /docs/reference/configuration.md#command-line-arguments
[`list_directory`]: ../tools/file-system.md#1-list_directory-readfolder
[`read_file`]: ../tools/file-system.md#2-read_file-readfile
[`grep_search`]: ../tools/file-system.md#5-grep_search-searchtext
[`write_file`]: ../tools/file-system.md#3-write_file-writefile
[`glob`]: ../tools/file-system.md#4-glob-findfiles
[`google_web_search`]: ../tools/web-search.md
[`replace`]: ../tools/file-system.md#6-replace-edit
[MCP tools]: ../tools/mcp-server.md
[`save_memory`]: ../tools/memory.md
[`activate_skill`]: ./skills.md
[`codebase_investigator`]: ../core/subagents.md#codebase_investigator
[`cli_help`]: ../core/subagents.md#cli_help
[subagents]: ../core/subagents.md
[custom subagents]: ../core/subagents.md#creating-custom-subagents
[policy engine]: ../reference/policy-engine.md
[`enter_plan_mode`]: ../tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: ../tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: ../tools/ask-user.md
[YOLO mode]: ../reference/configuration.md#command-line-arguments
[`plan.toml`]:
https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml
[auto model]: /docs/reference/configuration.md#model
[model routing]: /docs/cli/telemetry.md#model-routing
[preferred external editor]: /docs/reference/configuration.md#general
[session retention]: /docs/cli/session-management.md#session-retention
[extensions]: /docs/extensions/
[auto model]: ../reference/configuration.md#model-settings
[model routing]: ./telemetry.md#model-routing
[preferred external editor]: ../reference/configuration.md#general
[session retention]: ./session-management.md#session-retention
[extensions]: ../extensions/index.md
[Conductor]: https://github.com/gemini-cli-extensions/conductor
[open an issue]: https://github.com/google-gemini/gemini-cli/issues
[Agent Skills]: /docs/cli/skills.md
[Agent Skills]: ./skills.md
+2 -26
View File
@@ -50,31 +50,7 @@ Cross-platform sandboxing with complete process isolation.
**Note**: Requires building the sandbox image locally or using a published image
from your organization's registry.
### 3. gVisor / runsc (Linux only)
Strongest isolation available: runs containers inside a user-space kernel via
[gVisor](https://github.com/google/gvisor). gVisor intercepts all container
system calls and handles them in a sandboxed kernel written in Go, providing a
strong security barrier between AI operations and the host OS.
**Prerequisites:**
- Linux (gVisor supports Linux only)
- Docker installed and running
- gVisor/runsc runtime configured
When you set `sandbox: "runsc"`, Gemini CLI runs
`docker run --runtime=runsc ...` to execute containers with gVisor isolation.
runsc is not auto-detected; you must specify it explicitly (e.g.
`GEMINI_SANDBOX=runsc` or `sandbox: "runsc"`).
To set up runsc:
1. Install the runsc binary.
2. Configure the Docker daemon to use the runsc runtime.
3. Verify the installation.
### 4. LXC/LXD (Linux only, experimental)
### 3. LXC/LXD (Linux only, experimental)
Full-system container sandboxing using LXC/LXD. Unlike Docker/Podman, LXC
containers run a complete Linux system with `systemd`, `snapd`, and other system
@@ -157,7 +133,7 @@ gemini -p "run the test suite"
1. **Command flag**: `-s` or `--sandbox`
2. **Environment variable**:
`GEMINI_SANDBOX=true|docker|podman|sandbox-exec|runsc|lxc`
`GEMINI_SANDBOX=true|docker|podman|sandbox-exec|lxc`
3. **Settings file**: `"sandbox": true` in the `tools` object of your
`settings.json` file (e.g., `{"tools": {"sandbox": true}}`).
-4
View File
@@ -4,10 +4,6 @@ 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.
> **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](/plans/).
For most users, we recommend starting Gemini CLI and logging in with your
personal Google account.
-4
View File
@@ -39,10 +39,6 @@ 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.
> **Note:** Looking to upgrade for higher limits? To compare subscription
> options and find the right quota for your needs, see our
> [Plans page](/plans/).
Similarly, when you reach your daily usage limit for Gemini 2.5 Pro, youll see
a message prompting fallback to Gemini 2.5 Flash.
+57 -57
View File
@@ -19,12 +19,12 @@ available combinations.
| Action | Keys |
| ------------------------------------------- | ------------------------------------------------------------ |
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
| Move the cursor up one line. | `Up Arrow` |
| Move the cursor down one line. | `Down Arrow` |
| Move the cursor one character to the left. | `Left Arrow` |
| Move the cursor one character to the right. | `Right Arrow`<br />`Ctrl + F` |
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home (no Shift, Ctrl)` |
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Shift, Ctrl)` |
| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)` |
| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + F` |
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
@@ -39,7 +39,7 @@ available combinations.
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete`<br />`Alt + D` |
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
| Undo the most recent text edit. | `Cmd + Z`<br />`Alt + Z` |
| Undo the most recent text edit. | `Cmd + Z (no Shift)`<br />`Alt + Z (no Shift)` |
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`Shift + Alt + Z` |
#### Scrolling
@@ -55,72 +55,72 @@ available combinations.
#### History & Search
| Action | Keys |
| -------------------------------------------- | ------------ |
| Show the previous entry in history. | `Ctrl + P` |
| Show the next entry in history. | `Ctrl + N` |
| Start reverse search through history. | `Ctrl + R` |
| Submit the selected reverse-search match. | `Enter` |
| Accept a suggestion while reverse searching. | `Tab` |
| Browse and rewind previous interactions. | `Double Esc` |
| Action | Keys |
| -------------------------------------------- | --------------------- |
| Show the previous entry in history. | `Ctrl + P (no Shift)` |
| Show the next entry in history. | `Ctrl + N (no Shift)` |
| Start reverse search through history. | `Ctrl + R` |
| Submit the selected reverse-search match. | `Enter (no Ctrl)` |
| Accept a suggestion while reverse searching. | `Tab (no Shift)` |
| Browse and rewind previous interactions. | `Double Esc` |
#### Navigation
| Action | Keys |
| -------------------------------------------------- | --------------------- |
| Move selection up in lists. | `Up Arrow` |
| Move selection down in lists. | `Down Arrow` |
| Move up within dialog options. | `Up Arrow`<br />`K` |
| Move down within dialog options. | `Down Arrow`<br />`J` |
| Move to the next item or question in a dialog. | `Tab` |
| Move to the previous item or question in a dialog. | `Shift + Tab` |
| Action | Keys |
| -------------------------------------------------- | ------------------------------------------- |
| Move selection up in lists. | `Up Arrow (no Shift)` |
| Move selection down in lists. | `Down Arrow (no Shift)` |
| Move up within dialog options. | `Up Arrow (no Shift)`<br />`K (no Shift)` |
| Move down within dialog options. | `Down Arrow (no Shift)`<br />`J (no Shift)` |
| Move to the next item or question in a dialog. | `Tab (no Shift)` |
| Move to the previous item or question in a dialog. | `Shift + Tab` |
#### Suggestions & Completions
| Action | Keys |
| --------------------------------------- | ---------------------------- |
| Accept the inline suggestion. | `Tab`<br />`Enter` |
| Move to the previous completion option. | `Up Arrow`<br />`Ctrl + P` |
| Move to the next completion option. | `Down Arrow`<br />`Ctrl + N` |
| Expand an inline suggestion. | `Right Arrow` |
| Collapse an inline suggestion. | `Left Arrow` |
| Action | Keys |
| --------------------------------------- | -------------------------------------------------- |
| Accept the inline suggestion. | `Tab (no Shift)`<br />`Enter (no Ctrl)` |
| Move to the previous completion option. | `Up Arrow (no Shift)`<br />`Ctrl + P (no Shift)` |
| Move to the next completion option. | `Down Arrow (no Shift)`<br />`Ctrl + N (no Shift)` |
| Expand an inline suggestion. | `Right Arrow` |
| Collapse an inline suggestion. | `Left Arrow` |
#### Text Input
| Action | Keys |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Submit the current prompt. | `Enter` |
| Submit the current prompt. | `Enter (no Shift, Alt, Ctrl, Cmd)` |
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
| Open the current prompt or the plan in an external editor. | `Ctrl + X` |
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
#### App Controls
| Action | Keys |
| -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Alt + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift + Tab` |
| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` |
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` |
| Toggle current background shell visibility. | `Ctrl + B` |
| Toggle background shell list. | `Ctrl + L` |
| Kill the active background shell. | `Ctrl + K` |
| Confirm selection in background shell list. | `Enter` |
| Dismiss background shell list. | `Esc` |
| Move focus from background shell to Gemini. | `Shift + Tab` |
| Move focus from background shell list to Gemini. | `Tab` |
| Show warning when trying to move focus away from background shell. | `Tab` |
| Show warning when trying to move focus away from shell input. | `Tab` |
| Move focus from Gemini to the active shell. | `Tab` |
| Move focus from the shell back to Gemini. | `Shift + Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R`<br />`Shift + R` |
| Suspend the CLI and move it to the background. | `Ctrl + Z` |
| Action | Keys |
| -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Alt + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift + Tab` |
| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` |
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` |
| Toggle current background shell visibility. | `Ctrl + B` |
| Toggle background shell list. | `Ctrl + L` |
| Kill the active background shell. | `Ctrl + K` |
| Confirm selection in background shell list. | `Enter` |
| Dismiss background shell list. | `Esc` |
| Move focus from background shell to Gemini. | `Shift + Tab` |
| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
| Show warning when trying to move focus away from background shell. | `Tab (no Shift)` |
| Show warning when trying to move focus away from shell input. | `Tab (no Shift)` |
| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
| Move focus from the shell back to Gemini. | `Shift + Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Suspend the CLI and move it to the background. | `Ctrl + Z` |
<!-- KEYBINDINGS-AUTOGEN:END -->
@@ -156,7 +156,7 @@ available combinations.
## Limitations
- On [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal):
- `shift+enter` is only supported in version 1.25 and higher.
- `shift+enter` is not supported.
- `shift+tab`
[is not supported](https://github.com/google-gemini/gemini-cli/issues/20314)
on Node 20 and earlier versions of Node 22.
+36 -43
View File
@@ -1,13 +1,14 @@
# Gemini CLI: Quotas and pricing
Gemini CLI offers a generous free tier that covers many individual developers'
use cases. For enterprise or professional usage, or if you need increased quota,
use cases. For enterprise or professional usage, or if you need higher limits,
several options are available depending on your authentication account type.
For a high-level comparison of available subscriptions and to select the right
quota for your needs, see the [Plans page](/plans/).
See [privacy and terms](./tos-privacy.md) for details on the Privacy Policy and
Terms of Service.
## Overview
> **Note:** Published prices are list price; additional negotiated commercial
> discounting may apply.
This article outlines the specific quotas and pricing applicable to Gemini CLI
when using different authentication methods.
@@ -22,11 +23,10 @@ Generally, there are three categories to choose from:
## Free usage
Access to Gemini CLI begins with a generous free tier, perfect for
experimentation and light use.
Your journey begins with a generous free tier, perfect for experimentation and
light use.
Your free usage is governed by the following limits, which depend on your
authorization type.
Your free usage limits depend on your authorization type.
### Log in with Google (Gemini Code Assist for individuals)
@@ -78,12 +78,14 @@ Gemini CLI by upgrading to one of the following subscriptions:
Learn more at
[Gemini Code Assist Quotas and Limits](https://developers.google.com/gemini-code-assist/resources/quotas)
- [Purchase a Gemini Code Assist Subscription through Google Cloud](https://cloud.google.com/gemini/docs/codeassist/overview).
- [Purchase a Gemini Code Assist Subscription through Google Cloud ](https://cloud.google.com/gemini/docs/codeassist/overview)
by signing up in the Google Cloud console. Learn more at
[Set up Gemini Code Assist](https://cloud.google.com/gemini/docs/discover/set-up-gemini).
Quotas and pricing are based on a fixed price subscription with assigned
license seats. For predictable costs, you can sign in with Google.
This includes the following request limits:
This includes:
- Gemini Code Assist Standard edition:
- 1500 model requests / user / day
- 120 model requests / user / minute
@@ -104,27 +106,18 @@ recommended path for uninterrupted access.
To do this, log in using a Gemini API key or Vertex AI.
### Vertex AI (regular mode)
An enterprise-grade platform for building, deploying, and managing AI models,
including Gemini. It offers enhanced security, data governance, and integration
with other Google Cloud services.
- Quota: Governed by a dynamic shared quota system or pre-purchased provisioned
throughput.
- Cost: Based on model and token usage.
- Vertex AI (Regular Mode):
- Quota: Governed by a dynamic shared quota system or pre-purchased
provisioned throughput.
- Cost: Based on model and token usage.
Learn more at
[Vertex AI Dynamic Shared Quota](https://cloud.google.com/vertex-ai/generative-ai/docs/resources/dynamic-shared-quota)
and [Vertex AI Pricing](https://cloud.google.com/vertex-ai/pricing).
### Gemini API key
Ideal for developers who want to quickly build applications with the Gemini
models. This is the most direct way to use the models.
- Quota: Varies by pricing tier.
- Cost: Varies by pricing tier and model/token usage.
- Gemini API key:
- Quota: Varies by pricing tier.
- Cost: Varies by pricing tier and model/token usage.
Learn more at
[Gemini API Rate Limits](https://ai.google.dev/gemini-api/docs/rate-limits),
@@ -132,8 +125,7 @@ Learn more at
Its important to highlight that when using an API key, you pay per token/call.
This can be more expensive for many small calls with few tokens, but it's the
only way to ensure your workflow isn't interrupted by reaching a limit on your
quota.
only way to ensure your workflow isn't interrupted by quota limits.
## Gemini for workspace plans
@@ -143,12 +135,12 @@ Flow video editor). These plans do not apply to the API usage which powers the
Gemini CLI. Supporting these plans is under active consideration for future
support.
## Check usage and limits
## Check usage and quota
You can check your current token usage and applicable limits using the
You can check your current token usage and quota information using the
`/stats model` command. This command provides a snapshot of your current
session's token usage, as well as information about the limits associated with
your current quota.
session's token usage, as well as your overall quota and usage for the supported
models.
For more information on the `/stats` command and its subcommands, see the
[Command Reference](../../reference/commands.md#stats).
@@ -157,16 +149,17 @@ A summary of model usage is also presented on exit at the end of a session.
## Tips to avoid high costs
When using a pay-as-you-go plan, be mindful of your usage to avoid unexpected
When using a Pay as you Go API key, be mindful of your usage to avoid unexpected
costs.
- **Be selective with suggestions**: Before accepting a suggestion, especially
for a computationally intensive task like refactoring a large codebase,
consider if it's the most cost-effective approach.
- **Use precise prompts**: You are paying per call, so think about the most
efficient way to get your desired result. A well-crafted prompt can often get
you the answer you need in a single call, rather than multiple back-and-forth
interactions.
- **Monitor your usage**: Use the `/stats model` command to track your token
usage during a session. This can help you stay aware of your spending in real
time.
- Don't blindly accept every suggestion, especially for computationally
intensive tasks like refactoring large codebases.
- Be intentional with your prompts and commands. You are paying per call, so
think about the most efficient way to get the job done.
## Gemini API vs. Vertex
- Gemini API (gemini developer api): This is the fastest way to use the Gemini
models directly.
- Vertex AI: This is the enterprise-grade platform for building, deploying, and
managing Gemini models with specific security and control requirements.
-49
View File
@@ -1,49 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { appEvalTest } from './app-test-helper.js';
describe('Continuous Session Behavioral Evals', () => {
appEvalTest('ALWAYS_PASSES', {
name: 'Continuous Session: Model preserves technical state across manual compression',
configOverrides: {
excludeTools: ['run_shell_command', 'write_file'],
modelSteering: true,
},
files: {
'src/offender.ts': `// Line 1
// Line 2
const x = process.env.SECRET_KEY; // Line 3
// Line 4`,
},
prompt:
'Audit src/ for process.env usage. When you find one, checkpoint your state with the line number and then manually compress the context to clear your history before giving me the final report.',
setup: async (rig) => {
// Pause on our new tools to observe the workflow
rig.setBreakpoint(['checkpoint_state', 'compress']);
},
assert: async (rig) => {
// 1. Wait for Checkpoint
await rig.waitForPendingConfirmation('checkpoint_state', 45000);
await rig.resolveAwaitedTool();
// 2. Wait for Compression
await rig.waitForPendingConfirmation('compress', 45000);
await rig.resolveAwaitedTool();
// 3. Final Verification
// The model should report the finding even though the initial read_file
// is gone from history due to compression.
await rig.waitForOutput(/process\.env\.SECRET_KEY/i, 60000);
//await rig.waitForOutput(/Line 3/i, 60000);
await rig.waitForIdle(30000);
const output = rig.getStaticOutput();
expect(output).toContain('SECRET_KEY');
},
});
});
-44
View File
@@ -1,44 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { appEvalTest } from './app-test-helper.js';
describe('Head Entropy Behavioral Evals', () => {
appEvalTest('ALWAYS_PASSES', {
name: 'Head Entropy: Model distills a high-noise tool output',
configOverrides: {
modelSteering: true,
continuousSession: true,
},
files: {
'src/big-file.ts': '// NOISE\n'.repeat(100) + 'const SECRET = "SIGNAL";\n' + '// NOISE\n'.repeat(100),
},
prompt:
'Grep for SECRET in src/. If the result is very noisy, use distill_result to replace it with just the signal you found before continuing.',
setup: async (rig) => {
// Pause on our new tools to observe the workflow
rig.setBreakpoint(['grep_search', 'distill_result']);
},
assert: async (rig) => {
// 1. Wait for Grep
await rig.waitForPendingConfirmation('grep_search', 45000);
await rig.resolveAwaitedTool();
// 2. Wait for Distillation
await rig.waitForPendingConfirmation('distill_result', 45000);
await rig.resolveAwaitedTool();
// 3. Final Verification
await rig.waitForOutput(/SIGNAL/i, 60000);
await rig.waitForIdle(30000);
const output = rig.getStaticOutput();
expect(output).toContain('SIGNAL');
expect(output).not.toContain('NOISE'); // Should be elided/replaced
},
});
});
+2 -2
View File
@@ -55,7 +55,7 @@ describe.skip('ACP Environment and Auth', () => {
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn('node', [bundlePath, '--acp'], {
child = spawn('node', [bundlePath, '--experimental-acp'], {
cwd: rig.homeDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
@@ -120,7 +120,7 @@ describe.skip('ACP Environment and Auth', () => {
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
child = spawn('node', [bundlePath, '--acp'], {
child = spawn('node', [bundlePath, '--experimental-acp'], {
cwd: rig.homeDir!,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
+1 -1
View File
@@ -58,7 +58,7 @@ describe('ACP telemetry', () => {
'node',
[
bundlePath,
'--acp',
'--experimental-acp',
'--fake-responses',
join(rig.testDir!, 'fake-responses.json'),
],
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Part 1. "}],"role":"model"},"index":0}]},{"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":10,"totalTokenCount":110}},{"candidates":[{"content":{"parts":[{"text":"Part 2."}],"role":"model"},"index":0}],"finishReason":"STOP"}]}
-50
View File
@@ -1,50 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
describe('API Resilience E2E', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should not crash when receiving metadata-only chunks in a stream', async () => {
await rig.setup('api-resilience-metadata-only', {
fakeResponsesPath: join(
dirname(fileURLToPath(import.meta.url)),
'api-resilience.responses',
),
settings: {
planSettings: { modelRouting: false },
},
});
// Run the CLI with a simple prompt.
// The fake responses will provide a stream with a metadata-only chunk in the middle.
// We use gemini-3-pro-preview to minimize internal service calls.
const result = await rig.run({
args: ['hi', '--model', 'gemini-3-pro-preview'],
});
// Verify the output contains text from the normal chunks.
// If the CLI crashed on the metadata chunk, rig.run would throw.
expect(result).toContain('Part 1.');
expect(result).toContain('Part 2.');
// Verify telemetry event for the prompt was still generated
const hasUserPromptEvent = await rig.waitForTelemetryEvent('user_prompt');
expect(hasUserPromptEvent).toBe(true);
});
});
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll open https://example.com and check the page title for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and get the page title"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The page title of https://example.com is \"Example Domain\". The browser session has been completed and cleaned up successfully."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and analyze the links on the page."},{"functionCall":{"name":"browser_agent","args":{"task":"Go to https://example.com and find all links on the page, then describe them"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"After analyzing https://example.com, I found the following links:\n\n1. **\"More information...\"** - This is the main link on the page that points to the IANA (Internet Assigned Numbers Authority) website for more details about reserved domains.\n\nThe page is quite minimal with just this single informational link, which is typical for example domains used in documentation."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":70,"totalTokenCount":270}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you open https://example.com and analyze the page. Let me use the browser agent to navigate and capture the page information."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and capture the accessibility tree to get the page title and main content"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Based on the browser analysis of https://example.com:\n\n**Page Title**: Example Domain\n\n**Main Content**: The page contains a simple heading \"Example Domain\" and explanatory text stating \"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\"\n\nThe page has a clean, minimal layout typical of placeholder domains used for documentation and examples."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and take a screenshot for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully navigated to https://example.com and captured a screenshot. The page shows the familiar \"Example Domain\" header with explanatory text below it. The screenshot captures the clean, minimal layout of this demonstration website."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you navigate to https://example.com and perform both operations - capturing the accessibility tree and taking a screenshot."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":45,"totalTokenCount":145}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully completed both operations on https://example.com:\n\n**Accessibility Tree Snapshot**: The page has a clear structure with the main heading \"Example Domain\" and descriptive text about the domain's purpose for documentation examples.\n\n**Screenshot**: Captured a visual representation of the page showing the clean, minimal layout with the heading and explanatory text.\n\nBoth the accessibility data and visual screenshot confirm this is the standard example domain page used for documentation purposes."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]}
-206
View File
@@ -1,206 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Integration tests for the browser agent.
*
* These tests verify the complete end-to-end flow from CLI prompt through
* browser_agent delegation to MCP/Chrome DevTools and back. Unlike the unit
* tests in packages/core/src/agents/browser/ which mock all MCP components,
* these tests launch real Chrome instances in headless mode.
*
* Tests are skipped on systems without Chrome/Chromium installed.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, assertModelHasOutput } from './test-helper.js';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
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') {
// Check standard Windows installation paths using Node.js fs
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) {
// Fall back to PATH check
execSync('where chrome || where chromium', { stdio: 'ignore' });
}
} else {
return false;
}
return true;
} catch {
return false;
}
})();
describe.skipIf(!chromeAvailable)('browser-agent', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('should navigate to a page and capture accessibility tree', async () => {
rig.setup('browser-navigate-and-snapshot', {
fakeResponsesPath: join(
__dirname,
'browser-agent.navigate-snapshot.responses',
),
settings: {
agents: {
browser_agent: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
const result = await rig.run({
args: 'Open https://example.com in the browser and tell me the page title and main content.',
});
assertModelHasOutput(result);
const toolLogs = rig.readToolLogs();
const browserAgentCall = toolLogs.find(
(t) => t.toolRequest.name === 'browser_agent',
);
expect(
browserAgentCall,
'Expected browser_agent to be called',
).toBeDefined();
});
it('should take screenshots of web pages', async () => {
rig.setup('browser-screenshot', {
fakeResponsesPath: join(__dirname, 'browser-agent.screenshot.responses'),
settings: {
agents: {
browser_agent: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
const result = await rig.run({
args: 'Navigate to https://example.com and take a screenshot.',
});
const toolLogs = rig.readToolLogs();
const browserCalls = toolLogs.filter(
(t) => t.toolRequest.name === 'browser_agent',
);
expect(browserCalls.length).toBeGreaterThan(0);
assertModelHasOutput(result);
});
it('should interact with page elements', async () => {
rig.setup('browser-interaction', {
fakeResponsesPath: join(__dirname, 'browser-agent.interaction.responses'),
settings: {
agents: {
browser_agent: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
const result = await rig.run({
args: 'Go to https://example.com, find any links on the page, and describe them.',
});
const toolLogs = rig.readToolLogs();
const browserAgentCall = toolLogs.find(
(t) => t.toolRequest.name === 'browser_agent',
);
expect(
browserAgentCall,
'Expected browser_agent to be called',
).toBeDefined();
assertModelHasOutput(result);
});
it('should clean up browser processes after completion', async () => {
rig.setup('browser-cleanup', {
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
settings: {
agents: {
browser_agent: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
await rig.run({
args: 'Open https://example.com in the browser and check the page title.',
});
// Test passes if we reach here, relying on Vitest's timeout mechanism
// to detect hanging browser processes.
});
it('should handle multiple browser operations in sequence', async () => {
rig.setup('browser-sequential', {
fakeResponsesPath: join(__dirname, 'browser-agent.sequential.responses'),
settings: {
agents: {
browser_agent: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
const result = await rig.run({
args: 'Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot.',
});
const toolLogs = rig.readToolLogs();
const browserCalls = toolLogs.filter(
(t) => t.toolRequest.name === 'browser_agent',
);
expect(browserCalls.length).toBeGreaterThan(0);
// Should successfully complete all operations
assertModelHasOutput(result);
});
});
+3 -12
View File
@@ -76,8 +76,7 @@ export interface CliArgs {
policy: string[] | undefined;
allowedMcpServerNames: string[] | undefined;
allowedTools: string[] | undefined;
acp?: boolean;
experimentalAcp?: boolean;
experimentalAcp: boolean | undefined;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
resume: string | typeof RESUME_LATEST | undefined;
@@ -173,14 +172,9 @@ export async function parseArguments(
.filter(Boolean),
),
})
.option('acp', {
type: 'boolean',
description: 'Starts the agent in ACP mode',
})
.option('experimental-acp', {
type: 'boolean',
description:
'Starts the agent in ACP mode (deprecated, use --acp instead)',
description: 'Starts the agent in ACP mode',
})
.option('allowed-mcp-server-names', {
type: 'array',
@@ -603,7 +597,6 @@ export async function loadCliConfig(
// -i/--prompt-interactive forces interactive mode with an initial prompt
const interactive =
!!argv.promptInteractive ||
!!argv.acp ||
!!argv.experimentalAcp ||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.isCommand);
@@ -695,7 +688,6 @@ export async function loadCliConfig(
}
return new Config({
acpMode: !!argv.acp || !!argv.experimentalAcp,
sessionId,
clientVersion: await getVersion(),
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -759,7 +751,7 @@ export async function loadCliConfig(
bugCommand: settings.advanced?.bugCommand,
model: resolvedModel,
maxSessionTurns: settings.model?.maxSessionTurns,
experimentalZedIntegration: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
listSessions: argv.listSessions || false,
deleteSession: argv.deleteSession,
@@ -777,7 +769,6 @@ export async function loadCliConfig(
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
continuousSession: settings.experimental?.continuousSession,
modelSteering: settings.experimental?.modelSteering,
toolOutputMasking: settings.experimental?.toolOutputMasking,
noBrowser: !!process.env['NO_BROWSER'],
@@ -58,6 +58,46 @@ describe('keyBindings config', () => {
const config: KeyBindingConfig = defaultKeyBindings;
expect(config[Command.HOME]).toBeDefined();
});
it('should have correct specific bindings', () => {
// Verify navigation ignores shift
const navUp = defaultKeyBindings[Command.NAVIGATION_UP];
expect(navUp).toContainEqual({ key: 'up', shift: false });
const navDown = defaultKeyBindings[Command.NAVIGATION_DOWN];
expect(navDown).toContainEqual({ key: 'down', shift: false });
// Verify dialog navigation
const dialogNavUp = defaultKeyBindings[Command.DIALOG_NAVIGATION_UP];
expect(dialogNavUp).toContainEqual({ key: 'up', shift: false });
expect(dialogNavUp).toContainEqual({ key: 'k', shift: false });
const dialogNavDown = defaultKeyBindings[Command.DIALOG_NAVIGATION_DOWN];
expect(dialogNavDown).toContainEqual({ key: 'down', shift: false });
expect(dialogNavDown).toContainEqual({ key: 'j', shift: false });
// Verify physical home/end keys for cursor movement
expect(defaultKeyBindings[Command.HOME]).toContainEqual({
key: 'home',
ctrl: false,
shift: false,
});
expect(defaultKeyBindings[Command.END]).toContainEqual({
key: 'end',
ctrl: false,
shift: false,
});
// Verify physical home/end keys for scrolling
expect(defaultKeyBindings[Command.SCROLL_HOME]).toContainEqual({
key: 'home',
ctrl: true,
});
expect(defaultKeyBindings[Command.SCROLL_END]).toContainEqual({
key: 'end',
ctrl: true,
});
});
});
describe('command metadata', () => {
+67 -27
View File
@@ -134,12 +134,27 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.EXIT]: [{ key: 'd', ctrl: true }],
// Cursor Movement
[Command.HOME]: [{ key: 'a', ctrl: true }, { key: 'home' }],
[Command.END]: [{ key: 'e', ctrl: true }, { key: 'end' }],
[Command.MOVE_UP]: [{ key: 'up' }],
[Command.MOVE_DOWN]: [{ key: 'down' }],
[Command.MOVE_LEFT]: [{ key: 'left' }],
[Command.MOVE_RIGHT]: [{ key: 'right' }, { key: 'f', ctrl: true }],
[Command.HOME]: [
{ key: 'a', ctrl: true },
{ key: 'home', shift: false, ctrl: false },
],
[Command.END]: [
{ key: 'e', ctrl: true },
{ key: 'end', shift: false, ctrl: false },
],
[Command.MOVE_UP]: [
{ key: 'up', shift: false, alt: false, ctrl: false, cmd: false },
],
[Command.MOVE_DOWN]: [
{ key: 'down', shift: false, alt: false, ctrl: false, cmd: false },
],
[Command.MOVE_LEFT]: [
{ key: 'left', shift: false, alt: false, ctrl: false, cmd: false },
],
[Command.MOVE_RIGHT]: [
{ key: 'right', shift: false, alt: false, ctrl: false, cmd: false },
{ key: 'f', ctrl: true },
],
[Command.MOVE_WORD_LEFT]: [
{ key: 'left', ctrl: true },
{ key: 'left', alt: true },
@@ -168,8 +183,8 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
[Command.UNDO]: [
{ key: 'z', cmd: true },
{ key: 'z', alt: true },
{ key: 'z', cmd: true, shift: false },
{ key: 'z', alt: true, shift: false },
],
[Command.REDO]: [
{ key: 'z', ctrl: true, shift: true },
@@ -192,33 +207,56 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.PAGE_DOWN]: [{ key: 'pagedown' }],
// History & Search
[Command.HISTORY_UP]: [{ key: 'p', ctrl: true }],
[Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true }],
[Command.HISTORY_UP]: [{ key: 'p', shift: false, ctrl: true }],
[Command.HISTORY_DOWN]: [{ key: 'n', shift: false, ctrl: true }],
[Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
[Command.REWIND]: [{ key: 'double escape' }], // for documentation only
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return' }],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
[Command.REWIND]: [{ key: 'double escape' }],
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab', shift: false }],
// Navigation
[Command.NAVIGATION_UP]: [{ key: 'up' }],
[Command.NAVIGATION_DOWN]: [{ key: 'down' }],
[Command.NAVIGATION_UP]: [{ key: 'up', shift: false }],
[Command.NAVIGATION_DOWN]: [{ key: 'down', shift: false }],
// Navigation shortcuts appropriate for dialogs where we do not need to accept
// text input.
[Command.DIALOG_NAVIGATION_UP]: [{ key: 'up' }, { key: 'k' }],
[Command.DIALOG_NAVIGATION_DOWN]: [{ key: 'down' }, { key: 'j' }],
[Command.DIALOG_NEXT]: [{ key: 'tab' }],
[Command.DIALOG_NAVIGATION_UP]: [
{ key: 'up', shift: false },
{ key: 'k', shift: false },
],
[Command.DIALOG_NAVIGATION_DOWN]: [
{ key: 'down', shift: false },
{ key: 'j', shift: false },
],
[Command.DIALOG_NEXT]: [{ key: 'tab', shift: false }],
[Command.DIALOG_PREV]: [{ key: 'tab', shift: true }],
// Suggestions & Completions
[Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return' }],
[Command.COMPLETION_UP]: [{ key: 'up' }, { key: 'p', ctrl: true }],
[Command.COMPLETION_DOWN]: [{ key: 'down' }, { key: 'n', ctrl: true }],
[Command.ACCEPT_SUGGESTION]: [
{ key: 'tab', shift: false },
{ key: 'return', ctrl: false },
],
[Command.COMPLETION_UP]: [
{ key: 'up', shift: false },
{ key: 'p', shift: false, ctrl: true },
],
[Command.COMPLETION_DOWN]: [
{ key: 'down', shift: false },
{ key: 'n', shift: false, ctrl: true },
],
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],
// Text Input
// Must also exclude shift to allow shift+enter for newline
[Command.SUBMIT]: [{ key: 'return' }],
[Command.SUBMIT]: [
{
key: 'return',
shift: false,
alt: false,
ctrl: false,
cmd: false,
},
],
[Command.NEWLINE]: [
{ key: 'return', ctrl: true },
{ key: 'return', cmd: true },
@@ -245,17 +283,19 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: [{ key: 'l', ctrl: true }],
[Command.KILL_BACKGROUND_SHELL]: [{ key: 'k', ctrl: true }],
[Command.UNFOCUS_BACKGROUND_SHELL]: [{ key: 'tab', shift: true }],
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [{ key: 'tab' }],
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [{ key: 'tab' }],
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [{ key: 'tab' }],
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [{ key: 'tab', shift: false }],
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [
{ key: 'tab', shift: false },
],
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [{ key: 'tab', shift: false }],
[Command.BACKGROUND_SHELL_SELECT]: [{ key: 'return' }],
[Command.BACKGROUND_SHELL_ESCAPE]: [{ key: 'escape' }],
[Command.SHOW_MORE_LINES]: [{ key: 'o', ctrl: true }],
[Command.EXPAND_PASTE]: [{ key: 'o', ctrl: true }],
[Command.FOCUS_SHELL_INPUT]: [{ key: 'tab' }],
[Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }],
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab', shift: true }],
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
[Command.RESTART_APP]: [{ key: 'r' }, { key: 'r', shift: true }],
[Command.RESTART_APP]: [{ key: 'r' }],
[Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }],
};
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
ApprovalMode,
PolicyDecision,
@@ -29,10 +29,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
});
describe('Policy Engine Integration Tests', () => {
beforeEach(() => vi.stubEnv('GEMINI_SYSTEM_MD', ''));
afterEach(() => vi.unstubAllEnvs());
describe('Policy configuration produces valid PolicyEngine config', () => {
it('should create a working PolicyEngine from basic settings', async () => {
const settings: Settings = {
+2 -90
View File
@@ -97,7 +97,7 @@ describe('loadSandboxConfig', () => {
it('should throw if GEMINI_SANDBOX is an invalid command', async () => {
process.env['GEMINI_SANDBOX'] = 'invalid-command';
await expect(loadSandboxConfig({}, {})).rejects.toThrow(
"Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec, runsc, lxc",
"Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec, lxc",
);
});
@@ -194,7 +194,7 @@ describe('loadSandboxConfig', () => {
await expect(
loadSandboxConfig({}, { sandbox: 'invalid-command' }),
).rejects.toThrow(
"Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec, runsc, lxc",
"Invalid sandbox command 'invalid-command'. Must be one of docker, podman, sandbox-exec",
);
});
});
@@ -247,92 +247,4 @@ describe('loadSandboxConfig', () => {
},
);
});
describe('with sandbox: runsc (gVisor)', () => {
beforeEach(() => {
mockedOsPlatform.mockReturnValue('linux');
mockedCommandExistsSync.mockReturnValue(true);
});
it('should use runsc via CLI argument on Linux', async () => {
const config = await loadSandboxConfig({}, { sandbox: 'runsc' });
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc');
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
it('should use runsc via GEMINI_SANDBOX environment variable', async () => {
process.env['GEMINI_SANDBOX'] = 'runsc';
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc');
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
it('should use runsc via settings file', async () => {
const config = await loadSandboxConfig(
{ tools: { sandbox: 'runsc' } },
{},
);
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc');
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
it('should prioritize GEMINI_SANDBOX over CLI and settings', async () => {
process.env['GEMINI_SANDBOX'] = 'runsc';
const config = await loadSandboxConfig(
{ tools: { sandbox: 'docker' } },
{ sandbox: 'podman' },
);
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
});
it('should reject runsc on macOS (Linux-only)', async () => {
mockedOsPlatform.mockReturnValue('darwin');
await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow(
'gVisor (runsc) sandboxing is only supported on Linux',
);
});
it('should reject runsc on Windows (Linux-only)', async () => {
mockedOsPlatform.mockReturnValue('win32');
await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow(
'gVisor (runsc) sandboxing is only supported on Linux',
);
});
it('should throw if runsc binary not found', async () => {
mockedCommandExistsSync.mockReturnValue(false);
await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow(
"Missing sandbox command 'runsc' (from GEMINI_SANDBOX)",
);
});
it('should throw if Docker not available (runsc requires Docker)', async () => {
mockedCommandExistsSync.mockImplementation((cmd) => cmd === 'runsc');
await expect(loadSandboxConfig({}, { sandbox: 'runsc' })).rejects.toThrow(
"runsc (gVisor) requires Docker. Install Docker, or use sandbox: 'docker'.",
);
});
it('should NOT auto-detect runsc when both runsc and docker available', async () => {
mockedCommandExistsSync.mockImplementation(
(cmd) => cmd === 'runsc' || cmd === 'docker',
);
const config = await loadSandboxConfig({}, { sandbox: true });
expect(config?.command).toBe('docker');
expect(config?.command).not.toBe('runsc');
});
});
});
+5 -19
View File
@@ -27,7 +27,6 @@ const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
'docker',
'podman',
'sandbox-exec',
'runsc',
'lxc',
];
@@ -65,30 +64,17 @@ function getSandboxCommand(
)}`,
);
}
// runsc (gVisor) is only supported on Linux
if (sandbox === 'runsc' && os.platform() !== 'linux') {
throw new FatalSandboxError(
'gVisor (runsc) sandboxing is only supported on Linux',
);
}
// confirm that specified command exists
if (!commandExists.sync(sandbox)) {
throw new FatalSandboxError(
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
);
if (commandExists.sync(sandbox)) {
return sandbox;
}
// runsc uses Docker with --runtime=runsc; both must be available (prioritize runsc when explicitly chosen)
if (sandbox === 'runsc' && !commandExists.sync('docker')) {
throw new FatalSandboxError(
"runsc (gVisor) requires Docker. Install Docker, or use sandbox: 'docker'.",
);
}
return sandbox;
throw new FatalSandboxError(
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
);
}
// look for seatbelt, docker, or podman, in that order
// for container-based sandboxing, require sandbox to be enabled explicitly
// note: runsc is NOT auto-detected, it must be explicitly specified
if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
return 'sandbox-exec';
} else if (commandExists.sync('docker') && sandbox === true) {
+2 -2
View File
@@ -20,8 +20,8 @@ import {
type AdminControlsSettings,
} from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
import { DefaultDark } from '../ui/themes/builtin/dark/default-dark.js';
import { DefaultLight } from '../ui/themes/default-light.js';
import { DefaultDark } from '../ui/themes/default.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import {
type Settings,
@@ -1846,15 +1846,6 @@ const SETTINGS_SCHEMA = {
'Enable model steering (user hints) to guide the model during tool execution.',
showInDialog: true,
},
continuousSession: {
type: 'boolean',
label: 'Continuous Session',
category: 'Experimental',
requiresRestart: false,
default: false,
description: 'Enables tool and prompts for a continuous session effect.',
showInDialog: true,
},
directWebFetch: {
type: 'boolean',
label: 'Direct Web Fetch',
+3 -3
View File
@@ -79,7 +79,7 @@ import {
type InitializationResult,
} from './core/initializer.js';
import { validateAuthMethod } from './config/auth.js';
import { runAcpClient } from './acp/acpClient.js';
import { runZedIntegration } from './zed-integration/zedIntegration.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
@@ -672,8 +672,8 @@ export async function main() {
await getOauthClient(settings.merged.security.auth.selectedType, config);
}
if (config.getAcpMode()) {
return runAcpClient(config, settings, argv);
if (config.getExperimentalZedIntegration()) {
return runZedIntegration(config, settings, argv);
}
let input = config.getQuestion();
+2 -2
View File
@@ -179,7 +179,7 @@ describe('gemini.tsx main function cleanup', () => {
vi.restoreAllMocks();
});
it.skip('should log error when cleanupExpiredSessions fails', async () => {
it('should log error when cleanupExpiredSessions fails', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
@@ -216,7 +216,7 @@ describe('gemini.tsx main function cleanup', () => {
getMcpServers: () => ({}),
getMcpClientManager: vi.fn(),
getIdeMode: vi.fn(() => false),
getAcpMode: vi.fn(() => true),
getExperimentalZedIntegration: vi.fn(() => true),
getScreenReader: vi.fn(() => false),
getGeminiMdFileCount: vi.fn(() => 0),
getProjectRoot: vi.fn(() => '/'),
@@ -1,107 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Continuous Session Integration > should handle checkpoint_state and manual compress tools correctly > 1-before-checkpoint 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
{
"text": "Start the mission PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD",
},
],
"role": "user",
},
]
`;
exports[`Continuous Session Integration > should handle checkpoint_state and manual compress tools correctly > 2-with-checkpoint 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
{
"text": "<state_checkpoint>
GOAL: Implementation of session continuity.
PROGRESS: Tools implemented.
CONSTRAINT: Use high-fidelity summary.
</state_checkpoint>",
},
{
"text": "Start the mission PADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPADPAD",
},
],
"role": "user",
},
{
"parts": [
{
"text": "I will now checkpoint our progress.",
},
{
"functionCall": {
"args": {
"summary": "GOAL: Implementation of session continuity.
PROGRESS: Tools implemented.
CONSTRAINT: Use high-fidelity summary.",
},
"id": "<CALL_ID>",
"name": "checkpoint_state",
},
"thoughtSignature": "skip_thought_signature_validator",
},
],
"role": "model",
},
{
"parts": [
{
"functionResponse": {
"id": "<CALL_ID>",
"name": "checkpoint_state",
"response": {
"output": "First checkpoint created. No previous summary found.",
},
},
},
],
"role": "user",
},
]
`;
exports[`Continuous Session Integration > should handle checkpoint_state and manual compress tools correctly > final-curated-history 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
{
"text": "<state_checkpoint>
<state_snapshot>
<overall_goal>Implement session continuity</overall_goal>
<active_constraints>Use high-fidelity summary</active_constraints>
<key_knowledge>Tools implemented: checkpoint_state, compress</key_knowledge>
<task_state>1. [DONE] Implement tools
2. [IN PROGRESS] Verify continuity
</task_state>
</state_snapshot>
</state_checkpoint>",
},
],
"role": "user",
},
{
"parts": [
{
"text": "Compression successful. I have clear context and I remember our mission.",
},
],
"role": "model",
},
]
`;
@@ -1,124 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Distill Result Integration > should surgically replace a noisy tool result with a distilled version > 1-initial-prompt 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
{
"text": "Audit src/ for secrets",
},
],
"role": "user",
},
]
`;
exports[`Distill Result Integration > should surgically replace a noisy tool result with a distilled version > 2-request-with-noise 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
{
"text": "Audit src/ for secrets",
},
],
"role": "user",
},
{
"parts": [
{
"text": "**Thought**
I need to search for SECRET in the src/ directory.",
},
{
"functionCall": {
"args": {
"file_path": "src/foo.txt",
},
"id": "<CALL_ID>",
"name": "read_file",
},
"thoughtSignature": "skip_thought_signature_validator",
},
],
"role": "model",
},
{
"parts": [
{
"functionResponse": {
"id": "<CALL_ID>",
"name": "read_file",
"response": {
"error": "File not found: <TEST_DIR>/src/foo.txt",
},
},
},
],
"role": "user",
},
]
`;
exports[`Distill Result Integration > should surgically replace a noisy tool result with a distilled version > final-curated-history 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
{
"text": "Audit src/ for secrets",
},
],
"role": "user",
},
{
"parts": [
{
"text": "**Thought**
I need to search for SECRET in the src/ directory.",
},
{
"functionCall": {
"args": {
"file_path": "src/foo.txt",
},
"id": "<CALL_ID>",
"name": "read_file",
},
},
],
"role": "model",
},
{
"parts": [
{
"functionResponse": {
"id": "<CALL_ID>",
"name": "read_file",
"response": {
"distilled": true,
"distilled_output": "Found SECRET_KEY="12345" in src/env.ts",
"original_output_file": "<TMP_FILE>",
},
},
},
],
"role": "user",
},
{
"parts": [
{
"text": "I found the SECRET_KEY="12345" in src/env.ts after distilling the search results.",
},
],
"role": "model",
},
]
`;
@@ -1,66 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, afterEach } from 'vitest';
import { AppRig } from '../test-utils/AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { PolicyDecision } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Continuous Session Integration', () => {
let rig: AppRig | undefined;
afterEach(async () => {
await rig?.unmount();
});
it('should handle checkpoint_state and manual compress tools correctly', async () => {
const fakeResponsesPath = path.join(
__dirname,
'../test-utils/fixtures/continuous_session.responses',
);
rig = new AppRig({
fakeResponsesPath,
configOverrides: {
continuousSession: true,
},
});
await rig.initialize();
rig.render();
await rig.waitForIdle();
// Use ASK_USER to pause and inspect the curated history at key moments
rig.setToolPolicy('checkpoint_state', PolicyDecision.ASK_USER);
rig.setToolPolicy('compress', PolicyDecision.ASK_USER);
// Start the quest
await rig.type('Start the mission ' + 'PAD'.repeat(100));
await rig.pressEnter();
// 1. Wait for CheckpointState tool call
await rig.waitForOutput('CheckpointState');
// Verify curated history BEFORE checkpoint is applied
expect(rig.getLastSentRequestContents()).toMatchSnapshot('1-before-checkpoint');
await rig.resolveTool('CheckpointState');
// 2. Wait for Compress tool call
await rig.waitForOutput('Compress');
// Verify curated history contains the checkpoint
expect(rig.getLastSentRequestContents()).toMatchSnapshot('2-with-checkpoint');
await rig.resolveTool('Compress');
// 3. Wait for final model response after compression
await rig.waitForOutput('Compression successful.');
await rig.waitForIdle();
// Verify the final curated history:
// - Should contain the high-fidelity snapshot
// - Should NOT contain pre-compression turns
expect(rig.getCuratedHistory()).toMatchSnapshot('final-curated-history');
});
});
@@ -1,86 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, afterEach } from 'vitest';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { AppRig } from '../test-utils/AppRig.js';
import { PolicyDecision } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Distill Result Integration', () => {
let rig: AppRig | undefined;
afterEach(async () => {
await rig?.unmount();
});
it('should surgically replace a noisy tool result with a distilled version', async () => {
const fakeResponsesPath = path.resolve(
__dirname,
'../test-utils/fixtures/distill_result.responses',
);
rig = new AppRig({
fakeResponsesPath,
configOverrides: {
continuousSession: true,
modelSteering: true,
},
});
await rig.initialize();
rig.render();
await rig.waitForIdle();
rig.setMockCommands([
{
command: /read_file/,
result: {
output: 'NOISE\n'.repeat(50) + 'SECRET_KEY="12345"\n' + 'NOISE\n'.repeat(50),
exitCode: 0,
},
},
]);
// Use ASK_USER to pause and inspect the request before each model turn
rig.setToolPolicy('read_file', PolicyDecision.ASK_USER);
rig.setToolPolicy('distill_result', PolicyDecision.ASK_USER);
// 1. Initial Prompt: Audit for secrets
await rig.sendMessage('Audit src/ for secrets');
// 2. Model calls run_shell_command (the "Noise Bomb")
await rig.waitForOutput('ReadFile');
// Verify the curated history sent to model contains the initial user prompt
expect(rig.getLastSentRequestContents()).toMatchSnapshot('1-initial-prompt');
await rig.resolveTool('ReadFile');
// 3. Model realizes it's noisy and calls distill_result
await rig.waitForOutput('DistillResult');
// Verify history now includes the massive noise
expect(rig.getLastSentRequestContents()).toMatchSnapshot('2-request-with-noise');
await rig.resolveTool('DistillResult');
// 4. Model continues from the distilled state and finishes
await rig.waitForOutput(/found the SECRET_KEY/i);
await rig.waitForIdle();
// Verify the final curated history:
// - NO noise from the original read_file
// - original read_file response is replaced with our universal distillation schema
// - intermediate thoughts and the distill_result turn itself are elided
expect(rig.getCuratedHistory()).toMatchSnapshot('final-curated-history');
// Verify final output contains the signal
const output = rig.getStaticOutput();
expect(output).toContain('SECRET_KEY');
expect(output).toContain('12345');
});
});
+1 -65
View File
@@ -150,7 +150,6 @@ export class AppRig {
private settings: LoadedSettings | undefined;
private testDir: string;
private sessionId: string;
private appRigId: string;
private pendingConfirmations = new Map<string, PendingConfirmation>();
private breakpointTools = new Set<string | undefined>();
@@ -166,7 +165,6 @@ export class AppRig {
this.testDir = fs.mkdtempSync(
path.join(os.tmpdir(), `gemini-app-rig-${uniqueId.slice(0, 8)}-`),
);
this.appRigId = path.basename(this.testDir).toLowerCase();
this.sessionId = `test-session-${uniqueId}`;
activeRigs.set(this.sessionId, this);
}
@@ -682,10 +680,9 @@ export class AppRig {
await this.waitUntil(
() => {
const frame = this.lastFrame;
const matched = typeof pattern === 'string'
return typeof pattern === 'string'
? frame.includes(pattern)
: pattern.test(frame);
return matched;
},
{
timeout,
@@ -704,67 +701,6 @@ export class AppRig {
await this.pressEnter();
}
getSentRequests() {
if (!this.config) throw new Error('AppRig not initialized');
return this.config.getContentGenerator().getSentRequests?.() || [];
}
/**
* Helper to get the curated history (contents) sent in the most recent model request.
* This method scrubs unstable data like temp paths and IDs for deterministic goldens.
*/
getLastSentRequestContents() {
const requests = this.getSentRequests();
if (requests.length === 0) return [];
const contents = requests[requests.length - 1].contents || [];
return this.scrubUnstableData(contents);
}
/**
* Gets the final curated history of the active chat session.
*/
getCuratedHistory() {
if (!this.config) throw new Error('AppRig not initialized');
const history = this.config.getGeminiClient().getChat().getHistory(true);
return this.scrubUnstableData(history);
}
private scrubUnstableData(contents: any) {
// Deeply scrub unstable data
const scrubbed = JSON.parse(
JSON.stringify(contents)
.replace(new RegExp(this.testDir, 'g'), '<TEST_DIR>')
.replace(new RegExp(this.appRigId, 'g'), '<APP_RIG_ID>')
.replace(new RegExp(this.sessionId, 'g'), '<SESSION_ID>'),
);
if (scrubbed.length > 0) {
if (scrubbed[0].parts[0].text?.includes('<session_context>')) {
scrubbed[0].parts[0].text = '<SESSION_CONTEXT>';
}
}
const removeIds = (obj: any) => {
if (Array.isArray(obj)) {
obj.forEach(removeIds);
} else if (obj && typeof obj === 'object') {
if (obj.functionCall) {
obj.functionCall.id = '<CALL_ID>';
}
if (obj.functionResponse) {
obj.functionResponse.id = '<CALL_ID>';
if (obj.functionResponse?.response?.original_output_file) {
obj.functionResponse.response.original_output_file = '<TMP_FILE>';
}
}
Object.values(obj).forEach(removeIds);
}
};
removeIds(scrubbed);
return scrubbed;
}
async unmount() {
// Clean up global state for this session
sessionStateMap.delete(this.sessionId);
@@ -1,4 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I will now checkpoint our progress."},{"functionCall":{"name":"checkpoint_state","args":{"summary":"GOAL: Implementation of session continuity.\nPROGRESS: Tools implemented.\nCONSTRAINT: Use high-fidelity summary."}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"Checkpoint created. Now I will trigger compression to clear the context."},{"functionCall":{"name":"compress","args":{}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContent","response":{"candidates":[{"content":{"role":"model","parts":[{"text":"<state_snapshot>\n<overall_goal>Implement session continuity</overall_goal>\n<active_constraints>Use high-fidelity summary</active_constraints>\n<key_knowledge>Tools implemented: checkpoint_state, compress</key_knowledge>\n<task_state>1. [DONE] Implement tools\n2. [IN PROGRESS] Verify continuity\n</task_state>\n</state_snapshot>"}]}}],"finishReason":"STOP"}}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"Compression successful. I have clear context and I remember our mission."}]},"finishReason":"STOP"}]}]}
@@ -1,3 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"**Thought**\nI need to search for SECRET in the src/ directory."},{"functionCall":{"name":"read_file","args":{"file_path":"src/foo.txt"}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"**Thought**\nThe output is very noisy. I will distill it to keep the context clean."},{"functionCall":{"name":"distill_result","args":{"revised_text":"Found SECRET_KEY=\"12345\" in src/env.ts"}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I found the SECRET_KEY=\"12345\" in src/env.ts after distilling the search results."}]},"finishReason":"STOP"}]}]}
+1 -1
View File
@@ -42,7 +42,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
setSessionId: vi.fn(),
getSessionId: vi.fn().mockReturnValue('mock-session-id'),
getContentGeneratorConfig: vi.fn(() => ({ authType: 'google' })),
getAcpMode: vi.fn(() => false),
getExperimentalZedIntegration: vi.fn(() => false),
isBrowserLaunchSuppressed: vi.fn(() => false),
setRemoteAdminSettings: vi.fn(),
isYoloModeDisabled: vi.fn(() => false),
+1 -1
View File
@@ -50,7 +50,7 @@ import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
import { createMockSettings } from './settings.js';
import { SessionStatsProvider } from '../ui/contexts/SessionContext.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
import { DefaultLight } from '../ui/themes/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
import { generateSvgForTerminal } from './svg.js';
@@ -37,7 +37,6 @@ import {
import type { Key } from '../hooks/useKeypress.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import { formatCommand } from '../utils/keybindingUtils.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import type { Config } from '@google/gemini-cli-core';
import { ApprovalMode, coreEvents, debugLogger } from '@google/gemini-cli-core';
@@ -495,7 +494,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
buffer.insert(textToInsert, { paste: true });
if (isLargePaste(textToInsert)) {
appEvents.emit(AppEvent.TransientMessage, {
message: `Press ${formatCommand(Command.EXPAND_PASTE)} to expand pasted text`,
message: 'Press Ctrl+O to expand pasted text',
type: TransientMessageType.Hint,
});
}
@@ -731,7 +730,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
buffer.handleInput(key);
if (key.sequence && isLargePaste(key.sequence)) {
appEvents.emit(AppEvent.TransientMessage, {
message: `Press ${formatCommand(Command.EXPAND_PASTE)} to expand pasted text`,
message: 'Press Ctrl+O to expand pasted text',
type: TransientMessageType.Hint,
});
}
@@ -11,6 +11,17 @@ Enter to submit · Esc to cancel
"
`;
exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 2`] = `
"Select your preferred language:
1. TypeScript
2. JavaScript
● 3. Enter a custom value
Enter to submit · Esc to cancel
"
`;
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = `
"Select your preferred language:
@@ -22,6 +33,17 @@ Enter to submit · Esc to cancel
"
`;
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 2`] = `
"Select your preferred language:
1. TypeScript
2. JavaScript
● 3. Type another language...
Enter to submit · Esc to cancel
"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
"Choose an option
@@ -36,6 +58,20 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 2`] = `
"Choose an option
● 1. Option 1
Description 1
2. Option 2
Description 2
Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 1`] = `
"Choose an option
@@ -75,6 +111,45 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 2`] = `
"Choose an option
● 1. Option 1
Description 1
2. Option 2
Description 2
3. Option 3
Description 3
4. Option 4
Description 4
5. Option 5
Description 5
6. Option 6
Description 6
7. Option 7
Description 7
8. Option 8
Description 8
9. Option 9
Description 9
10. Option 10
Description 10
11. Option 11
Description 11
12. Option 12
Description 12
13. Option 13
Description 13
14. Option 14
Description 14
15. Option 15
Description 15
16. Enter a custom value
Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
exports[`AskUserDialog > Text type questions > renders text input for type: "text" 1`] = `
"What should we name this component?
@@ -27,6 +27,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
"Overview
Add user authentication to the CLI application.
Implementation Steps
1. Create src/auth/AuthService.ts with login/logout methods
2. Add session storage in src/storage/SessionStore.ts
3. Update src/commands/index.ts to check auth status
4. Add tests in src/auth/__tests__/
Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Type your feedback...
Enter to submit · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
@@ -54,6 +81,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 2`] = `
"Overview
Add user authentication to the CLI application.
Implementation Steps
1. Create src/auth/AuthService.ts with login/logout methods
2. Add session storage in src/storage/SessionStore.ts
3. Update src/commands/index.ts to check auth status
4. Add tests in src/auth/__tests__/
Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Add tests
Enter to submit · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = `
" Error reading plan: File not found
"
@@ -140,6 +194,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
"Overview
Add user authentication to the CLI application.
Implementation Steps
1. Create src/auth/AuthService.ts with login/logout methods
2. Add session storage in src/storage/SessionStore.ts
3. Update src/commands/index.ts to check auth status
4. Add tests in src/auth/__tests__/
Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Type your feedback...
Enter to submit · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
@@ -167,6 +248,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 2`] = `
"Overview
Add user authentication to the CLI application.
Implementation Steps
1. Create src/auth/AuthService.ts with login/logout methods
2. Add session storage in src/storage/SessionStore.ts
3. Update src/commands/index.ts to check auth status
4. Add tests in src/auth/__tests__/
Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Add tests
Enter to submit · Ctrl+X to edit plan · Esc to cancel
"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = `
" Error reading plan: File not found
"
@@ -78,6 +78,27 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file
@@ -137,14 +137,6 @@ describe('<ShellToolMessage />', () => {
{ status: CoreToolCallStatus.Error, resultDisplay: 'Error output' },
undefined,
],
[
'renders in Cancelled state with partial output',
{
status: CoreToolCallStatus.Cancelled,
resultDisplay: 'Partial output before cancellation',
},
undefined,
],
[
'renders in Alternate Buffer mode while focused',
{
@@ -309,14 +309,6 @@ exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode whi
"
`;
exports[`<ShellToolMessage /> > Snapshots > renders in Cancelled state with partial output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ - Shell Command A shell command │
│ │
│ Partial output before cancellation │
"
`;
exports[`<ShellToolMessage /> > Snapshots > renders in Error state 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ x Shell Command A shell command │
@@ -25,7 +25,6 @@ import {
} from '../../utils/textUtils.js';
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
import { keyMatchers, Command } from '../../keyMatchers.js';
import { formatCommand } from '../../utils/keybindingUtils.js';
/**
* Represents a single item in the settings dialog.
@@ -626,7 +625,7 @@ export function BaseSettingsDialog({
{/* Help text */}
<Box marginX={1}>
<Text color={theme.text.secondary}>
(Use Enter to select, {formatCommand(Command.CLEAR_SCREEN)} to reset
(Use Enter to select, Ctrl+L to reset
{showScopeSelector ? ', Tab to change focus' : ''}, Esc to close)
</Text>
</Box>
@@ -288,7 +288,7 @@ describe('KeypressContext', () => {
expect.objectContaining({
name: 'escape',
shift: false,
alt: false,
alt: true,
cmd: false,
}),
);
@@ -297,7 +297,7 @@ describe('KeypressContext', () => {
expect.objectContaining({
name: 'escape',
shift: false,
alt: false,
alt: true,
cmd: false,
}),
);
@@ -326,7 +326,7 @@ describe('KeypressContext', () => {
expect.objectContaining({
name: 'escape',
shift: false,
alt: false,
alt: true,
cmd: false,
}),
);
@@ -178,7 +178,8 @@ function nonKeyboardEventFilter(
}
/**
* Converts return keys pressed quickly after insertable keys into a shift+return
* Converts return keys pressed quickly after other keys into plain
* insertable return characters.
*
* This is to accommodate older terminals that paste text without bracketing.
*/
@@ -200,7 +201,7 @@ function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler {
} else {
keypressHandler(key);
}
lastKeyTime = key.insertable ? now : 0;
lastKeyTime = now;
};
}
@@ -629,7 +630,7 @@ function* emitKeys(
} else if (sequence === `${ESC}${ESC}`) {
// Double escape
name = 'escape';
alt = false;
alt = true;
// Emit first escape key here, then continue processing
keypressHandler({
@@ -644,7 +645,7 @@ function* emitKeys(
} else if (escaped) {
// Escape sequence timeout
name = ch.length ? undefined : 'escape';
alt = ch.length > 0;
alt = true;
} else {
// Any other character is considered printable.
insertable = true;
@@ -785,8 +786,6 @@ export function KeypressProvider({
);
useEffect(() => {
terminalCapabilityManager.enableSupportedModes();
const wasRaw = stdin.isRaw;
if (wasRaw === false) {
setRawMode(true);
+3 -15
View File
@@ -1278,22 +1278,10 @@ export const useGeminiStream = (
case ServerGeminiEventType.ChatCompressed:
handleChatCompressionEvent(event.value, userMessageTimestamp);
break;
case ServerGeminiEventType.ToolCallResponse: {
const response = event.value;
if (response.resultDisplay) {
addItem(
{
type: MessageType.INFO,
text: typeof response.resultDisplay === 'string'
? response.resultDisplay
: 'Tool execution completed.',
},
userMessageTimestamp,
);
}
break;
}
case ServerGeminiEventType.ToolCallConfirmation:
case ServerGeminiEventType.ToolCallResponse:
// do nothing
break;
case ServerGeminiEventType.MaxSessionTurns:
handleMaxSessionTurnsEvent();
break;
@@ -23,7 +23,7 @@ vi.mock('../themes/theme-manager.js', () => ({
DEFAULT_THEME: { name: 'Default' },
}));
vi.mock('../themes/builtin/dark/holiday-dark.js', () => ({
vi.mock('../themes/holiday.js', () => ({
Holiday: { name: 'Holiday' },
}));
+1 -1
View File
@@ -8,7 +8,7 @@ import { useState, useEffect, useMemo } from 'react';
import { getAsciiArtWidth } from '../utils/textUtils.js';
import { debugState } from '../debug.js';
import { themeManager } from '../themes/theme-manager.js';
import { Holiday } from '../themes/builtin/dark/holiday-dark.js';
import { Holiday } from '../themes/holiday.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useTerminalSize } from './useTerminalSize.js';
import { shortAsciiLogo } from '../components/AsciiArt.js';
@@ -65,7 +65,7 @@ vi.mock('../themes/theme-manager.js', async (importOriginal) => {
};
});
vi.mock('../themes/builtin/light/default-light.js', () => ({
vi.mock('../themes/default-light.js', () => ({
DefaultLight: { name: 'default-light' },
}));
@@ -11,7 +11,7 @@ import {
shouldSwitchTheme,
} from '../themes/color-utils.js';
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
import { DefaultLight } from '../themes/builtin/light/default-light.js';
import { DefaultLight } from '../themes/default-light.js';
import { useSettings } from '../contexts/SettingsContext.js';
import type { Config } from '@google/gemini-cli-core';
import { useTerminalContext } from '../contexts/TerminalContext.js';
+32 -38
View File
@@ -32,12 +32,8 @@ describe('keyMatchers', () => {
},
{
command: Command.ESCAPE,
positive: [createKey('escape')],
negative: [
createKey('e'),
createKey('esc'),
createKey('escape', { ctrl: true }),
],
positive: [createKey('escape'), createKey('escape', { ctrl: true })],
negative: [createKey('e'), createKey('esc')],
},
// Cursor movement
@@ -196,21 +192,13 @@ describe('keyMatchers', () => {
},
{
command: Command.PAGE_UP,
positive: [createKey('pageup')],
negative: [
createKey('pagedown'),
createKey('up'),
createKey('pageup', { shift: true }),
],
positive: [createKey('pageup'), createKey('pageup', { shift: true })],
negative: [createKey('pagedown'), createKey('up')],
},
{
command: Command.PAGE_DOWN,
positive: [createKey('pagedown')],
negative: [
createKey('pageup'),
createKey('down'),
createKey('pagedown', { ctrl: true }),
],
positive: [createKey('pagedown'), createKey('pagedown', { ctrl: true })],
negative: [createKey('pageup'), createKey('down')],
},
// History navigation
@@ -226,21 +214,13 @@ describe('keyMatchers', () => {
},
{
command: Command.NAVIGATION_UP,
positive: [createKey('up')],
negative: [
createKey('p'),
createKey('u'),
createKey('up', { ctrl: true }),
],
positive: [createKey('up'), createKey('up', { ctrl: true })],
negative: [createKey('p'), createKey('u')],
},
{
command: Command.NAVIGATION_DOWN,
positive: [createKey('down')],
negative: [
createKey('n'),
createKey('d'),
createKey('down', { ctrl: true }),
],
positive: [createKey('down'), createKey('down', { ctrl: true })],
negative: [createKey('n'), createKey('d')],
},
// Dialog navigation
@@ -353,12 +333,14 @@ describe('keyMatchers', () => {
},
{
command: Command.SUSPEND_APP,
positive: [createKey('z', { ctrl: true })],
positive: [
createKey('z', { ctrl: true }),
createKey('z', { ctrl: true, shift: true }),
],
negative: [
createKey('z'),
createKey('y', { ctrl: true }),
createKey('z', { alt: true }),
createKey('z', { ctrl: true, shift: true }),
],
},
{
@@ -383,12 +365,8 @@ describe('keyMatchers', () => {
},
{
command: Command.ACCEPT_SUGGESTION_REVERSE_SEARCH,
positive: [createKey('tab')],
negative: [
createKey('return'),
createKey('space'),
createKey('tab', { ctrl: true }),
],
positive: [createKey('tab'), createKey('tab', { ctrl: true })],
negative: [createKey('return'), createKey('space')],
},
{
command: Command.FOCUS_SHELL_INPUT,
@@ -435,6 +413,22 @@ describe('keyMatchers', () => {
});
});
});
it('should properly handle ACCEPT_SUGGESTION_REVERSE_SEARCH cases', () => {
expect(
keyMatchers[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH](
createKey('return', { ctrl: true }),
),
).toBe(false); // ctrl must be false
expect(
keyMatchers[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH](createKey('tab')),
).toBe(true);
expect(
keyMatchers[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH](
createKey('tab', { ctrl: true }),
),
).toBe(true); // modifiers ignored
});
});
describe('Custom key bindings', () => {
+7 -6
View File
@@ -13,15 +13,16 @@ import { Command, defaultKeyBindings } from '../config/keyBindings.js';
* Pure data-driven matching logic
*/
function matchKeyBinding(keyBinding: KeyBinding, key: Key): boolean {
// Check modifiers:
// Check modifiers - follow original logic:
// undefined = ignore this modifier (original behavior)
// true = modifier must be pressed
// false or undefined = modifier must NOT be pressed
// false = modifier must NOT be pressed
return (
keyBinding.key === key.name &&
!!key.shift === !!keyBinding.shift &&
!!key.alt === !!keyBinding.alt &&
!!key.ctrl === !!keyBinding.ctrl &&
!!key.cmd === !!keyBinding.cmd
(keyBinding.shift === undefined || key.shift === keyBinding.shift) &&
(keyBinding.alt === undefined || key.alt === keyBinding.alt) &&
(keyBinding.ctrl === undefined || key.ctrl === keyBinding.ctrl) &&
(keyBinding.cmd === undefined || key.cmd === keyBinding.cmd)
);
}
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { lightSemanticColors } from '../../semantic-tokens.js';
import { type ColorsTheme, Theme } from './theme.js';
import { lightSemanticColors } from './semantic-tokens.js';
const ansiLightColors: ColorsTheme = {
type: 'light',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { darkSemanticColors } from '../../semantic-tokens.js';
import { type ColorsTheme, Theme } from './theme.js';
import { darkSemanticColors } from './semantic-tokens.js';
const ansiColors: ColorsTheme = {
type: 'dark',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const atomOneDarkColors: ColorsTheme = {
type: 'dark',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const ayuLightColors: ColorsTheme = {
type: 'light',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const ayuDarkColors: ColorsTheme = {
type: 'dark',
@@ -1,10 +1,10 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { lightTheme, Theme } from '../../theme.js';
import { lightTheme, Theme } from './theme.js';
export const DefaultLight: Theme = new Theme(
'Default Light',
@@ -1,10 +1,10 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { darkTheme, Theme } from '../../theme.js';
import { darkTheme, Theme } from './theme.js';
export const DefaultDark: Theme = new Theme(
'Default',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const draculaColors: ColorsTheme = {
type: 'dark',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const githubDarkColors: ColorsTheme = {
type: 'dark',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const githubLightColors: ColorsTheme = {
type: 'light',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme, lightTheme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme, lightTheme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const googleCodeColors: ColorsTheme = {
type: 'light',
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const holidayColors: ColorsTheme = {
type: 'dark',
@@ -1,12 +1,12 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { ColorsTheme } from '../theme.js';
import { Theme } from '../theme.js';
import type { SemanticColors } from '../semantic-tokens.js';
import type { ColorsTheme } from './theme.js';
import { Theme } from './theme.js';
import type { SemanticColors } from './semantic-tokens.js';
const noColorColorsTheme: ColorsTheme = {
type: 'ansi',
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -8,8 +8,8 @@
* Shades of Purple Theme for Highlight.js.
* @author Ahmad Awais <https://twitter.com/mrahmadawais/>
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const shadesOfPurpleColors: ColorsTheme = {
type: 'dark',
@@ -1,12 +1,12 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme, interpolateColor } from '../../theme.js';
import { type SemanticColors } from '../../semantic-tokens.js';
import { DEFAULT_SELECTION_OPACITY } from '../../../constants.js';
import { type ColorsTheme, Theme, interpolateColor } from './theme.js';
import { type SemanticColors } from './semantic-tokens.js';
import { DEFAULT_SELECTION_OPACITY } from '../constants.js';
const solarizedDarkColors: ColorsTheme = {
type: 'dark',
@@ -1,12 +1,12 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme, interpolateColor } from '../../theme.js';
import { type SemanticColors } from '../../semantic-tokens.js';
import { DEFAULT_SELECTION_OPACITY } from '../../../constants.js';
import { type ColorsTheme, Theme, interpolateColor } from './theme.js';
import { type SemanticColors } from './semantic-tokens.js';
import { DEFAULT_SELECTION_OPACITY } from '../constants.js';
const solarizedLightColors: ColorsTheme = {
type: 'light',
+18 -18
View File
@@ -1,23 +1,23 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { AyuDark } from './builtin/dark/ayu-dark.js';
import { AyuLight } from './builtin/light/ayu-light.js';
import { AtomOneDark } from './builtin/dark/atom-one-dark.js';
import { Dracula } from './builtin/dark/dracula-dark.js';
import { GitHubDark } from './builtin/dark/github-dark.js';
import { GitHubLight } from './builtin/light/github-light.js';
import { GoogleCode } from './builtin/light/googlecode-light.js';
import { Holiday } from './builtin/dark/holiday-dark.js';
import { DefaultLight } from './builtin/light/default-light.js';
import { DefaultDark } from './builtin/dark/default-dark.js';
import { ShadesOfPurple } from './builtin/dark/shades-of-purple-dark.js';
import { SolarizedDark } from './builtin/dark/solarized-dark.js';
import { SolarizedLight } from './builtin/light/solarized-light.js';
import { XCode } from './builtin/light/xcode-light.js';
import { AyuDark } from './ayu.js';
import { AyuLight } from './ayu-light.js';
import { AtomOneDark } from './atom-one-dark.js';
import { Dracula } from './dracula.js';
import { GitHubDark } from './github-dark.js';
import { GitHubLight } from './github-light.js';
import { GoogleCode } from './googlecode.js';
import { Holiday } from './holiday.js';
import { DefaultLight } from './default-light.js';
import { DefaultDark } from './default.js';
import { ShadesOfPurple } from './shades-of-purple.js';
import { SolarizedDark } from './solarized-dark.js';
import { SolarizedLight } from './solarized-light.js';
import { XCode } from './xcode.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { Theme, ThemeType, ColorsTheme } from './theme.js';
@@ -36,9 +36,9 @@ import {
DEFAULT_SELECTION_OPACITY,
DEFAULT_BORDER_OPACITY,
} from '../constants.js';
import { ANSI } from './builtin/dark/ansi-dark.js';
import { ANSILight } from './builtin/light/ansi-light.js';
import { NoColorTheme } from './builtin/no-color.js';
import { ANSI } from './ansi.js';
import { ANSILight } from './ansi-light.js';
import { NoColorTheme } from './no-color.js';
import process from 'node:process';
import { debugLogger, homedir } from '@google/gemini-cli-core';
@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ColorsTheme, Theme } from '../../theme.js';
import { interpolateColor } from '../../color-utils.js';
import { type ColorsTheme, Theme } from './theme.js';
import { interpolateColor } from './color-utils.js';
const xcodeColors: ColorsTheme = {
type: 'light',
@@ -98,7 +98,6 @@ describe('TerminalCapabilityManager', () => {
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(true);
});
@@ -142,8 +141,6 @@ describe('TerminalCapabilityManager', () => {
// Should resolve without waiting for timeout
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(true);
expect(manager.getTerminalBackgroundColor()).toBe('#000000');
});
@@ -159,7 +156,6 @@ describe('TerminalCapabilityManager', () => {
vi.advanceTimersByTime(1000);
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(true);
});
@@ -171,7 +167,6 @@ describe('TerminalCapabilityManager', () => {
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(false);
});
@@ -186,7 +181,6 @@ describe('TerminalCapabilityManager', () => {
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(true);
});
@@ -202,8 +196,6 @@ describe('TerminalCapabilityManager', () => {
await promise;
manager.enableSupportedModes();
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
@@ -218,8 +210,6 @@ describe('TerminalCapabilityManager', () => {
await promise;
manager.enableSupportedModes();
expect(enableModifyOtherKeys).not.toHaveBeenCalled();
});
@@ -234,7 +224,6 @@ describe('TerminalCapabilityManager', () => {
stdin.emit('data', Buffer.from('\x1b[?62c'));
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(true);
expect(enableKittyKeyboardProtocol).toHaveBeenCalled();
@@ -252,8 +241,6 @@ describe('TerminalCapabilityManager', () => {
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(false);
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
@@ -270,8 +257,6 @@ describe('TerminalCapabilityManager', () => {
await promise;
manager.enableSupportedModes();
expect(enableModifyOtherKeys).toHaveBeenCalled();
});
@@ -287,8 +272,6 @@ describe('TerminalCapabilityManager', () => {
await promise;
manager.enableSupportedModes();
expect(manager.getTerminalBackgroundColor()).toBe('#1a1a1a');
expect(manager.getTerminalName()).toBe('tmux');
@@ -304,8 +287,6 @@ describe('TerminalCapabilityManager', () => {
await promise;
manager.enableSupportedModes();
expect(manager.isKittyProtocolEnabled()).toBe(false);
expect(enableModifyOtherKeys).not.toHaveBeenCalled();
});
@@ -138,6 +138,9 @@ export class TerminalCapabilityManager {
process.stdin.setRawMode(false);
}
this.detectionComplete = true;
this.enableSupportedModes();
resolve();
};
@@ -243,11 +246,9 @@ export class TerminalCapabilityManager {
enableSupportedModes() {
try {
if (this.kittySupported) {
debugLogger.log('Enabling Kitty keyboard protocol');
enableKittyKeyboardProtocol();
this.kittyEnabled = true;
} else if (this.modifyOtherKeysSupported) {
debugLogger.log('Enabling modifyOtherKeys');
enableModifyOtherKeys();
}
// Always enable bracketed paste since it'll be ignored if unsupported.
-53
View File
@@ -573,57 +573,4 @@ describe('sandbox', () => {
});
});
});
describe('gVisor (runsc)', () => {
it('should use docker with --runtime=runsc on Linux', async () => {
vi.mocked(os.platform).mockReturnValue('linux');
const config: SandboxConfig = {
command: 'runsc',
image: 'gemini-cli-sandbox',
};
// Mock image check
interface MockProcessWithStdout extends EventEmitter {
stdout: EventEmitter;
}
const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
mockImageCheckProcess.stdout = new EventEmitter();
vi.mocked(spawn).mockImplementationOnce(() => {
setTimeout(() => {
mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
mockImageCheckProcess.emit('close', 0);
}, 1);
return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
});
// Mock docker run
const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
typeof spawn
>;
mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
if (event === 'close') {
setTimeout(() => cb(0), 10);
}
return mockSpawnProcess;
});
vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);
await start_sandbox(config, [], undefined, ['arg1']);
// Verify docker (not runsc) is called for image check
expect(spawn).toHaveBeenNthCalledWith(
1,
'docker',
expect.arrayContaining(['images', '-q', 'gemini-cli-sandbox']),
);
// Verify docker run includes --runtime=runsc
expect(spawn).toHaveBeenNthCalledWith(
2,
'docker',
expect.arrayContaining(['run', '--runtime=runsc']),
expect.objectContaining({ stdio: 'inherit' }),
);
});
});
});
+16 -45
View File
@@ -215,10 +215,7 @@ export async function start_sandbox(
return await start_lxc_sandbox(config, nodeArgs, cliArgs);
}
// runsc uses docker with --runtime=runsc
const command = config.command === 'runsc' ? 'docker' : config.command;
debugLogger.log(`hopping into sandbox (command: ${command}) ...`);
debugLogger.log(`hopping into sandbox (command: ${config.command}) ...`);
// determine full path for gemini-cli to distinguish linked vs installed setting
const gcPath = process.argv[1] ? fs.realpathSync(process.argv[1]) : '';
@@ -261,7 +258,7 @@ export async function start_sandbox(
stdio: 'inherit',
env: {
...process.env,
GEMINI_SANDBOX: command, // in case sandbox is enabled via flags (see config.ts under cli package)
GEMINI_SANDBOX: config.command, // in case sandbox is enabled via flags (see config.ts under cli package)
},
},
);
@@ -269,7 +266,9 @@ export async function start_sandbox(
}
// stop if image is missing
if (!(await ensureSandboxImageIsPresent(command, image, cliConfig))) {
if (
!(await ensureSandboxImageIsPresent(config.command, image, cliConfig))
) {
const remedy =
image === LOCAL_DEV_SANDBOX_IMAGE_NAME
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
@@ -283,17 +282,11 @@ export async function start_sandbox(
// run init binary inside container to forward signals & reap zombies
const args = ['run', '-i', '--rm', '--init', '--workdir', containerWorkdir];
// add runsc runtime if using runsc
if (config.command === 'runsc') {
args.push('--runtime=runsc');
}
// add custom flags from SANDBOX_FLAGS
if (process.env['SANDBOX_FLAGS']) {
const flags = parse(process.env['SANDBOX_FLAGS'], process.env).filter(
(f): f is string => typeof f === 'string',
);
args.push(...flags);
}
@@ -429,7 +422,7 @@ export async function start_sandbox(
// if using proxy, switch to internal networking through proxy
if (proxy) {
execSync(
`${command} network inspect ${SANDBOX_NETWORK_NAME} || ${command} network create --internal ${SANDBOX_NETWORK_NAME}`,
`${config.command} network inspect ${SANDBOX_NETWORK_NAME} || ${config.command} network create --internal ${SANDBOX_NETWORK_NAME}`,
);
args.push('--network', SANDBOX_NETWORK_NAME);
// if proxy command is set, create a separate network w/ host access (i.e. non-internal)
@@ -437,7 +430,7 @@ export async function start_sandbox(
// this allows proxy to work even on rootless podman on macos with host<->vm<->container isolation
if (proxyCommand) {
execSync(
`${command} network inspect ${SANDBOX_PROXY_NAME} || ${command} network create ${SANDBOX_PROXY_NAME}`,
`${config.command} network inspect ${SANDBOX_PROXY_NAME} || ${config.command} network create ${SANDBOX_PROXY_NAME}`,
);
}
}
@@ -456,7 +449,7 @@ export async function start_sandbox(
} else {
let index = 0;
const containerNameCheck = (
await execAsync(`${command} ps -a --format "{{.Names}}"`)
await execAsync(`${config.command} ps -a --format "{{.Names}}"`)
).stdout.trim();
while (containerNameCheck.includes(`${imageName}-${index}`)) {
index++;
@@ -606,7 +599,7 @@ export async function start_sandbox(
args.push('--env', `SANDBOX=${containerName}`);
// for podman only, use empty --authfile to skip unnecessary auth refresh overhead
if (command === 'podman') {
if (config.command === 'podman') {
const emptyAuthFilePath = path.join(os.tmpdir(), 'empty_auth.json');
fs.writeFileSync(emptyAuthFilePath, '{}', 'utf-8');
args.push('--authfile', emptyAuthFilePath);
@@ -670,38 +663,16 @@ export async function start_sandbox(
if (proxyCommand) {
// run proxyCommand in its own container
// build args array to prevent command injection
const proxyContainerArgs = [
'run',
'--rm',
'--init',
...(userFlag ? userFlag.split(' ') : []),
'--name',
SANDBOX_PROXY_NAME,
'--network',
SANDBOX_PROXY_NAME,
'-p',
'8877:8877',
'-v',
`${process.cwd()}:${workdir}`,
'--workdir',
workdir,
image,
// proxyCommand may be a shell string, so parse it into tokens safely
...parse(proxyCommand, process.env).filter(
(f): f is string => typeof f === 'string',
),
];
proxyProcess = spawn(command, proxyContainerArgs, {
const proxyContainerCommand = `${config.command} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
proxyProcess = spawn(proxyContainerCommand, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false, // <-- no shell; args are passed directly
shell: true,
detached: true,
});
// install handlers to stop proxy on exit/signal
const stopProxy = () => {
debugLogger.log('stopping proxy container ...');
execSync(`${command} rm -f ${SANDBOX_PROXY_NAME}`);
execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`);
};
process.off('exit', stopProxy);
process.on('exit', stopProxy);
@@ -722,7 +693,7 @@ export async function start_sandbox(
process.kill(-sandboxProcess.pid, 'SIGTERM');
}
throw new FatalSandboxError(
`Proxy container command '${command} ${proxyContainerArgs.join(' ')}' exited with code ${code}, signal ${signal}`,
`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
});
debugLogger.log('waiting for proxy to start ...');
@@ -732,13 +703,13 @@ export async function start_sandbox(
// connect proxy container to sandbox network
// (workaround for older versions of docker that don't support multiple --network args)
await execAsync(
`${command} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`,
`${config.command} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`,
);
}
// spawn child and let it inherit stdio
process.stdin.pause();
sandboxProcess = spawn(command, args, {
sandboxProcess = spawn(config.command, args, {
stdio: 'inherit',
});
@@ -13,7 +13,7 @@ import {
type Mocked,
type Mock,
} from 'vitest';
import { GeminiAgent } from './acpClient.js';
import { GeminiAgent } from './zedIntegration.js';
import * as acp from '@agentclientprotocol/sdk';
import {
ApprovalMode,
@@ -14,7 +14,7 @@ import {
type Mock,
type Mocked,
} from 'vitest';
import { GeminiAgent, Session } from './acpClient.js';
import { GeminiAgent, Session } from './zedIntegration.js';
import type { CommandHandler } from './commandHandler.js';
import * as acp from '@agentclientprotocol/sdk';
import {
@@ -208,16 +208,7 @@ describe('GeminiAgent', () => {
});
expect(response.protocolVersion).toBe(acp.PROTOCOL_VERSION);
expect(response.authMethods).toHaveLength(4);
const gatewayAuth = response.authMethods?.find(
(m) => m.id === AuthType.GATEWAY,
);
expect(gatewayAuth?._meta).toEqual({
gateway: {
protocol: 'google',
restartRequired: 'false',
},
});
expect(response.authMethods).toHaveLength(3);
const geminiAuth = response.authMethods?.find(
(m) => m.id === AuthType.USE_GEMINI,
);
@@ -237,8 +228,6 @@ describe('GeminiAgent', () => {
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
undefined,
undefined,
undefined,
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
@@ -258,8 +247,6 @@ describe('GeminiAgent', () => {
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.USE_GEMINI,
'test-api-key',
undefined,
undefined,
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
@@ -268,45 +255,6 @@ describe('GeminiAgent', () => {
);
});
it('should authenticate correctly with gateway method', async () => {
await agent.authenticate({
methodId: AuthType.GATEWAY,
_meta: {
gateway: {
baseUrl: 'https://example.com',
headers: { Authorization: 'Bearer token' },
},
},
} as unknown as acp.AuthenticateRequest);
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.GATEWAY,
undefined,
'https://example.com',
{ Authorization: 'Bearer token' },
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'security.auth.selectedType',
AuthType.GATEWAY,
);
});
it('should throw acp.RequestError when gateway payload is malformed', async () => {
await expect(
agent.authenticate({
methodId: AuthType.GATEWAY,
_meta: {
gateway: {
// Invalid baseUrl
baseUrl: 123,
headers: { Authorization: 'Bearer token' },
},
},
} as unknown as acp.AuthenticateRequest),
).rejects.toThrow(/Malformed gateway payload/);
});
it('should create a new session', async () => {
vi.useFakeTimers();
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
@@ -70,7 +70,7 @@ import { runExitCleanup } from '../utils/cleanup.js';
import { SessionSelector } from '../utils/sessionUtils.js';
import { CommandHandler } from './commandHandler.js';
export async function runAcpClient(
export async function runZedIntegration(
config: Config,
settings: LoadedSettings,
argv: CliArgs,
@@ -98,8 +98,6 @@ export class GeminiAgent {
private sessions: Map<string, Session> = new Map();
private clientCapabilities: acp.ClientCapabilities | undefined;
private apiKey: string | undefined;
private baseUrl: string | undefined;
private customHeaders: Record<string, string> | undefined;
constructor(
private config: Config,
@@ -133,17 +131,6 @@ export class GeminiAgent {
name: 'Vertex AI',
description: 'Use an API key with Vertex AI GenAI API',
},
{
id: AuthType.GATEWAY,
name: 'AI API Gateway',
description: 'Use a custom AI API Gateway',
_meta: {
gateway: {
protocol: 'google',
restartRequired: 'false',
},
},
},
];
await this.config.initialize();
@@ -192,38 +179,7 @@ export class GeminiAgent {
if (apiKey) {
this.apiKey = apiKey;
}
// Extract gateway details if present
const gatewaySchema = z.object({
baseUrl: z.string().optional(),
headers: z.record(z.string()).optional(),
});
let baseUrl: string | undefined;
let headers: Record<string, string> | undefined;
if (meta?.['gateway']) {
const result = gatewaySchema.safeParse(meta['gateway']);
if (result.success) {
baseUrl = result.data.baseUrl;
headers = result.data.headers;
} else {
throw new acp.RequestError(
-32602,
`Malformed gateway payload: ${result.error.message}`,
);
}
}
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
headers,
);
await this.config.refreshAuth(method, apiKey ?? this.apiKey);
} catch (e) {
throw new acp.RequestError(-32000, getAcpErrorMessage(e));
}
@@ -253,12 +209,7 @@ export class GeminiAgent {
let isAuthenticated = false;
let authErrorMessage = '';
try {
await config.refreshAuth(
authType,
this.apiKey,
this.baseUrl,
this.customHeaders,
);
await config.refreshAuth(authType, this.apiKey);
isAuthenticated = true;
// Extra validation for Gemini API key
@@ -420,12 +371,7 @@ export class GeminiAgent {
// This satisfies the security requirement to verify the user before executing
// potentially unsafe server definitions.
try {
await config.refreshAuth(
selectedAuthType,
this.apiKey,
this.baseUrl,
this.customHeaders,
);
await config.refreshAuth(selectedAuthType, this.apiKey);
} catch (e) {
debugLogger.error(`Authentication failed: ${e}`);
throw acp.RequestError.authRequired();
@@ -147,7 +147,7 @@ describe('BrowserManager', () => {
// Verify StdioClientTransport was created with correct args
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
command: 'npx',
args: expect.arrayContaining([
'-y',
expect.stringMatching(/chrome-devtools-mcp@/),
@@ -185,7 +185,7 @@ describe('BrowserManager', () => {
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
command: 'npx',
args: expect.arrayContaining(['--headless']),
}),
);
@@ -210,7 +210,7 @@ describe('BrowserManager', () => {
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
command: 'npx',
args: expect.arrayContaining(['--userDataDir', '/path/to/profile']),
}),
);
@@ -283,7 +283,7 @@ export class BrowserManager {
// stderr is piped (not inherited) to prevent MCP server banners and
// warnings from corrupting the UI in alternate buffer mode.
this.mcpTransport = new StdioClientTransport({
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
command: 'npx',
args: mcpArgs,
stderr: 'pipe',
});
@@ -63,7 +63,7 @@ import {
} from './types.js';
import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
import type { ToolCallRequestInfo } from '../scheduler/types.js';
import { CompressionStatus } from '../core/compression-status.js';
import { CompressionStatus } from '../core/turn.js';
import { ChatCompressionService } from '../services/chatCompressionService.js';
import type {
ModelConfigKey,
+1 -5
View File
@@ -7,7 +7,6 @@
import type { Config } from '../config/config.js';
import { reportError } from '../utils/errorReporting.js';
import { GeminiChat, StreamEventType } from '../core/geminiChat.js';
import { Turn } from '../core/turn.js';
import {
Type,
type Content,
@@ -18,7 +17,7 @@ import {
} from '@google/genai';
import { ToolRegistry } from '../tools/tool-registry.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { CompressionStatus } from '../core/compression-status.js';
import { CompressionStatus } from '../core/turn.js';
import { type ToolCallRequestInfo } from '../scheduler/types.js';
import { ChatCompressionService } from '../services/chatCompressionService.js';
import { getDirectoryContextString } from '../utils/environmentContext.js';
@@ -751,7 +750,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
}
const role = LlmRole.SUBAGENT;
const turnId = Turn.generateId();
const responseStream = await chat.sendMessageStream(
{
@@ -762,8 +760,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
promptId,
signal,
role,
undefined,
turnId,
);
const functionCalls: FunctionCall[] = [];
@@ -49,16 +49,15 @@ export function resolvePolicyChain(
const useCustomToolModel =
useGemini31 &&
config.getContentGeneratorConfig?.()?.authType === AuthType.USE_GEMINI;
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
const resolvedModel = resolveModel(
modelFromConfig,
useGemini31,
useCustomToolModel,
hasAccessToPreview,
);
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
const isAutoConfigured = isAutoModel(configuredModel);
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
chain = getFlashLitePolicyChain();
@@ -81,7 +80,7 @@ export function resolvePolicyChain(
} else {
// User requested Gemini 3 but has no access. Proactively downgrade
// to the stable Gemini 2.5 chain.
chain = getModelPolicyChain({
return getModelPolicyChain({
previewEnabled: false,
userTier: config.getUserTier(),
useGemini31,
+1 -1
View File
@@ -109,7 +109,7 @@ const mockConfig = {
getNoBrowser: () => false,
getProxy: () => 'http://test.proxy.com:8080',
isBrowserLaunchSuppressed: () => false,
getAcpMode: () => false,
getExperimentalZedIntegration: () => false,
isInteractive: () => true,
} as unknown as Config;

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